Consumer-Driven Contract Testing with Pact and Jest
This challenge focuses on implementing consumer-driven contract testing using Pact in a Jest TypeScript environment. Contract testing is crucial for ensuring that services that depend on each other can communicate reliably, even when developed independently. By writing a consumer test, you'll define expectations for a provider's API, which can then be verified against the actual provider.
Problem Description
You are tasked with building a TypeScript application that consumes an external API. To ensure your application's integration with this API remains stable, you need to implement consumer-driven contract testing using Pact.
Specifically, you will:
- Define a Pact contract for a consumer of a hypothetical "User Service". This contract will specify the expected behavior of the User Service's API from the perspective of your consumer.
- Write Jest tests that drive the creation of this Pact contract. These tests will simulate the consumer's requests to the User Service and assert the expected responses.
- Generate the Pact JSON file. This file will represent the agreed-upon contract between your consumer and the User Service.
The goal is to demonstrate your understanding of how to set up and execute Pact tests within a TypeScript Jest project, ensuring that your consumer's expectations are clearly documented and verifiable.
Examples
Example 1: Fetching a specific user
Imagine your consumer needs to fetch details of a user with ID 123.
Consumer Test Setup (Conceptual):
// consumer.test.ts (simplified for illustration)
import { Pact } from '@pact-foundation/pact';
const provider = new Pact({
consumer: 'MyConsumerApp',
provider: 'UserService',
port: 8080, // Mock server port
});
describe('User Service API', () => {
it('should return user details for a given ID', async () => {
const userId = '123';
const expectedUser = {
id: userId,
name: 'Jane Doe',
email: 'jane.doe@example.com',
};
await provider.given('a user with ID 123 exists').uponReceiving('a request for user 123').withRequest({
method: 'GET',
path: `/users/${userId}`,
}).willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: expectedUser,
});
// In a real test, you would now call your actual service client
// that makes the GET /users/123 request and assert its response.
// For this challenge, we are primarily focused on defining the contract.
// Let's assume a successful mock interaction.
expect(true).toBe(true); // Placeholder for actual service client assertion
});
});
Generated Pact JSON (Conceptual):
{
"consumer": {
"name": "MyConsumerApp"
},
"provider": {
"name": "UserService"
},
"interactions": [
{
"description": "a request for user 123",
"providerState": "a user with ID 123 exists",
"request": {
"method": "GET",
"path": "/users/123"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"id": "123",
"name": "Jane Doe",
"email": "jane.doe@example.com"
}
}
}
],
// ... other metadata
}
Explanation:
The consumer test defines an expectation: when it makes a GET request to /users/123, it expects the provider (UserService) to respond with a 200 OK status and a JSON body containing the user's details. This interaction is recorded by Pact and will be used to generate the contract file.
Example 2: Handling a non-existent user
Consider the scenario where the consumer requests a user that does not exist.
Consumer Test Setup (Conceptual):
// consumer.test.ts (continued)
import { Pact } from '@pact-foundation/pact';
// ... Pact setup as above ...
describe('User Service API', () => {
// ... previous test ...
it('should return a 404 for a non-existent user', async () => {
const userId = '999';
await provider.given('no user with ID 999 exists').uponReceiving('a request for non-existent user 999').withRequest({
method: 'GET',
path: `/users/${userId}`,
}).willRespondWith({
status: 404,
body: null, // Or a specific error body if defined
});
// Placeholder for actual service client assertion
expect(true).toBe(true);
});
});
Generated Pact JSON (Conceptual):
// ... (previous interactions) ...
{
"description": "a request for non-existent user 999",
"providerState": "no user with ID 999 exists",
"request": {
"method": "GET",
"path": "/users/999"
},
"response": {
"status": 404
// body might be omitted or be null depending on provider implementation
}
}
// ... other metadata
Explanation:
This test case covers the scenario where the requested user ID does not exist. The consumer expects a 404 Not Found status code. Pact will record this expectation, ensuring the provider adheres to this error handling.
Constraints
- TypeScript Project: The solution must be implemented within a standard TypeScript project using Jest as the test runner.
- Pact Dependency: You must use the
@pact-foundation/pactnpm package. - Consumer Logic: You will not be required to implement the actual consumer service client that makes HTTP requests. The focus is on setting up the Pact consumer tests and defining the contract. A placeholder assertion in the test will suffice.
- Provider Verification: You are not required to implement or run the Pact provider verification step in this challenge. The goal is solely to generate the consumer-side contract.
- Single Pact Instance: For simplicity, you can manage a single Pact instance for all tests within your test file.
- Minimal API Surface: Focus on testing at least two distinct API interactions for the User Service (e.g., successful retrieval and error handling).
Notes
- Setup: You'll need to initialize Pact in your test file and configure it with consumer and provider names.
provider.given(...): Use this to define the state of the provider before the interaction.uponReceiving(...): Describe the specific interaction from the consumer's perspective.withRequest(...): Define the outgoing HTTP request from the consumer.willRespondWith(...): Define the expected HTTP response from the provider.- Pact File Generation: The Pact library will automatically generate a
pact.jsonfile in a configured directory (often./pacts) once the tests are run. - Mock Server: Pact runs a mock HTTP server locally to simulate the provider during consumer tests. You don't need to start a separate server.
- Dependencies: Ensure you have Node.js and npm/yarn installed. You'll need to install
@pact-foundation/pactandjest(along with its TypeScript types).