-
Notifications
You must be signed in to change notification settings - Fork 54
Mock Server
Mock Server allows you to mock any server or service via HTTP or HTTPS, such as a REST endpoint. Simply it is a simulator for HTTP-based APIs.
At one end pactum is a REST API testing tool and on the other, it can act as a standalone mock server. It packs with a powerful request & response matching & you can spin it up in a matter of seconds.
Mock server comes in handy while using this library for component & contract testing.
The below code will run the pactum mock server on port 3000
// server.js
const mock = require('pactum').mock;
mock.start(3000);
# Running Mock Server
node server.js
# Returns OK
curl http://localhost:3000/api/pactum/health
Use start
to start the mock server on default port 9393 or send a port number as a parameter. It returns a promise. Waiting for the server to start will be useful while running the mock server along with your unit or component tests.
// starts mock server on port 9393
await mock.start();
// starts mock server on port 3000
await mock.start(3000);
Use stop
to stop the mock server. It returns a promise. Waiting for the server to stop will be useful while running the mock server along with your unit or component tests.
await mock.stop();
By now, you know how to start & stop a mock server. To add behavior, we need to add interactions to it. An interaction contains request & response details. When a real request is sent to mock server, it will try to match the received request with interactions request. If a match is found it will return the specified response or 404 will be returned.
We have two kinds of interactions. They are
- Mock Interactions
- Pact Interactions
The simplest way to add behavior to the mock server is through mock interactions. Use addMockInteraction
to add a mock interaction to the server. It has withRequest
& willRespondWith
objects to handle a request. When the mock server receives a request, it will match the request with interactions withRequest
properties. If the request matches, the mock server will respond with details in willRespondWith
object.
Matching:
- Performs exact match on received HTTP Method & Path.
- Performs loose match on received query params, headers & JSON body.
addMockInteraction
returns a string containing the id of the interaction. (This id will be useful when you want to add assertions based on the call count or to check whether the interaction is exercised or not.)
const mock = require('pactum').mock;
mock.addMockInteraction({
withRequest: {
method: 'GET',
path: '/api/users/1'
},
willRespondWith: {
status: 200,
body: {
id: 1,
name: 'Jon'
}
}
});
mock.start(3000);
- Invoking GET
/api/users/1
will return a JSON with 200 status code. - Invoking GET
/api/users
will return a status code 404. - Invoking GET
/api/users/2
will return a status code 404. - Invoking POST
/api/users
will return a status code 404.
Note on how the mock server performs an exact match on HTTP method & path. If a match is not found it will return a status code 404.
To further distinguish the responses, let's add two mock interactions with query params
mock.addMockInteraction({
withRequest: {
method: 'GET',
path: '/api/users',
query: {
id: 1
}
},
willRespondWith: {
status: 200,
body: {
id: 1,
name: 'Jon'
}
}
});
mock.addMockInteraction({
withRequest: {
method: 'GET',
path: '/api/users',
query: {
id: 2
}
},
willRespondWith: {
status: 200,
body: {
id: 2,
name: 'Snow'
}
}
});
- Invoking GET
/api/users?id=1
will return project-1 details. - Invoking GET
/api/users?id=1&name=snow
will still return project-1 details. - Invoking GET
/api/users?id=1&foo=boo
will still return project-1 details. - Invoking GET
/api/users?id=2
will return project-2 details. - Invoking GET
/api/users
will return a status code 404. - Invoking GET
/api/users?id=3
will return a status code 404. - Invoking GET
/api/users?name=Jon
will return a status code 404.
When query params are mentioned in the interaction, the received request is matched when it has the specified query params. Same applies for headers & body.
Let's look at an example
mock.addMockInteraction({
withRequest: {
method: 'POST',
path: '/api/users',
body: {
id: 3,
married: false,
country: 'True North'
}
},
willRespondWith: {
status: 200
}
})
Posting the below JSON to /api/users
will return a 200 response.
{
"id": 3,
"name": "Jon",
"married": false,
"house": "Stark",
"country": "True North"
}
Posting the below JSON to /api/users
will return a 404 response. The id & country fields won't match with the above interaction.
{
"id": 4,
"name": "Jon",
"married": false,
"house": "Stark"
}
Property | Description |
---|---|
id | id of the interaction |
provider | name of the provider |
withRequest | request details |
withRequest.method | HTTP method |
withRequest.path | api path |
withRequest.headers | request headers |
withRequest.query | query parameters |
withRequest.body | request body |
withRequest.graphQL | graphQL details |
withRequest.graphQL.query | graphQL query |
withRequest.graphQL.variables | graphQL variables |
willRespondWith | response details |
willRespondWith.status | response status code |
willRespondWith.headers | response headers |
willRespondWith.body | response body |
willRespondWith.fixedDelay | delays the response by ms |
willRespondWith.randomDelay | random delay details |
willRespondWith.randomDelay.min | delay the response by min ms |
willRespondWith.randomDelay.max | delay the response by max ms |
willRespondWith.onCall | response on consecutive calls |
willRespondWith(req, res) | response with custom function |
onCall
defines the behavior of the interaction on the nth call. Useful for testing sequential interactions.
mock.addMockInteraction({
withRequest: {
method: 'GET',
path: '/api/health'
},
willRespondWith: {
onCall: {
0: {
status: 500
},
1: {
status: 200,
body: 'OK'
}
}
}
})
- Invoking GET
/api/health
will return a status code 500. - Invoking GET
/api/health
again will return a status code 200. - Invoking GET
/api/health
again will return a status code 404.
Note how the behavior of interaction falls back to the default behavior (404) on the third call.
We can also define the default behavior by providing normal response details along with onCall
details.
willRespondWith : {
status: 200,
onCall: {
0: {
status: 500
}
}
}
fixedDelay
& randomDelay
adds delay to the response. The following interaction will send a response after 1000 ms.
mock.addMockInteraction({
withRequest: {
method: 'POST',
path: '/api/users',
body: {
id: 3
}
},
willRespondWith: {
status: 200,
fixedDelay: 1000
}
})
The final way of adding behavior to the mock server is through pact interactions. It is similar to a mock interaction & supports most of the options from it.
Major differences between mock & pact interactions are
- Pact Interactions generate a contract file which is later used for Contract Testing
- Performs a strong match on the received request's query params & JSON body.
- Performs a loose match on the received request's headers.
Use addPactInteraction
to add a pact interaction to the server. It returns a string containing the id of the interaction. (This id will be useful when you want to add assertions based on the call count or to check whether the interaction is exercised or not.)
mock.addPactInteraction({
provider: 'project-service',
state: 'there is a project with id 1',
uponReceiving: 'a request for project 1',
withRequest: {
method: 'GET',
path: '/api/projects',
query: {
id: 1
}
},
willRespondWith: {
status: 200,
headers: {
'content-type': 'application/json'
},
body: {
id: 1,
name: 'fake'
}
}
});
Property | Description |
---|---|
id | id of the interaction |
provider | name of the provider |
state | state of the provider |
uponReceiving | description of the request |
withRequest | request details |
withRequest.method | HTTP method |
withRequest.path | api path |
withRequest.headers | request headers |
withRequest.query | query parameters |
withRequest.body | request body |
withRequest.graphQL | graphQL details |
withRequest.graphQL.query | graphQL query |
withRequest.graphQL.variables | graphQL variables |
willRespondWith | response details |
willRespondWith.status | response status code |
willRespondWith.headers | response headers |
willRespondWith.body | response body |
Interaction handlers help us to reuse same kind of interactions with modifications. Use handler.addMockInteractionHandler
or handler.addPactInteractionHandler
functions to temporarily store interactions & later use the handler name to add them to the mock server.
It accepts two arguments
- handler name - a string to refer the interaction later
- callback function - it should return a mock interaction or reference to other interaction
const mock = pactum.mock;
const handler = pactum.handler;
handler.addMockInteractionHandler('get product', (ctx) => {
return {
withRequest: {
method: 'GET',
path: '/api/inventory',
query: {
product: ctx.data.product
}
},
willRespondWith: {
status: 200,
body: {
"InStock": ctx.data.inStock
}
}
}
});
mock.addMockInteraction('get product', { product: 'iPhone', inStock: true });
mock.addMockInteraction('get product', { product: 'iPhone', inStock: false });
mock.start(3000);
Interaction handlers can refer other interaction handlers to make them more reusable. Instead of returning interaction, return an object with name
& optional data
property.
const mock = pactum.mock;
const handler = pactum.handler;
handler.addMockInteractionHandler('get product', (ctx) => {
return {
withRequest: {
method: 'GET',
path: '/api/inventory',
query: {
product: ctx.data.product
}
},
willRespondWith: {
status: 200,
body: {
"InStock": ctx.data.inStock
}
}
}
});
handler.addMockInteractionHandler('get product in stock', () => {
return { name: 'get product', data: { product: 'iPhone', inStock: true } };
});
handler.addMockInteractionHandler('get product out of stock', () => {
return { name: 'get product', data: { product: 'iPhone', inStock: false } };
});
mock.addMockInteraction('get product in stock');
mock.addMockInteraction('get product out of stock');
mock.start(3000);
Data Management can also be applied to the mock server to re-use mock data across interactions. Learn more about data management with pactum at Data Management
const pactum = require('pactum');
const stash = pactum.stash;
const mock = pactum.mock;
stash.addDataTemplate({
'User:New': {
"FirstName": "Jon",
"LastName": "Snow",
"Age": 26,
"Gender": "male",
"House": "Castle Black"
}
});
mock.addMockInteraction({
withRequest: {
method: 'GET',
path: '/api/users'
},
willRespondWith: {
status: 200,
headers: {
'content-type': 'application/json'
},
body: [
{
'@DATA:TEMPLATE@': 'User:New'
},
{
'@DATA:TEMPLATE@': 'User:New',
'OVERRIDES': {
'House': 'WinterFell'
}
}
]
}
});
mock.start(3000);
Allows matching of request/response with a set of matchers. See Matching for more usage details.
Matchers can be applied to
- Path (supports only regex)
- Query Params
- Headers
- Body (JSON)
Often, you will not care what the exact value is at a particular path is, you just care that a value is present and that it is of the expected type.
-
like
orsomethingLike
eachLike
Type matching for primitive data types - string/number/boolean
const { like } = pactum.matchers;
const interaction = {
withRequest: {
method: 'GET',
path: '/api/orders',
query: {
// matches if it has id & of type string
id: like('abc')
}
},
willRespondWith: {
status: 200,
body: {
// Note: Response Matching is used in Contract Testing
// matches if it has quantity & active properties with number & bool types
quantity: like(2),
active: like(true)
}
}
}
Type matching for objects in pactum deviates from in pact.io when matching nested objects.
const { like } = pactum.matchers;
const interaction = {
withRequest: {
method: 'POST',
path: '/api/orders',
// matches if the body is JSON
// & it has quantity & active properties
// & with number & boolean types respectively
body: like({
quantity: 2,
active: true
})
},
willRespondWith: {
status: 200
}
}
// matching doesn't expand to nested objects
const { like } = pactum.matchers;
const interaction = {
withRequest: {
method: 'GET',
path: '/api/orders'
},
willRespondWith: {
status: 200,
// Note: Response Matching is used in Contract Testing
// matches if body is JSON object
// & it has quantity, active & item properties
// & with number, bool & object types respectively
// & item has name & brand properties with 'car' & 'v40' values respectively
body: like({
quantity: 2,
active: true,
item: {
name: 'car',
brand: 'v40'
}
})
}
}
// to match nested objects with type, we need apply 'like()' explicitly to nested objects
const { like } = pactum.matchers;
const interaction = {
withRequest: {
method: 'POST',
path: '/api/orders',
// matches if body is JSON object
// & it has quantity, active, item & delivery properties
// & with number, bool & object types respectively
// & item has name & brand properties with string types
// & delivery has pin property with type number
// & delivery has state property with 'AP' as value
body: like({
quantity: 2,
active: true,
item: like({
name: 'car',
brand: 'v40'
}),
delivery: {
pin: like(500081),
state: 'AP'
}
})
},
willRespondWith: {
status: 200
}
}
eachLike is similar to like but applies to arrays.
const { eachLike } = pactum.matchers;
const interaction = {
withRequest: {
method: 'GET',
path: '/api/orders'
},
willRespondWith: {
status: 200,
// Note: Response Matching is used in Contract Testing
// matches if body is an array
// & each item in the array is an object
// & each object should have quantity, active, product properties
// & quantity, active, product should be of types number, boolean & object
// & product has a name property with string type
// & product has a colors property with array of strings
body: eachLike({
quantity: 2,
active: true,
product: like({
name: 'car',
colors: eachLike('blue')
}
})
}
}
Sometimes you will have keys in a request or response with values that are hard to know beforehand - timestamps and generated IDs are two examples.
What you need is a way to say "I expect something matching this regular expression, but I don't care what the actual value is".
regex
has two flavors. One accepts a string & the other accepts an object as param. For mock interactions use the regex method which accepts string & for pact interactions use the regex method which accepts object.
const { regex } = pactum.matchers;
const interaction = {
withRequest: {
method: 'POST',
path: regex('/api/projects/\\d+'),
body: {
name: 'Jon'
birthDate: regex({
generate: '03/05/2020',
matcher: /\d{2}\/\d{2}\/\d{4}/
})
}
},
willRespondWith: {
status: 200
}
}
Pactum allows us to add or remove interactions dynamically through the REST API. Once the server is started, interact with the following API to control the mock server.
const mock = require('pactum').mock;
mock.start(3000);
Returns a single or all mock interactions.
# Fetches a mock interaction with id "m1uh9"
curl --location --request GET 'http://localhost:9393/api/pactum/mockInteractions?id=m1uh9'
# Fetches all mock interactions
curl --location --request GET 'http://localhost:9393/api/pactum/mockInteractions'
Response - returns all the mock interactions
[
{
"id": "<id>",
"exercised": false,
"callCount": 0,
"withRequest": {
"method": "GET",
"path": "/api/projects/2",
"query": {
"name": "fake"
}
},
"willRespondWith": {
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"id": 1,
"name": "fake"
}
}
}
]
Adds multiple mock interactions to the server.
curl --location --request POST 'http://localhost:9393/api/pactum/mockInteractions' \
--header 'Content-Type: application/json' \
--data-raw '[{
"withRequest": {
"method": "GET",
"path": "/api/projects/2",
"query": {
"name": "fake"
}
},
"willRespondWith": {
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"id": 1,
"name": "fake"
}
}
}]'
Response - returns the mock interaction id.
[ "x121w" ]
Removes a mock interaction or all mock interactions from the mock server.
# Removes a single mock interaction with id m1uh9
curl --location --request DELETE 'http://localhost:9393/api/pactum/mockInteractions?id=m1uh9'
# Removes all mock interactions
curl --location --request DELETE 'http://localhost:9393/api/pactum/mockInteractions'
Returns a single or all pact interactions.
# Fetches a pact interaction with id "m1uh9"
curl --location --request GET 'http://localhost:9393/api/pactum/pactInteractions?id=m1uh9'
# Fetches all pact interactions
curl --location --request GET 'http://localhost:9393/api/pactum/pactInteractions'
Response - returns all the pact interactions
[
{
"id": "<id>",
"exercised": true,
"callCount": 1,
"consumer": "consumer-name",
"provider": "provider-name",
"state": "provider state",
"uponReceiving": "description",
"withRequest": {
"method": "GET",
"path": "/api/projects/2",
"query": {
"name": "fake"
}
},
"willRespondWith": {
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"id": 1,
"name": "fake"
}
}
}
]
Adds multiple pact interactions to the server.
curl --location --request POST 'http://localhost:9393/api/pactum/pactInteractions' \
--header 'Content-Type: application/json' \
--data-raw '[{
"consumer": "consumer-name",
"provider": "provider-name",
"state": "provider state",
"uponReceiving": "description",
"withRequest": {
"method": "GET",
"path": "/api/projects/2",
"query": {
"name": "fake"
}
},
"willRespondWith": {
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"id": 1,
"name": "fake"
}
}
}]'
Response - returns the pact interaction id.
[ "x121w" ]
Removes a pact interaction or all pact interactions from the mock server.
# Removes a single pact interaction with id m1uh9
curl --location --request DELETE 'http://localhost:9393/api/pactum/pactInteractions?id=m1uh9'
# Removes all pact interactions
curl --location --request DELETE 'http://localhost:9393/api/pactum/pactInteractions'
Interaction Handler allows us to dynamically enable & reuse interactions in the mock server. Let's say you have a mock server running on port 3000 with one interaction handler.
const pactum = require('pactum');
const mock = pactum.mock;
const handler = pactum.handler;
handler.addMockInteractionHandler('get user with', (ctx) => {
const user = db.getUser(ctx.data.id);
return {
withRequest: {
method: 'GET',
path: `/api/users/${ctx.data.id}`
},
willRespondWith: {
status: 200,
body: user
}
}
});
mock.start(3000);
If you start the above mock server, by default it will have zero interactions in it. To add the interaction with handler name 'get user with'
, perform the following request.
curl --location --request POST 'http://localhost:9393/api/pactum/handlers' \
--header 'Content-Type: application/json' \
--data-raw '{
"handlers": [
{
"name": "get user with",
"type": "MOCK"
}
],
"data": {
"id": 10
}
}'
Response - returns the mock interaction id.
[ "x121w" ]