The Architecture Behind Acceptance Tests That Don’t Break
How four layers turn fragile tests into executable specifications
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.
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.

