Defining a server-driven interaction
Server-driven interactions are defined with willReceiveHttpRequest. During
definition, ContractCase acts as the mock client: it sends the request from
the interaction definition to your real running server, and checks that the
response matches.
Because the real server runs during definition, there are two differences from defining an HTTP client contract:
- You don't write triggers or
testResponsefunctions - ContractCase generates the client side itself. - You implement state handlers at definition time, since the server needs to be put into the right state before each interaction runs.
Telling ContractCase where your server is
Start your server before the tests run, and tell ContractCase its base URL
with the mockConfig configuration option:
- Typescript
- Java
defineContract(
{
consumerName: 'http request consumer',
providerName: 'http request provider',
stateHandlers,
mockConfig: {
http: {
// Replace this with your own server URL
baseUrlUnderTest: `http://localhost:${port}`,
},
},
},
(contract) => {
// ... interaction definitions go here
},
);
private static final ContractDefiner contract = new ContractDefiner(
ContractCaseConfigBuilder.aContractCaseConfig()
.consumerName("http request consumer")
.providerName("http request provider")
.mockConfig("http", Map.of(
// Replace this with your own server URL
"baseUrlUnderTest", "http://localhost:" + port))
// State handlers are described below
.build());
Defining an interaction
The request and response are described the same way as for HTTP client
contracts - only the direction is reversed. runInteraction is used for
interactions where the client code would succeed, and
runRejectingInteraction (in Java, runThrowingInteraction) for interactions
where a client would treat the response as an error:
- Typescript
- Java
describe('When the server is up', () => {
const state = inState('Server is up');
it('returns a healthy status', () =>
contract.runInteraction({
states: [state],
definition: willReceiveHttpRequest({
request: {
method: 'GET',
path: '/health',
headers: { accept: 'application/json' },
},
response: { status: 200, body: { status: 'up' } },
}),
}));
});
describe('When the server is down', () => {
it('returns an error status', () =>
contract.runRejectingInteraction({
states: [inState('Server is down')],
definition: willReceiveHttpRequest({
request: {
method: 'GET',
path: '/health',
},
response: { status: httpStatus(['4XX', '5XX']) },
}),
}));
});
contract.runInteraction(
new InteractionDefinition<>(
List.of(new InState("Server is up")),
WillReceiveHttpRequest.builder()
.request(HttpRequest.builder()
.method("GET")
.path("/health")
.headers(Map.of("accept", "application/json"))
.build())
.response(HttpResponse.builder()
.status(200)
.body(Map.of("status", "up"))
.build())
.build()));
contract.runThrowingInteraction(
new InteractionDefinition<>(
List.of(new InState("Server is down")),
WillReceiveHttpRequest.builder()
.request(HttpRequest.builder()
.method("GET")
.path("/health")
.build())
.response(HttpResponse.builder()
.status(new HttpStatusCodes(List.of("4XX", "5XX")))
.build())
.build()));
All the usual Test Equivalence Matchers are available in both the request and the response.
State handlers during definition
Any states referenced by your interactions need state handlers - functions that put your running server into the named state. These work exactly the same way as state handlers during verification, and the same advice applies: mock the repository layer of your service if you can.
- Typescript
- Java
const stateHandlers: StateHandlers = {
'Server is up': () => {
mockHealthStatus = true;
},
'Server is down': () => {
mockHealthStatus = false;
},
'A user exists': {
setup: () => {
const userId = '42';
mockGetUser = (id) =>
id === userId ? { userId, name: 'John' } : undefined;
// Return the userId as a state variable
return { userId };
},
teardown: () => {
mockGetUser = () => undefined;
},
},
};
ContractCaseConfigBuilder.aContractCaseConfig()
.stateHandler(
"Server is up",
StateHandler.setupFunction(() -> {
// Put your server into the 'Server is up' state here,
// for example by mocking the repository layer
}))
.stateHandler(
"Server is down",
StateHandler.setupFunction(() -> {
// Put your server into the 'Server is down' state here
}))
.stateHandler(
"A user exists",
StateHandler.setupAndTeardown(
() -> {
// Set up the user in your server's repository layer,
// then return the userId as a state variable
return Map.of("userId", "42");
},
() -> {
// Remove the mock, so that the server state
// is the same as it was before the test
}))
Next steps
Once your contract is defined, upload it to a broker so that your clients can verify it.