Instruction to Deploy Contract Test between NextJS and NestJS with Pact

Introduction

  • Contract Testing is an integration testing method focused on verifying the interaction between a Consumer (the service user) and a Provider (the service provider). Instead of testing the entire system, Contract Testing ensures that both parties adhere to a shared covenant (contract). Key benefits include: early detection of non-compatible errors between Frontend and Backend, reduction of dependency on complex staging environments and enabling API changes to be made more confidently without breaking the partner's application.
  • Pact is currently the most popular Consumer-Driven Contract Testing tool. It allows the Consumer to define expectations regarding the response from the Provider and then packages these expectations into a JSON file (Pact file). Pact's advantages lie in supporting multiple languages, automatically generating documentation based on test cases and having robust integration capabilities into CI/CD pipelines to ensure the Provider always meets the Consumer's exact requirements before deployment.

Prerequisites

This article continues to build upon previous articles, so detailed source code will not be provided. Please refer back if you need the full code.


Detail

Please install the following package into both NextJS and NestJS projects:

yarn add -D @pact-foundation/pact


When applying Contract Testing, we have two approaches:

  • Consumer-Driven Contract Testing (CDCT): meaning the Frontend (API user) initiates requirements first.
    • Why does Pact prioritize Frontend definition first?
    • Pact's philosophy is: Backend should only provide what the Frontend truly needs.
    • If the Backend defines it, it often returns excess data (e.g., returning both password_hash, internal_id which the Frontend does not use).
    • When Frontend defines it first, Backend will know exactly which fields they need to avoid "accidentally" deleting those critical fields.
  • Provider-Driven / Bi-Directional Contract Testing: if the Backend team wants to proactively define the schema first, we still have a workaround.
    • The Backend can proactively define the API schema on Swagger and the Frontend team will use that data to write contract tests, thus returning the workflow back to the beginning like CDCT.


Summarized Workflow will be as follows:

  1. The FE team as the consumer defines the API including necessary fields to be used.
  2. Run the contract test to generate the JSON file.
  3. Share this JSON file with the BE team (can be via a sub repo to avoid manual sharing).
  4. The BE team as the provider when implementing a feature only needs to ensure it passes the contract test from this JSON file.
  5. Later, before deployment, simply run the contract test again to know if changes in the code base affect the APIs in use.

Note that when running contract test on the FE side, it will not involve calling the server directly (as the BE might not even have implemented that feature yet), only the contract test run from the BE side involves actual API calling.


NextJS Project

Create test/ChatBot/contract.test.ts as follows:

import { MatchersV3, PactV3 } from "@pact-foundation/pact";
import path from "path";

const provider = new PactV3({
consumer: "NextJS-Chat-Frontend",
provider: "NestJS-AI-Backend",
dir: path.resolve(process.cwd(), "pacts"),
});

describe("Contract Test with NestJS Backend", () => {
it("returns a list of conversations", async () => {
provider
.given("conversations exist")
.uponReceiving("a request for all conversations")
.withRequest({ method: "GET", path: "/conversations" })
.willRespondWith({
status: 200,
body: MatchersV3.eachLike({
id: MatchersV3.string("1"),
name: MatchersV3.string("John Doe"),
lastMsg: MatchersV3.string("Hello"),
}),
});

await provider.executeTest(async mockService => {
const url = `${mockService.url}/conversations`;
const response = await fetch(url);
const data = await response.json();
expect(data[0].name).toBe("John Doe");
});
});
});

The code above defines a Consumer Test on the NextJS side. It uses PactV3 to initialize a contract between "NextJS-Chat-Frontend" and "NestJS-AI-Backend". In it, the given method sets the Provider state, uponReceiving describes the request purpose and withRequest defines the endpoint to call. The willRespondWith part uses Matchers to regulate the returned data type (here a list of objects where id, name, lastMsg are strings). Finally, executeTest will run a simulated server to perform actual API calling and verify if the received data matches the commitment.


When the test runs successfully, it will automatically generate the pacts/NextJS-Chat-Frontend-NestJS-AI-Backend.json file with similar content:

{
"consumer": {
"name": "NextJS-Chat-Frontend"
},
"interactions": [
{
"description": "a request for all conversations",
"providerStates": [
{
"name": "conversations exist"
}
],
"request": {
"method": "GET",
"path": "/conversations"
},
"response": {
"body": [
{
"id": "1",
"lastMsg": "Hello",
"name": "John Doe"
}
],
"headers": {
"Content-Type": "application/json"
},
"matchingRules": {
"body": {
"$": {
"combine": "AND",
"matchers": [
{
"match": "type",
"min": 1
}
]
},
"$[*].id": {
"combine": "AND",
"matchers": [
{
"match": "type"
}
]
},
"$[*].lastMsg": {
"combine": "AND",
"matchers": [
{
"match": "type"
}
]
},
"$[*].name": {
"combine": "AND",
"matchers": [
{
"match": "type"
}
]
}
}
},
"status": 200
}
}
],
"metadata": {
"pact-js": {
"version": "16.3.1"
},
"pactRust": {
"ffi": "0.5.4",
"models": "1.3.11"
},
"pactSpecification": {
"version": "3.0.0"
}
},
"provider": {
"name": "NestJS-AI-Backend"
}
}

