Optivem Journal

Optivem Journal

Automated Testing

The Architecture Behind Acceptance Tests That Don’t Break

How four layers turn fragile tests into executable specifications

Valentina Jemuović's avatar
Valentina Jemuović
Mar 19, 2026
∙ Paid

This test doesn’t lie:

scenario
    .given().product().withUnitPrice(20.00)
    .and().country().withCode("US").withTaxRate(0.08)
    .when().placeOrder().withQuantity(5).withCountry("US")
    .then().shouldSucceed()
    .and().order()
        .hasBasePrice(100.00)
        .hasTaxAmount(8.00)
        .hasTotalPrice(108.00);

Clean. Readable. Runs in seconds. Survives refactors.

But how does it actually work?

What’s behind .given().product().withUnitPrice(20.00)? How does the same test run against both the API and the UI? Where does the “magic” come from?

I’m going to take this test apart, layer by layer, and show you exactly how the DSL is built.


⚡Ready to see this in action?

I’m running a live, hands-on workshop where we build acceptance tests using this exact architecture. Every Release Is a Nightmare doesn’t have to be your reality.

Join the workshop →

Limited spots. Register now with the early bird discount - 100 EUR off with code EARLYBIRD100


The four layers

An acceptance test DSL has four layers. Each layer has one job, and they only talk to the layer directly below them:

Test           →  what we're testing (the scenario)
DSL            →  how we express it (the fluent API)
Driver Port    →  what we need from the system (interfaces)
Driver Adapter →  how we interact with the system (API calls, browser clicks)

This separation is the entire reason acceptance tests are maintainable. Remove any layer and the whole thing falls apart.


Layer 1: The Test

This is what you’ve already seen. A test method that reads like a business scenario:

@TestTemplate
@Channel({ChannelType.UI, ChannelType.API})
void shouldCalculateCorrectTotalWithTaxRate() {
    scenario
        .given().product()
            .withUnitPrice(20.00)
        .and().country()
            .withCode("US")
            .withTaxRate(0.08)
        .when().placeOrder()
            .withQuantity(5)
            .withCountry("US")
        .then().shouldSucceed()
        .and().order()
            .hasBasePrice(100.00)
            .hasTaxAmount(8.00)
            .hasTotalPrice(108.00);
}

Notice what’s not here:

  • No URLs

  • No HTTP calls

  • No CSS selectors

  • No database queries

  • No setup or teardown

The test only knows what it’s testing. It has no idea how the system works under the hood. That’s the point.

The @Channel annotation is interesting — it tells the test runner to execute this same test once through the API and once through the UI. Same scenario, same assertions, two completely different ways of talking to the system. Here’s how it actually works.

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2026 Valentina Jemuović, Optivem · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture