Hone logo
Hone
Problems

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:

  1. 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.
  2. 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.
  3. 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/pact npm 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.json file 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/pact and jest (along with its TypeScript types).
Loading editor...
typescript