This is the Pact (contract) file automatically generated in JSON format. It contains all the detailed information about the interactions defined in the previous step, including information about the Consumer, Provider, required request (request) and expected response (response). Specifically, the matchingRules part records data type checking rules (match by type) instead of exact values, making verification on the Provider side more flexible while ensuring the required data structure.


NestJS Project

Create test/pact-provider.spec.ts

import { INestApplication } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import { Verifier } from "@pact-foundation/pact";
import path from "path";
import { AppModule } from "src/app.module";

describe("Pact Verification", () => {
let app: INestApplication;

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleFixture.createNestApplication();
await app.init();
await app.listen(3001);
});

afterAll(async () => {
await app.close();
});

it("should validate the expectations of NextJS-Chat-Frontend", async () => {
const verifier = new Verifier({
provider: "NestJS-AI-Backend",
providerBaseUrl: "http://localhost:3001",
pactUrls: [path.resolve(process.cwd(), "pacts/nextjs-chat-frontend-nestjs-ai-backend.json")],
stateHandlers: {
"conversations exist": async () => {
return Promise.resolve();
},
},
});

await verifier.verifyProvider();
});
});

This code performs the verification task (Verification) on the Provider (NestJS) side. First, it initializes the actual NestJS application and runs on port 3001. Then, the Verifier object is used to read the Pact file created from the Frontend. It will automatically send the simulated requests within the contract file to the running NestJS server and compare the returned results with what the Consumer expects. The stateHandlers part is usually used to initialize initial data (such as inserting data into the database) to prepare for testing; here, when nothing is defined, it only checks if the actual API request and response match the content from the pact file.


Test result when running:

$ yarn test
yarn run v1.22.22
$ jest
PASS src/app.controller.spec.ts
PASS src/test/test.controller.spec.ts
PASS src/test/pact-provider.spec.ts

Test Suites: 3 passed, 3 total
Tests: 11 passed, 11 total
Snapshots: 0 total
Time: 1.333 s, estimated 2 s
Ran all test suites.

Verifying a pact between NextJS-Chat-Frontend and NestJS-AI-Backend
a request for all conversations (3ms loading, 258ms verification)
Given conversations exist
returns a response which
has status code 200 (OK)
includes headers
"Content-Type" with value "application/json" (OK)
has a matching body (OK)
Done in 1.94s.


You can modify the content of this controller/conversations.controller.ts file, for example, I delete the id field as follows:

import { Controller, Get } from "@nestjs/common";

@Controller("conversations")
export class ConversationsController {
@Get()
getConversations() {
return [
{
name: "NestJS System",
lastMsg: "Socket.io is ready",
time: "10:00",
online: true,
},
{
name: "React Squad",
lastMsg: "New components added",
time: "Yesterday",
online: false,
},
];
}
}


Result when testing again:

$ yarn test
yarn run v1.22.22
$ jest
PASS src/app.controller.spec.ts
PASS src/test/test.controller.spec.ts
FAIL src/test/pact-provider.spec.ts

Pact Verification should validate the expectations of NextJS-Chat-Frontend

Test Suites: 1 failed, 2 passed, 3 total
Tests: 1 failed, 10 passed, 11 total
Snapshots: 0 total
Time: 1.482 s, estimated 2 s
Ran all test suites.

Verifying a pact between NextJS-Chat-Frontend and NestJS-AI-Backend
a request for all conversations (2ms loading, 259ms verification)
Given conversations exist
returns a response which
has status code 200 (OK)
includes headers
"Content-Type" with value "application/json" (OK)
has a matching body (FAILED)

Failures:
1) Verifying a pact between NextJS-Chat-Frontend and NestJS-AI-Backend Given conversations exist - a request for all conversations
1.1) has a matching body
$[1] -> Actual map is missing the following keys: id
$[0] -> Actual map is missing the following keys: id
There were 1 pact failures
error Command failed with exit code 1.

You can see that it has reported a missing key id error accordingly. This way, before deploying in the future, you only need to run the test again to know if your changes affect the frontend team's integration.


Happy coding!

See more articles here.

Comments

Popular posts from this blog

All Practice Series

Kubernetes Deployment for Zero Downtime

Deploying a NodeJS Server on Google Kubernetes Engine

Setting up Kubernetes Dashboard with Kind

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Using Kafka with Docker and NodeJS

Practicing with Google Cloud Platform - Google Kubernetes Engine to deploy nginx

Kubernetes Practice Series

Sitemap

DevOps Practice Series