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.

Consumer driven contract testing has three main steps:

  1. Contract definition (this section), where you describe the expectations that the consumer has about the provider
  2. Contract verification, where you verify that the provider meets the expectations of its consumers
  3. Deploy checks, where at deploy time, you ensure that the thing you're about to deploy is compatible with the environment you're deploying in. That is:
    • Any expectations it has about providers have been verified by the version(s) of providers that are currently deployed`
    • Any expectations from any consumers that are already deployed have been verified

This section describes how to write contracts, and details the lifecycle of an interaction during contract definition.

If you already have a contract, and you want to know how to verify it, you can skip to contract verification.

Creating a contract

Every contract begins 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.runInteraction(
/* described later in this chapter */
);
});
});

describe('with no access token', () => {
it('throws an error', async () => {
await contract.runRejectingInteraction(
/* described later in this chapter */
);
});
});
});
/* arbitrary other contract examples */
},
);

In some languages, you may also need to end the contract once all of your tests have completed. This tells ContractCase to write the contract file to disk.

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

How to name your consumers and providers

The names of your consumer and provider should be the human readable name of your consumer / provider deployable - for example, "ExampleCo desktop app" or "User Profile Service".

The names should be unique within your deployment ecosystem - if you end up with two implementations of the same service that might deploy at different times, they should have different names. For example, if you have an existing deployable called "Foo CLI", and some of your engineers decide they should rewrite it in Rust, their consumer / provider name can't also be "Foo CLI" - ideally, it should be something that the people on your teams will understand - it could be something like "Foo CLI (Rust)"

Note

There's no reason you can't have a consumer be its own provider - this is common in the case of queues that a service both reads and writes to. At verification time, you would always verify the current version of the contract, and also any previous versions that might reasonably still have messages in the queue

Choosing which side is the consumer

In consumer-driven contract testing, we define the contract on the consumer side, and verify it at the provider side.

Unlike other contract testing systems, the client in ContractCase isn't always the consumer. However, although it's possible for the server to be the consumer, this is a rare use case. If it's your first time with contract testing, we strongly recommend you choose the client to be the consumer.

If you're unsure, each specific contract definition section starts with some guidance on the use cases that it's appropriate for.

Contracts don't have to be all one type of communication

Although each guide is written assuming that all interactions in your contract are of the same type, there's no reason you can't mix them up in the same contract. For example, a consumer might consume messages and http responses from the same provider. ContractCase supports this - just mix definitions within the same .

Circular contract dependencies

Sometimes, service A is a consumer of service B, but service B also consumes service A. For example, Service A might be a client of some http endpoints on service B; while Service B might be a client of different http endpoints on Service A.

Service A -> Service B
Service A <- Service B

This is completely supported by ContractCase. Just:

  • Define a contract at Service A for Service B
  • Set up contract verification on Service B
  • Define a contract at Service B for service A.
  • Set up contract verification on Service A

You may complete these steps in any practical order - a circular dependency introduces no additional challenges for contract testing.