Skip to main content

Test data

By default, the data used in an example is strictly matched. However, this can be inconvenient for many provider teams. For example, sometimes it's not possible to know user IDs before the users have been created. Alternatively, it can be frustrating to have to maintain the exact same test data between the consumer test and the provider verification.

You can use Test Equivalence Matchers and state variables to avoid needing to keep the test data in sync between the contract definition and verification.

Test Equivalence Matchers

A test equivalence matcher says "this test is equivalent to one that passes this matcher"

For example, during a test for a client's getUser call, we might not mind what the specific name of the user is, just that the user name was a valid, non-empty string.

At definition time, we need a specific example because we do need to confirm that we read it correctly in the testResponse function. However, we might not need to use the same example at verification time.

In the examples in the earlier sections, we used john smith as the example username. At provider time, we would be happy to accept any example that was a string, and we would be quite confident that the contract was still satisfied.

We can express this with the anyString(example?: string) Test Equivalence Matcher:

body: {
userId: 'foo'
// You can read this as
// "this test covers any name property that is a string (eg 'john smith').
name: anyString('john smith'),
},

Parametrising the test with state variables

Sometimes, there are constraints on the provider team that are hard to know from the consumer side. For example, perhaps their test data is constrained in some way, and so it is hard to set up a user with the ID 'foo'. To allow them to specify some of the state, state variables can be used. Instead of:

        inState('A user with id "foo" exists'),

you can provide a state variable (and a default value) by providing an object keyed by the variable name:

        // inState(
// stateName: string,
// defaultValues: Record<string, AnyConstraintOrData>
// )
inState('A user exists', { userId: '123' }),

This can later be accessed using the stateVariable(name: string) Test Equivalence Matcher. You can also access the variables using the config object passed to the trigger and testResponse functions:

await contract.runExample({
states: [
inState('Server is up'),
inState('A user exists', { userId: '123' }),
],
definition: willSendHttpRequest({
request: {
method: 'GET',
path: '/users',
query: { id: stateVariable('userId') },
},
response: {
status: 200,
body: {
userId: stateVariable('userId'),
name: anyString('John Smith')
},
},
}),
{
trigger: (config: HttpRequestConfig) =>
new YourApi(config.baseUrl).getUser(config.variables['userId']),

testResponse: (user, config) => {
expect(user).toEqual({
userId: config.variables['userId'],
name: 'John Smith'
});
},
}
});

The default value passed to the state variable can be a Test Equivalence Matcher, so you could add additional constraints:

inState('a user has a score that is an integer', { userScore: anyInteger(10) });

This would prevent the state handler from returning any userScore value that doesn't pass the anyInteger constraint.

Note: Remember that the goal here is not to describe the API, it's to describe the constraints of the service defining the contract. So, if the userScore field is an integer, but the client would actually be happy with any number, you don't need to constrain it.

As a convenience, all default values passed to state variables are automatically wrapped in a shapedLike() constraint. This means that userId: 'foo' will accept any string value, and not just the exact string 'foo'.

Improving your contract

ContractCase has a number of convenience methods and approaches for writing tests concisely and without need for intensive maintenance effort. See the section on convenient definitions for more information.

Next steps

If you are defining a contract from an HTTP client, you will need to provide triggers to allow ContractCase to invoke your code.

If you are defining a contract from an HTTP server, you will need to provide state handlers to allow ContractCase to set up any preconditions.

Once you have created your contract, you'll want to upload it to a broker.