🏆TDD in Legacy Code - Acceptance Tests & External System Contract Tests (Summary)
Acceptance Tests verify System Behavior. How do we separate System Behavior from System Structure using the Four Layer Model?
📢 My next Live Q&A Session about Unit Tests is on Wed 30th April 17:00. If you’re an Optivem Journal paid member, get your 100% discount ticket.
Upgrade to join the Live Q&A for free:
Acceptance Tests help us verify System Behavior from the User Perspective. Thus, they are business-facing tests.
Acceptance Tests are black-box tests that are agnostic to the underlying System Architecture. Thus, the Acceptance Tests should remain the same irrespective of whether we have an MVP app, or Frontend & Monolithic Backend, or Frontend & Microservice Backend. It also doesn’t matter whether our system is well-designed or whether it’s a Big Ball of Mud. Acceptance Tests don’t care about this.
But how do we handle the problem of UX/UI changes when we’d have to rewrite all our Acceptance Tests? What about when we have multiple channels - e.g. one Web UI, one Mobile UI, and perhaps a public-facing REST API - do we then have to write three tests for each functionality?
Fortunately, Dave Farley came up with a solution to this problem - if we write tests that are expressed in terms of the domain language (the DSL), rather than being expressed in terms of the UI, we can then write a test that is insensitive to UX/UI changes and such that the single test can be executed against multiple channels (Web UI, Mobile UI, Desktop UI, public REST API).
Acceptance Tests - Four Layer Model
In the Four Layer Model, the Acceptance Test calls the DSL, which calls the Drivers, and finally the Drivers call the Systems.
We can see below that the Acceptance Test calls the System DSL to execute operations on the System, and it calls the External System Stub DSL to execute operations on the External System Stub. The DSL is written in the domain language.
The DSL needs to communicate with the underlying Systems. The System DSL needs to communicate with the System via System Driver(s). The External System Stub DSL needs to communicate with the External System Stub via External System Stub Driver.
Note: Why communicate with the External System Stub, rather than the Real External System? It’s because the External System Stub enables us to have full control over the External System, we can execute any scenario on the System. This is contrasted with E2E Tests, which do not use Stubs, and thus have limited scenarios that can be executed.
Note: In the image above, there’s only one External System. If there’s multiple external systems, then for each one you’d have a corresponding Stub DSL and Stub Driver (e.g. External System A Stub DSL, External System A Stub Driver).
You can read more about Farley’s Four Layer Model.
External System Contract Tests - Three Layer Model
Above, we saw that Acceptance Tests rely on External System Stubs, rather than connecting to the (real) External System Test Instance.
But what if the External System Stub doesn’t match the Real External System? Then, our Acceptance Tests could pass, but yet it wouldn’t work in production.
In order to have assurance that our External System Stub matches the Real External System, we need to write External System Contract Tests.
The External System Contract Test calls the External System Driver. This is done both for the “Real“ and the “Stub“ to verify that the “Stub“ matches the “Real“. This provides us assurance that our assumptions about External Systems are correct, thus we can trust our Acceptance Tests.
Sample User Story
Let’s start with this sample User Story:
User Story: Place an Order
As a customer, I want to place an order for a product, so that I can purchase items I need.
Acceptance Criteria #1: Successfully place an order for "Apple"
Given the ERP is set up with the product "APPLE1001" and price $1.99
When the product "APPLE1001" is added to the cart with quantity 5
And the customer details are provided with name "John", address "My Street", and card number "123451234512"
And the order is placed
Then the order confirmation message should be "You have successfully placed your order"
Acceptance Criteria #2: Fail to place an order due to negative quantity
Given the ERP is set up with the product "APPLE1001" and price $1.99
When the product "Apple" is added to the cart with quantity -2
And the customer details are provided with name "John", address "My Street", and card number "123451234512"
And the order is placed
Then an error message should be displayed indicating invalid quantity
Acceptance Criteria #3: ....
...
Now select the first Acceptance Criteria:
Acceptance Criteria #1: Successfully place an order for "Apple"
Given the ERP is set up with the product "APPLE1001" and price $1.99
When the product "APPLE1001" is added to the cart with quantity 5
And the customer details are provided with name "John", address "My Street", and card number "123451234512"
And the order is placed
Then the order confirmation message should be "You have successfully placed order <Order number>"
How do we write the Acceptance Test & External System Test(s)? How do we implement the Test Architecture such that System Behavior is separated from System Structure?
Let’s see some examples…