-
Notifications
You must be signed in to change notification settings - Fork 54
Interactions
Interactions are the way to instruct or train the mock server to respond in a particular way when a particular request is received. An interaction is an individual message that combines a request sent by the consumer & minimal expected response replied by the provider. Interactions help you to control the state of your providers. They are categorized into two:
- Mock Interaction
- Pact Interaction
The main difference between mock & pact interactions is that mock interaction cannot form a contract while a pact interaction can.
Interactions can be added or removed from the mock server in the following ways.
- When using pactum as a testing tool
-
pactum.addPactInteraction({ })
- auto removed after the test case execution -
pactum.addMockInteraction({ })
- auto removed after the test case execution
-
- When using pactum as a testing tool or mock server
pactum.mock.addDefaultMockInteraction({ })
pactum.mock.addDefaultPactInteraction({ })
pactum.mock.addDefaultMockInteractions([{ }])
pactum.mock.addDefaultPactInteractions([{ }])
pactum.mock.removeDefaultInteraction('')
pactum.mock.clearDefaultInteractions()
- Through remote api
/api/pactum/mockInteraction
/api/pactum/pactInteraction
Learn more about these methods at Mock Server
Mock Interaction will have the following properties.
Property | Type | Description |
---|---|---|
id | string | id of the interaction |
provider | string | name of the provider |
withRequest | object | request details |
withRequest.method | string | HTTP method |
withRequest.path | string | api path |
withRequest.headers | object | request headers |
withRequest.query | object | query parameters |
withRequest.body | any | request body |
withRequest.ignoreQuery | boolean | ignore request query |
withRequest.ignoreBody | boolean | ignore request body |
withRequest.graphQL | object | graphQL details |
withRequest.graphQL.query | string | graphQL query |
withRequest.graphQL.variables | object | graphQL variables |
willRespondWith | any | response details |
willRespondWith.status | number | response status code |
willRespondWith.headers | object | response headers |
willRespondWith.body | any | response body |
willRespondWith.fixedDelay | number | delays the response by ms |
willRespondWith.randomDelay | object | delays the response by ms |
willRespondWith.randomDelay.min | number | delays the response by ms |
willRespondWith.randomDelay.max | number | delays the response by ms |
willRespondWith.onCall | object | response on consecutive calls |
willRespondWith(req, res) | function | response with custom function |
The response of a mock interaction can be controlled in three forms:
- Static Object
- Custom Function
- Dynamic Object
Type: Function
// Static Object
pactum.addMockInteraction({
withRequest: {
method: 'GET',
path: '/api/currency/INR'
},
willRespondWith: {
status: 200,
headers: {
'content-type': 'application/json'
},
body: {
id: 'INR',
description: 'Indian Rupee'
}
}
});
// Custom Function
pactum.addMockInteraction({
withRequest: {
method: 'GET',
path: '/api/currency/INR'
},
willRespondWith: function (req, res) {
res.status(200);
res.send({
id: 1,
name: 'fake'
});
}
});
// Dynamic Object (change behavior on consecutive calls)
pactum.addDefaultMockInteraction({
withRequest: {
method: 'GET',
path: '/api/projects/1'
},
willRespondWith: {
status: 204, // default response
onCall: {
0: {
status: 200,
body: 'Success :)'
},
1: {
status: 201
},
2: {
status: 202
},
5: {
status: 500,
body: 'No Success :('
}
}
}
});
Property | Type | Description |
---|---|---|
id | string | id of the interaction |
provider | string | name of the provider |
state | string | state of the provider |
uponReceiving | string | description of the request |
withRequest | object | request details |
withRequest.method | string | HTTP method |
withRequest.path | string | api path |
withRequest.headers | object | request headers |
withRequest.query | object | query parameters |
withRequest.body | any | request body |
withRequest.graphQL | object | graphQL details |
withRequest.graphQL.query | string | graphQL query |
withRequest.graphQL.variables | object | graphQL variables |
willRespondWith | object | response details |
willRespondWith.status | number | response status code |
willRespondWith.headers | object | response headers |
willRespondWith.body | any | response body |
Type: Function
pactum.addPactInteraction({
provider: 'currency-service',
state: 'there is INR currency',
uponReceiving: 'a request for INR currency',
withRequest: {
method: 'GET',
path: '/api/currency/INR'
},
willRespondWith: {
status: 200,
headers: {
'content-type': 'application/json'
},
body: {
id: 'INR',
description: 'Indian Rupee'
}
}
})
In real world applications, sometimes it is hard to match an expected request/response with actual request/response.
const interaction = {
withRequest: {
method: 'GET',
path: '/api/orders',
query: {
// randomly generated id
id: 'p144XiRTu'
}
},
willRespondWith: {
status: 200
}
}
To overcome such issues, pactum provides a mechanism for request matching during Component Testing & Consumer Testing and response matching during Provider Verification.
pactum matching mechanism is inspired from Pact - Matching
It supports following matchers
-
like
orsomethingLike
- matches with the type -
regex
orterm
- matches with the regular expression -
eachLike
- matches all the elements in the array with the specified type -
contains
- checks if actual value contains a specified value in it
Matchers can be applied to
- Path - supports only regex
- Query
- Headers - headers are ignored during request matching until explicitly specified
- Body
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: {
// 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: 'GET',
path: '/api/orders'
},
willRespondWith: {
status: 200,
// matches if body is JSON & has quantity & active properties with number & bool types
body: like({
quantity: 2,
active: true
})
}
}
// matching doesn't expand to nested objects
const { like } = pactum.matchers;
const interaction = {
withRequest: {
method: 'GET',
path: '/api/orders'
},
willRespondWith: {
status: 200,
// 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: 'GET',
path: '/api/orders'
},
willRespondWith: {
status: 200,
// 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 & 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'
}
})
}
}
eachLike is similar to like but applies to arrays.
const { eachLike } = pactum.matchers;
const interaction = {
withRequest: {
method: 'GET',
path: '/api/orders'
},
willRespondWith: {
status: 200,
// matches if body is an array of JSON objects
// & each object in array should have it has quantity, active, item
// & item has name property with string type
// & item has colors property with array of strings
body: eachLike({
quantity: 2,
active: true,
item: 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".
const { regex } = pactum.matchers;
const interaction = {
withRequest: {
method: 'GET',
path: regex('/api/projects/\\d+')
},
willRespondWith: {
status: 200,
body: {
name: 'xyx'
birthDate: regex({
generate: '03/05/2020',
matcher: /\d{2}\/\d{2}\/\d{4}/
})
}
}
}
When a request is received by the pactum's mock server, it will attempt to match each interaction with the request. If a match is found, it will respond with the response details in the matched interaction. If no match is found, the mock server will respond with status 404.
As a rule of thumb, you generally want to use exact matching when you're setting up the expectations for a request because you're under control of the data at this stage, and according to Postel's Law, we want to be "strict" with what we send out. Note that the request matching does not allow "unexpected" values to be present in JSON request bodies or query strings. (It does however allow extra headers, because we found that writing expectations that included the headers added by the various frameworks that might be used resulted in tests that were very fiddly to maintain.)
Applicable during component testing and consumer testing
const pactum = require('pactum');
const { like, somethingLike, term, regex, eachLike, contains } = pactum.matchers;
it('Matchers - Path & Query', () => {
return pactum
.addMockInteraction({
withRequest: {
method: 'GET',
// Matches path with value in matcher. Value inside generate is used in contract testing.
path: regex({ generate: '/api/projects/1', matcher: /\/api\/projects\/\d+/ }),
query: {
// Matches with type. Date can be any string.
date: like('08/04/2020'),
// No matcher is applied to the age.
age: '10'
}
},
willRespondWith: {
status: 200
}
})
.get('http://localhost:9393/api/projects/1')
.withQuery('date', '12/00/9632')
.expectStatus(200);
});
it('Matchers - Body', () => {
return pactum
.addMockInteraction({
withRequest: {
method: 'POST',
path: '/api/person',
// checks if the body has array of objects with id, name & address
body: eachLike({
// checks if id matches with regular expression
id: term({ generate: 123, matcher: /\d+/ }),
name: 'Bark',
address: like({
// checks if city is string
city: 'some',
// checks if pin is number
pin: 123
})
})
},
willRespondWith: {
status: 200
}
})
.post('http://localhost:9393/api/person')
.withBody([{
id: 100,
name: 'Dark',
address: {
city: 'another',
pin: 5000
}
}])
.expectStatus(200);
});
You want to be as loose as possible with the matching for the response though. This stops the tests being brittle on the provider side. Generally speaking, it doesn't matter what value the provider actually returns during verification, as long as the types match. When you need certain formats in the values (eg. URLS), you can use regex. Really really consider before you start introducing too many matchers however - for example, yes, the provider might be currently returning a GUID, but would anything in your consumer really break if they returned a different format of string ID? (If it did, that's a nasty code smell!) Note that during provider verification, following Postel's Law of being "relaxed" with what we accept, "unexpected" values in JSON response bodies are ignored. This is expected and is perfectly OK. Another consumer may have an expectation about that field.
Applicable during provider verification
const pactum = require('pactum');
const { like, somethingLike, term, regex, eachLike, contains } = pactum.matchers;
it('Matchers - Body', () => {
return pactum
.addMockInteraction({
withRequest: {
method: 'POST',
path: '/api/person'
},
willRespondWith: {
status: 200,
// create a single array of object with id, name & address
body: eachLike({
// creates a id with 123
id: term({ generate: 123, matcher: /\d+/ }),
name: 'Bark',
address: like({
// creates a city with 'hyd'
city: 'hyd',
// creates a pin with '500081'
pin: 500081
})
})
}
})
.get('http://localhost:9393/api/person')
.expectStatus(200)
.expectJson([{
id: 123,
name: 'Bark',
address: {
cit: 'hyd',
pin: 500081
}
}]);
});