Injectable Pact.js Consumer/Provider for NestJS
Pact-JS v10.x+ users
npm i -D nestjs-pact @pact-foundation/pactPact-JS v9.x users
npm i -D nestjs-pact@2.2.2 @pact-foundation/pact@9.18.1If you want to see a fully working end-to-end example with NestJS and Pact I recommend you to jump to the NestJS official examples at the PactJS Github repository
This package enables you to consume Pact.js in a way that can be used very easily in NestJS.
Like the nature of Pact, this package is for testing purposes only.
If you are not familiar with Pact, Pact is fast, easy and reliable testing framework for integrating web apps, APIs and microservices. Read more on Pact official website
There are two main modules suggested; one for the Provider role (Verifier), and one for the Consumer role (creating Pact files), each loaded separately.
Of course, you can also use both modules and play the role of Consumer and Provider at the same time.
The use of each of the modules suggested here, is made in the common and accepted form of NestJS modules.
The simplest way is to use the register method and pass the settings directly.
It is also enable to use the registerAsync method to pass the settings in the form of useFactory or useClass for example.
One more thing - the usage of the modules is for testing purposes only, which is not quite common in the use of NestJS modules, so there are some good examples down below. The obvious advantage of this package is that Pact can be used in combination with the techniques and benefits offered by NestJS.
In order to use the Consumer module, you need to follow a few simple steps, let's go over it!
First, create a file called pact.module.ts in your test folder (or wherever you put your tests), and simply
load the PactConsumerModule, PactV2ConsumerModule, or PactV3ConsumerModule like below:
test/pact/pact.module.ts
import { Module } from '@nestjs/common';
import { PactV2ConsumerModule } from 'nestjs-pact';
import { PactV2ConsumerConfigOptionsService } from './pact-consumer-config-options.service';
import { AppModule } from '../../src/app.module';
@Module({
imports: [
PactV2ConsumerModule.registerAsync({
imports: [AppModule],
useClass: PactV2ConsumerConfigOptionsService,
}),
],
})
export class PactModule {}import { DynamicModule, Module } from '@nestjs/common';
import { PactV3ConsumerModule } from 'nestjs-pact';
import { PactV3ConsumerConfigOptionsService } from './pact-consumer-config-options.service';
import { AppModule } from '../../src/app.module';
@Module({
imports: [
PactV3ConsumerModule.registerAsync({
imports: [AppModule],
useClass: PactV3ConsumerConfigOptionsService,
}) as DynamicModule,
],
})
export class PactModule {}import { DynamicModule, Module } from '@nestjs/common';
import { PactConsumerModule } from 'nestjs-pact';
import { PactConsumerConfigOptionsService } from './pact-consumer-config-options.service';
import { AppModule } from '../../src/app.module';
@Module({
imports: [
PactConsumerModule.registerAsync({
imports: [AppModule],
useClass: PactConsumerConfigOptionsService,
}) as DynamicModule,
],
})
export class PactModule {}Yay, now let's create the test file! let's call it my-test.spec.ts
test/pact/my-test.spec.ts
import { PactV2 } from '@pact-foundation/pact';
import { Test } from '@nestjs/testing';
import { PactV2Factory } from 'nestjs-pact';
import { PactModule } from '@test/pact/pact.module';
describe('Pact', () => {
let pactFactory: PactV2Factory;
let provider: PactV2;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [SomeOtherModule, AndAnotherModuleYouNeed, PactModule],
}).compile();
pactFactory = moduleRef.get(PactV2Factory);
provider = pactFactory.createContractBetween({
consumer: 'Consumer Service Name',
provider: 'Provider Service Name',
});
await provider.setup();
});
afterEach(() => provider.verify());
afterAll(() => provider.finalize());
describe('when something happens', () => {
describe('and another thing happens too', () => {
beforeAll(() => provider.addInteraction({ ... }));
it('should do something', () => {
return expect( ... );
});
});
});
});import { PactV3 } from '@pact-foundation/pact';
import { Test } from '@nestjs/testing';
import { PactV3Factory } from 'nestjs-pact';
import { PactModule } from '@test/pact/pact.module';
describe('Pact', () => {
let pactFactory: PactV3Factory;
let provider: PactV3;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [SomeOtherModule, AndAnotherModuleYouNeed, PactModule],
}).compile();
pactFactory = moduleRef.get(PactV3Factory);
provider = pactFactory.createContractBetween({
consumer: 'Consumer Service Name',
provider: 'Provider Service Name',
});
});
describe('when something happens', () => {
describe('and another thing happens too', () => {
beforeAll(() => provider.addInteraction({ ... }));
it('returns a 401 unauthorized', () => {
return provider.executeTest(async (mockServer) => {
// mockServer.url is the full url + port of the pact mock server
process.env.API_HOST = mockServer.url;
return expect( ... );
});
});
});
});
});import { Pact } from '@pact-foundation/pact';
import { Test } from '@nestjs/testing';
import { PactFactory } from 'nestjs-pact';
import { PactModule } from '@test/pact/pact.module';
describe('Pact', () => {
let pactFactory: PactFactory;
let provider: Pact;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [SomeOtherModule, AndAnotherModuleYouNeed, PactModule],
}).compile();
pactFactory = moduleRef.get(PactFactory);
provider = pactFactory.createContractBetween({
consumer: 'Consumer Service Name',
provider: 'Provider Service Name',
});
});
describe('when something happens', () => {
it('returns the animal', async () => {
await provider
.addInteraction()
.given('Has an animal with ID 1')
.uponReceiving('a request for an animal with ID 1')
.withRequest('GET', regex('/animals/[0-9]+','/animals/1'), (builder) => {
builder.headers({ Authorization: 'Bearer token' });
})
.willRespondWith(HttpStatus.OK, (builder) => {
builder.headers({ 'Content-Type': 'application/json; charset=utf-8' });
builder.jsonBody(animalBodyExpectation);
})
.executeTest(async (mockServer) => {
process.env.API_HOST = mockServer.url;
const suggestedMates = await animalsService.getAnimalById(11);
expect(suggestedMates).toHaveProperty('id', 1);
});
});
});
});Publishing pacts to a Pact Broker is done outside of this library using the @pact-foundation/pact-cli npm package, which wraps the Pact CLI binaries.
Install the CLI as a dev dependency:
npm i -D @pact-foundation/pact-cliThen publish your pacts from your CI pipeline:
npx pact-broker publish ./pacts \
--broker-base-url=https://your-broker.example.com \
--consumer-app-version=$(git rev-parse HEAD) \
--branch=$(git rev-parse --abbrev-ref HEAD)Or add it as a package.json script:
{
"scripts": {
"pact:publish": "pact-broker publish ./pacts --broker-base-url=$PACT_BROKER_URL --consumer-app-version=$GIT_COMMIT --branch=$GIT_BRANCH"
}
}Publishing is typically only done in CI — not locally. See the Pact CLI docs for all available options.
The usage in the Provider service is quite easy; In your /test folder (or wherever you put your tests)
create a simple test module with NestJS Test.createTestingModule method and import the PactProviderModule module
from nestjs-pact.
You can use register or registerAsync method, make sure you stick to PactProviderOptions interface options.
After creating the Nest application from the testing module, pass the app instance to the verify method,
it will generate a random (available) port, spin up the application and run the verifier against the application url.
You can read more about Pact Verification in the official Pact documentation
Here is a quick and simple example:
import { Test } from '@nestjs/testing';
import { INestApplication, Logger, LoggerService } from '@nestjs/common';
import { PactProviderModule, PactVerifierService } from 'nestjs-pact';
import { AppModule } from '@app/app.module';
describe('Pact Verification', () => {
let verifierService: PactVerifierService;
let logger: LoggerService;
let app: INestApplication;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule, PactProviderModule.register({ ... })],
}).compile();
verifierService = moduleRef.get(PactVerifierService);
logger = moduleRef.get(Logger);
app = moduleRef.createNestApplication();
await app.init();
});
it('validates the expectations of Matching Service', async () => {
const { output } = await verifierService.verify(app);
logger.log('Pact Verification Completed!');
logger.log(output);
});
afterAll(async () => {
await app.close();
});
});Distributed under the MIT License. See LICENSE for more information.
