pact-js-mock
v1.0.1
Published
A Library to build Pact interactions by leveraging existing mocks
Downloads
11
Readme
pact-js-mock
pact-js-mock is a Node.js library that allows you to build Pact contracts by leveraging your existings mocks. It could be used with your existing mocks defined with msw or Cypress. This library provides an easy way to generate contracts that can be used for testing and verifying API interactions between consumer and provider.
Install
yarn add -D pact-js-mockGetting started with MSW
pact-js-mock provides a simplified API for MSW that automatically records Pact interactions. Just use pactHttp and pactGraphql instead of MSW's http and graphql - they work exactly the same way!
1. Optional: Configure with pact.config.json
Create a pact.config.json file in your project root (optional):
{
"consumerName": "test-consumer",
"pactVersion": "2.0.0",
"outputDir": "./pacts"
}If you don't create a config file, sensible defaults are used. Provider names are automatically inferred from request URLs.
2. Create your consumer contract
Creating a contract with pact-js-mock is straightforward - you can leverage your existing MSW mocks! Simply replace your MSW imports with pact-js-mock's wrappers:
REST API - Before:
import { http, HttpResponse } from 'msw'REST API - After:
import { pactHttp as http, HttpResponse } from 'pact-js-mock/lib/msw'GraphQL - Before:
import { graphql, HttpResponse } from 'msw'GraphQL - After:
import { pactGraphql as graphql, HttpResponse } from 'pact-js-mock/lib/msw'That's it! Your existing MSW handlers will now automatically record Pact interactions. The API is identical to MSW, so no other changes are needed.
REST API handlers:
// handlers.ts
import { pactHttp as http, HttpResponse } from 'pact-js-mock/lib/msw'
// Simple handler - Pact recording happens automatically
export const getMovies = http.get('*/movies', () => {
return HttpResponse.json([
{ id: 1, name: 'Movie 1', year: 2008 },
{ id: 2, name: 'Movie 2', year: 2008 },
])
})
// With Pact options (optional third parameter)
export const getMovieById = http.get(
'*/movies/*',
({ params }) => {
return HttpResponse.json({
id: params.id,
name: 'Movie 1',
year: 2008,
})
},
{
providerState: 'a movie with the given id exists',
description: 'a request to get a movie by id',
},
)GraphQL handlers:
// handlers.ts
import { pactGraphql as graphql, HttpResponse } from 'pact-js-mock/lib/msw'
// Query handler
export const getTodos = graphql.query('todos', () =>
HttpResponse.json({
data: {
todos: [
{ id: '1', title: 'Buy groceries', completed: false },
{ id: '2', title: 'Do laundry', completed: true },
],
},
}),
)
// Mutation handler with Pact options
export const createTodo = graphql.mutation(
'createTodo',
() =>
HttpResponse.json({
data: {
createTodo: { id: '1', title: 'New todo', completed: false },
},
}),
{
description: 'a request to create a new todo',
},
)3. Use in your tests
⚠️ Important: The following lifecycle hooks are mandatory for generating pact contract files with MSW. They ensure pact files are properly managed throughout your test suite.
With Jest:
import { setupServer } from 'msw/node'
import { getMovies } from './handlers'
import { pactRegistry } from 'pact-js-mock/lib/msw'
import {
deletePacts,
reloadPacts,
writePacts,
setCurrentSourceForPacts,
} from 'pact-js-mock/lib/utils'
const server = setupServer()
before(() => {
// Delete all existing pact files before starting tests
deletePacts(pactRegistry)
server.listen()
})
beforeEach(function () {
// Set the current test name as the source for all pacts
setCurrentSourceForPacts(pactRegistry, this.currentTest?.title)
// Reload existing pact files (if any) to preserve previous interactions
reloadPacts(pactRegistry)
})
afterEach(() => {
// Write all pact files after each test to persist interactions
writePacts(pactRegistry)
server.resetHandlers()
})
after(() => {
server.close()
})
it('get all movies', async () => {
server.use(getMovies)
const movies = await fetchMovies()
expect(movies).toEqual([
{ id: 1, name: 'Movie 1', year: 2008 },
{ id: 2, name: 'Movie 2', year: 2008 },
])
})With Mocha:
import { setupServer } from 'msw/node'
import {
pactHttp as http,
HttpResponse,
pactRegistry,
} from 'pact-js-mock/lib/msw'
import {
deletePacts,
reloadPacts,
writePacts,
setCurrentSourceForPacts,
} from 'pact-js-mock/lib/utils'
const server = setupServer()
before(() => {
// Delete all existing pact files before starting tests
deletePacts(pactRegistry)
server.listen()
})
beforeEach(function () {
// Set the current test name as the source for all pacts
setCurrentSourceForPacts(pactRegistry, this.currentTest?.title)
// Reload existing pact files (if any) to preserve previous interactions
reloadPacts(pactRegistry)
})
afterEach(() => {
// Write all pact files after each test to persist interactions
writePacts(pactRegistry)
server.resetHandlers()
})
after(() => {
server.close()
})
it('get all movies', async () => {
const mockMovies = http.get('*/movies', () =>
HttpResponse.json([
{ id: 1, name: 'Movie 1', year: 2008 },
{ id: 2, name: 'Movie 2', year: 2008 },
]),
)
server.use(mockMovies)
const movies = await fetchMovies()
expect(movies).toEqual([
{ id: 1, name: 'Movie 1', year: 2008 },
{ id: 2, name: 'Movie 2', year: 2008 },
])
})Lifecycle hooks explained:
beforeAll/before:deletePacts(registry)- Cleans up existing pact files before running testsbeforeEach:setCurrentSourceForPacts(registry, testName)- Sets the current test name as the source for all pactsreloadPacts(registry)- Reloads existing pact files to preserve interactions from previous tests
afterEach:writePacts(registry)- Writes all pact files after each test to persist recorded interactions
Key Features
- ✅ Drop-in replacement - Use
pactHttpandpactGraphqlexactly like MSW'shttpandgraphql - ✅ Automatic Pact recording - No need to manually create Pact instances or wrap resolvers
- ✅ Zero boilerplate - Just write standard MSW handlers
- ✅ Optional configuration - Use
pact.config.jsonor rely on sensible defaults - ✅ GraphQL support - Full support for queries, mutations, and operations
You can find complete examples:
Note: If you're using the old API with
Pactinstances andtoResolver(), see the Migration Guide for instructions on migrating to the new simplified API.
Getting started with Cypress
pact-js-mock ships with an auto-setup path for Cypress so you can record pacts with almost no boilerplate.
1. Import the Cypress support shim
Add a single import in your Cypress support file (for example cypress/support/component.ts or cypress/support/e2e.ts):
import 'pact-js-mock/lib/cypress'This registers the cy.pactIntercept() command and wires the lifecycle hooks that read existing pacts before tests and write the updated files afterwards.
TypeScript Setup: The type declarations for cy.pactIntercept() are automatically included when you import the module. Most projects won't need any tsconfig changes - TypeScript automatically processes type declarations from node_modules when modules are imported.
However, you must ensure your Cypress support file is included in your TypeScript compilation. Here's a minimal tsconfig.json for Cypress:
{
"compilerOptions": {
"types": ["cypress", "node"],
"moduleResolution": "node"
},
"include": ["cypress/**/*.ts", "cypress/**/*.tsx"]
}Key points:
- ✅ The
includearray must contain your Cypress support files (where you importpact-js-mock/lib/cypress) - ✅
types: ["cypress"]ensures Cypress types are available (usually already configured) - ✅
moduleResolution: "node"is the default and allows TypeScript to resolve types fromnode_modules - ✅ No special path mappings or configuration needed - TypeScript automatically processes types from imported modules
The global type augmentation will be available once you import pact-js-mock/lib/cypress in a file that TypeScript processes.
2. Register the plugin
To persist pact files across runs, wire the Pact plugin in your Cypress configuration. The example below uses the classic cypress/plugins/index.js entry:
// cypress/plugins/index.js
const pactPlugin = require('pact-js-mock/lib/cypress/plugin').default
module.exports = (on, config) => {
return pactPlugin(on, config, {
consumerName: 'web-app',
pactVersion: '4.0.0',
})
}If you're using the newer setupNodeEvents API inside cypress.config.ts, import the same plugin and return the value from setupNodeEvents instead.
// cypress.config.ts
import { defineConfig } from 'cypress'
import pactPlugin from 'pact-js-mock/lib/cypress/plugin'
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
return pactPlugin(on, config, {
consumerName: 'web-app',
pactVersion: '4.0.0',
})
},
},
})3. Write tests with cy.pactIntercept()
Use cy.pactIntercept() instead of cy.intercept(). You can pass either a plain response body or a full Pact interaction. Provider names are inferred from the URL, and pact files are generated automatically:
describe('todos', () => {
it('lists all todos', () => {
cy.pactIntercept('GET', '/todo-service/todos', [
{ id: '1', title: 'Buy milk' },
{ id: '2', title: 'Pay bills' },
]).as('todos')
cy.visit('/todos')
cy.wait('@todos').its('response').its('statusCode').should('eq', 200)
})
})You can find complete examples under:
Troubleshooting TypeScript Issues
Quick Answer: Most projects don't need tsconfig changes. TypeScript automatically processes type declarations from node_modules. The only requirement is that your Cypress support file (where you import pact-js-mock/lib/cypress) must be included in your tsconfig.json include array.
If TypeScript doesn't recognize cy.pactIntercept() in your project, try the following solutions:
Solution 1: Ensure Your Support File is Included in tsconfig.json
The most common issue is that the Cypress support file (where you import pact-js-mock/lib/cypress) is not included in your TypeScript compilation. Make sure your tsconfig.json includes your Cypress files:
{
"compilerOptions": {
"types": ["cypress", "node"],
"moduleResolution": "node"
},
"include": ["cypress/**/*.ts", "cypress/**/*.tsx"]
}Important: If you have a separate cypress/tsconfig.json, ensure it extends your main config or includes the support files:
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": ["cypress", "node"]
},
"include": ["../cypress/**/*.ts", "../cypress/**/*.tsx"]
}Solution 2: Verify the Import is in Your Support File
Ensure you're importing pact-js-mock/lib/cypress in a file that's included in your TypeScript compilation. The import should be in your Cypress support file (e.g., cypress/support/component.ts or cypress/support/e2e.ts):
// cypress/support/component.ts or cypress/support/e2e.ts
import 'pact-js-mock/lib/cypress'Solution 3: Create a Type Declaration File (Advanced)
If the above solutions don't work, you can create a type declaration file that explicitly imports the types:
// cypress/support/index.d.ts or cypress/support/e2e.d.ts
import 'pact-js-mock/lib/cypress'This ensures TypeScript processes the type declarations. Make sure this file is included in your tsconfig.json:
{
"include": [
"cypress/**/*.ts",
"cypress/**/*.tsx",
"cypress/support/**/*.d.ts"
]
}Solution 4: Check Module Resolution and skipLibCheck
Module Resolution: If you're using a monorepo or have custom module resolution, ensure TypeScript can resolve the package. The default moduleResolution: "node" should work in most cases.
skipLibCheck: If your tsconfig.json has skipLibCheck: true (common for performance), this is fine - it won't prevent types from being available. However, if you're still having issues, you can try temporarily setting it to false to see if it helps with type resolution.
Monorepo path mappings: If you're in a monorepo and TypeScript can't resolve the package, you might need path mappings (though this is rarely necessary):
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"pact-js-mock/*": ["node_modules/pact-js-mock/lib/*"]
}
}
}The type declarations use global augmentation, so once the module is imported in a file that TypeScript processes, the cy.pactIntercept() command should be available throughout your Cypress tests.
Solution 5: Verify TypeScript Version
Ensure you're using a modern TypeScript version (4.1+). Older versions may have issues with global type augmentations from node_modules. You can check your version with:
npx tsc --versionIf you're using an older version, consider updating to TypeScript 4.5+ for better support of type declarations from node_modules.
Author
👤 Ludovic Dorival
- Github: @ludorival
Show your support
Give a ⭐️ if this project helped you!
📝 License
Copyright © 2021 Ludovic Dorival. This project is BSD--3--Clause licensed.
This README was generated with ❤️ by readme-md-generator
