stack twitter rss linkedin cross

Wilco van Esch

Skip to main content

Search results

    What do bi-directional contract testing flows look like?

    It's pretty easy to find an explanation of bi-directional contract testing, but much harder if not impossible to find concrete explanations of how it impacts team workflows for realistic scenarios. Here's an attempt.

    Background

    Team Contractors' API endpoint /contractor contains parameters such as available and stock.

    When assigning a contractor to a job, Team Odd Jobs consumes the endpoint, using available to retrieve available contractors.

    When calculating total stock, Team Materials consumes the endpoint, using stock to retrieve active stock.

    Now Team Contractors would like to make a change to their /contractor endpoint, changing the stock data type from a string to an integer.

    Without bi-directional contract testing

    Let's say there is some testing in place.

    There is a test that checks the GET request on /contractor returns a 200 response. This should PASS.

    There is a test that checks the method that works with stock is supplied with a string. This should FAIL.

    Or maybe there is a test that checks the full API response structure against a JSON schema. This should FAIL.

    Team Contractors attempt to merge their change to production.

    The pipeline succeeds.

    For Team Odd Jobs this is fine, but places where Team Materials retrieve the value for stock from the /contractor endpoint fail, not with a 400 Bad Request on the request itself (they are doing a simple GET on /contractor and taking the value from something like response.data.stock), but at the point the value is used in the code with some method expecting to work with a string.

    Why did the tests not stop the breaking change?

    Because those tests run in Team Materials' pipeline, not in that of Team Contractors.

    Each team is able to independently deploy their service, with build and test stages covering only their own service and mocking or virtualising any interaction with other services. There are companies who, despite having a microservices architecture in every other respect, solve this by having a stage in the pipeline of each service that builds all services and runs integration and end-to-end tests in order to safeguard interactions between services. This is antithetical to the idea of independently deploying. There is an alternative that's (almost) as safe and allows us to move a lot faster.

    With bi-directional contract testing

    Now let's say we have bi-directional contract testing (BDCT) in place.

    Team Contractors attempt to merge their change to production.

    Their pipeline fails. It fails at the point where the provider-side contract containing the changed data type is statically compared (by a contract broker) against the latest consumer-side contracts, which in the case of Team Materials still defines an expectation for the old data type. Team Odd Jobs uses the same endpoint, but doesn't do anything with the stock parameter, so their contract doesn't even include stock and the contract comparison passes. This consumer is not affected.

    Team Contractors have discovered their breaking change and can instead handle the change properly, such as by applying the expand and contract pattern or by communicating with Team Materials about being more liberal in what they accept or by some other method the team finds appropriate.

    Flow details

    This is what happens in BDCT when a provider (in this case Team Contractors) makes a change:

    1. Team Contractors makes a change to their API specification.
    2. Team Contractors makes the same change to their API endpoint.
    3. The pipeline runs automated schema validation checks comparing the API specification against the API endpoint.
    4. The pipeline runs any additional human-coded functional API checks against the API endpoint.
    5. The pipeline generates a provider-side contract from the API specification, now that we've proven it matches the actual endpoint.
    6. The pipeline returns a response from the contract broker, which statically compares the provider-side contract against the latest consumer-side contracts that use the endpoint.

    This is what happens in BDCT when a consumer (in this case Team Materials) makes a change:

    1. Team Materials changes their mocked test which defines what they need from the provider endpoint.
    2. Team Materials makes the same change to the actual request.
    3. The pipeline generates a consumer-side contract from the request-response pair the team's mocked test defines.
    4. The pipeline returns a response from the contract broker, which statically compares the consumer-side contract against the latest provider-side contract for the endpoint.

    In this case of a deliberate change on the consumer side, a failure would mean they should communicate with the provider about the new needs they have, so the provider can update their capabilities. Of course they should be doing this before actually making the change and seeing the pipeline fail, BDCT is just there to say "what do you think you're doing?" in case this does not happen or when a valid expectation is inadvertently changed to an invalid one.

    Who are the providers and consumers?

    The flows apply regardless of whether the providers and consumers are:

    • Microservices interacting with each other (and each could be a provider, a consumer, or both)
    • The backend (provider) and frontend (consumer) within a service
    • External services, if we can agree to upload contracts to a broker one of us owns