Defining a ContractCase Example
In ContractCase, a contract is a series of examples. Each example is independent - for example: "a getUser request looks like $X, and a success response looks like $Y":
- Typescript
- Java
import { willSendHttpRequest } from '@contract-case/contract-case-jest';
await contract.runExample({
definition: willSendHttpRequest({
request: {
method: 'GET',
path: '/users/foo',
},
response: {
status: 200,
body: {
// Note that we only describe the fields
// that your consumer actually needs for
// this particular test. The real response
// might have more elements, but if your
// consumer doesn't need them, you don't
// need to put them in the contract.
userId: 'foo',
name: 'john smith',
},
},
}),
});
public void testGetUser() {
contract.runExample(
new ExampleDefinition<>(
List.of(), // State definitions, covered below
new WillSendHttpRequest(HttpExample.builder()
.request(
new HttpRequest(HttpRequestExample.builder()
.method("GET")
.path("/users/foo")
.build())
)
.response(new HttpResponse(HttpResponseExample.builder()
.status(200)
.body(
// Note that we only describe the fields
// that your consumer actually needs for
// this particular test. The real response
// might have more elements, but if your
// consumer doesn't need them, you don't
// need to put them in the contract.
Map.ofEntries(
Map.entry("status", "up"),
Map.entry("name", "john smith")
)
)
.build()))
.build())
),
);
}
Examples should be independent
Instead of making examples dependent on each other (and the corresponding explosion of complexity and potentially brittle tests), each contract definition example runs independently.
This is achieved with state definitions and state setup
handlers, which you can use to set up any preconditions
that your example needs (for example, a user existing)
required by the getUser
request first.
Any preconditions needed to set up the examples are handled with state - for example: "when a user with ID 'foo' exists, a getUser request looks like $X, and a success response looks like $Y":
- Typescript
- Java
import {
willSendHttpRequest,
inState,
stateVariable,
stringPrefix,
anyString,
} from '@contract-case/contract-case-jest';
it('behaves as expected', async () => {
await contract.runExample({
states: [
inState('Server is up'),
inState('A user with id "foo" exists'),
],
definition: willSendHttpRequest({
request: {
method: 'GET',
path: '/users/foo',
},
response: {
status: 200,
body: {
userId: 'foo',
name: 'john smith',
},
},
}),
});
});
contract.runExample(
new ExampleDefinition<>(
List.of(
new InState("Server is up"),
new InState("A user with id \"foo\" exists")
),
new WillSendHttpRequest(HttpExample.builder()
.request(
new HttpRequest(HttpRequestExample.builder()
.method("GET")
.path("/users/foo")
.build())
)
.response(new HttpResponse(HttpResponseExample.builder()
.status(200)
.body(
Map.ofEntries(
Map.entry("status", "up"),
Map.entry("name", "john smith")
)
)
.build()))
.build())
),
);
See the state handlers section for more on state handlers.
Overriding configuration
You can override the configuration for individual tests by providing an
optional ContractCaseConfig
object as the final argument. See the
configuration options reference for full details.
For example, here's how you could set the log level to debug for an individual
test:
- Typescript
- Java
await contract.runExample(
{
states: [
/* as above */
],
definition:
/* as above */
},
{ logLevel: 'debug' },
);
contract.runExample(
new ExampleDefinition<>(
List.of(
/* as above */
),
new WillSendHttpRequest(
HttpExample.builder()
/* as above */
.build())
),
IndividualSuccessTestConfigBuilder.builder()
.withLogLevel(LogLevel.DEBUG)
.build()
);
Setting the log level to debug is a useful way to see what is happening during the test, which can help you understand why an example might be failing.
How do I tell ContractCase that a field is optional?
Instead of marking fields as optional, write two contract tests, one with the field and one without. To provide the best deployment confidence, a contract test needs specific examples, so there is no optional matcher.
This is because a test which allows optional fields could still pass even if the field was never present. You wouldn't be sure that your provider could ever generate that missing field.
Do I describe the whole API payload?
You only need to describe the parts of the API payload that your consumer relies on.
This is advantageous because it's not a breaking change to rename or remove a field/endpoint that no-one is using.
Next steps
Next, set up your state state definitions.