Cover photo by israel palacio on Unsplash
Context
Contract testing is a known concept. It is nicely described in the Contract Tests section of the excellent article "The Practical Test Pyramid" by Ham Vocke.
Takeaway points:
- Test consumer & producer together
- Test only the contract layers, without the behaviour of either the consumer or producer
At Software Sauna we developed a similar approach, albeit with some modifications to fit our own flow of developing web apps:
- We do not test on the HTTP layer - tests interact with a domain-specific layer which hides the data transport infrastructure (here called API functions).
- We do not setup test-double behaviour on the producer side. Rather, we expose test utility endpoints and drive test double behaviour from tests directly so both input & expected data are visible in test.
The web app
Software Sauna predominantly produces web apps & not distributed systems, so most of contracts we deal with are those between the web UI usually called frontend & the behaviour & busines logic of the app usually called backend.
The frontend communicates with the backend by sending HTTP requests.
We want readable & maintainable code, so we slice our frontend & backend codebases into abstraction layers. This results in seams throughout the codebases, enabling us to have tests on relevant scopes.
One such seam is between code that implements behaviour (does stuff) & code that deals with HTTP communication.
Now we can look at the frontend's API-related layer, the HTTP channel & backend's API-related layer as a single component - the contract.
The contract has one responsibility, that is to transfer data from frontend to backend & back.
However, other things related to this communication are also part of it, like authentication, authorisation, throttling, etc.
Here is an example of a web app stack with the contract components in more detail:
To test the contract, we just replace the frontend & backend behaviours with tests & test doubles, respectively:
Example test
Every API test has two parts:
- Frontend test
- Backend test double replacing the actual service
The test in the example tests the API function getUsers()
, which will be called by components who want to display a list of the system's users.
Test
Line 9: Log in as a specific user. This tests both frontend & backend authentication implementation & config. It is practical to create a single dummy user for each user role in the app.
Line 16: Stub the method labelled get-users
with some test data.
Line 18: Call the API function which should result in a method call to the appropriate backend service (actually test double).
Line 20: Assert that the stubbed data is returned from the API function.
There is also an example of an unauthorised call to show that we can test the backend's authorisation config.
Test double
This example uses Spring to auto-wire services (actually, their test doubles) into the DI context.
The method labels (as string parameters in the withMethod
calls) have to match string parameters in frontend-side calls to setStubbing
& failWith
. If they don't, the tests will fail.
Spying
Sometimes we want to know if the desired arguments were passed to the backend method.
Our backend test double acts both as a stub and a spy, enabling the frontend test to check returned values & also check that the correct arguments were sent.
Other stuff
Testing error handling
Similar to stubbing values, we can tell the backend test double that a method should fail. This allows us to test:
- backend mapping of failures (i.e. exceptions) to HTTP error responses
- frontend handling of errors
Part of error handling was already shown in the example for unauthorised calls.
Test-driving the API layer
This approach allows the API layers to be implemented within a TDD workflow.
We test-drive a UI component. The component requires data from the backend. As this is crossing the seam, we test-drive the implementation using a test double to replace the API function.
That test double defines the API function signature.
Then we can test-drive the API function itself, by providing a test-double on the backend side, which in turn defines the service method signature.
Conclusion
During our usage of this pattern in Software Sauna, we have found that it completely eliminates common errors like request/response structure definition discrepancies, but also harder to find errors buried in URL path & header definitions, error handling, authentication & authorisation setup.
Top comments (0)