Skip to main content

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).

Which side is the consumer?

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 a User object as JSON)

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:

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 */
},
);

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:

// With Typescript/Javascript, there is no need 
// to explictly write the contract as the
// `defineContract` Jest DSL handles this for you

Next steps

Next, we will discuss the ContractCase Example Lifecycle.