Defining contracts
ContractCase is consumer driven, meaning that we define the contract on the side of the communication boundary that consumes information. Later, that contract is verified on the side of the communication boundary where information is provided.
This section describes how to write contracts, and details the lifecycle of an example during contract definition. If you already have a contract, and you want to know how to verify it, you can skip to contract verification.
Which side is the consumer?
With message-based communication, it's clear which side should be the consumer - as the message sender provides messages, and the message receiver consumes them.
However, with request/response pairs like HTTP and RPC calls, it's less clear. Although some contract testing frameworks consider the client to always be the consumer, technically in a request / response pair like HTTP and RPC calls, either side could be considered the consumer:
- The client provides requests and consumes responses
- The server consumes requests and provides responses
Usually, it's best practice to consider the client as the consumer - as the server exists in "service" of the client, so the communication is for the client. However, in some cases it might make sense to consider the server as the consumer (for example, in remote logging frameworks, where the client side might not even read the response).
The consumer is the side of the communication boundary that the communication is for. Usually, this is the client.
ContractCase supports considering either the client or the server as the consumer; if you are unsure, we recommend starting with your client as the consumer.
What is in a contract?
In ContractCase, a contract is a series of examples, not a specification. Each example is independent. To achieve this independence, any preconditions are handled by state setup functions. This means each example contains:
- Each example contains:
- Example Request (eg, an HTTP
GET
for/users/12
) - Provider state (eg
"User 12 exists"
) - Example Response (eg, a
200 OK
response containing aUser
object as JSON)
- Example Request (eg, an HTTP
A contract contains one or more of these examples.
Creating a contract
You can begin contract definition with a defineContract
call, which names the
consumer and provider pairs that this contract is for. Additional configuration
can be provided at this point, see the configuration options reference for details.
For example, defining a contract might look like:
- Typescript
- Java
import {
ContractCaseDefiner,
defineContract,
} from '@contract-case/contract-case-jest';
defineContract(
{
/* The name of the service writing the contract */
consumerName: 'Example-Client',
/* The name of the service that will verify the contract */
providerName: 'Example-Server',
/* Any additional ContractCaseConfig goes here */
},
(contract: ContractCaseDefiner) => {
describe('some API method', () => {
describe('with a valid access token', () => {
it('behaves as expected', async () => {
await contract.runExample(
/* described later in this chapter */
);
});
});
describe('with no access token', () => {
it('throws an error', async () => {
await contract.runRejectingExample(
/* described later in this chapter */
);
});
});
});
/* arbitrary other contract examples */
},
);
private static final ContractDefiner contract = new ContractDefiner(
ContractCaseConfig.ContractCaseConfigBuilder.aContractCaseConfig()
.consumerName("Example-Client")
.providerName("Example-Server")
.build());
public void testSomeApiMethod() {
contract.runExample(
/* described later in this chapter */
);
}
public void testSomeFailingMethod() {
contract.runThrowingExample(
/* described later in this chapter */
);
}
In a contract, you can have as many runExample
and runRejectingExample
calls as you like. When all the tests are over, you may need to tell ContractCase to write the contract to disk:
- Typescript
- Java
// With Typescript/Javascript, there is no need
// to explictly write the contract as the
// `defineContract` Jest DSL handles this for you
// annotate with @AfterAll if using JUnit
static void after() {
contract.endRecord();
}
Next steps
Next, we will discuss the ContractCase Example Lifecycle.