Declarative assertions
@zimic/interceptor
provides a way to declaratively check if your application is making the expected HTTP requests,
using restriction and number of request assertions. They make it possible to
have robust tests with declarative conditions verifying your application, without having to write complex logic to
manually check all requests.
Restrictions​
Restrictions can be declared using
handler.with()
and specify conditions expected
for the intercepted requests. These include headers,
search params, and
bodies. A handler can have multiple restrictions, and all of them
must be met for the request to be matched and receive the mock response.
Restrictions are available in two forms: static and computed.
Static restrictions​
Static restrictions are defined by passing an object to the
handler.with()
method. The object can contain
the properties headers
, searchParams
, and body
, which are used to check the intercepted requests.
Static header restrictions​
To create static restrictions for headers, first define the request headers in your
schema. Then, use the header
property with
the headers that the request must contain.
- Local interceptor
- Remote interceptor
interceptor
.get('/users')
.with({
headers: { authorization: `Bearer ${token}` },
})
.respond({
status: 200,
body: users,
});
await interceptor
.get('/users')
.with({
headers: { authorization: `Bearer ${token}` },
})
.respond({
status: 200,
body: users,
});
Static search param restrictions​
As with headers, declare the request search params in your
schema. Then, use the
searchParams
property with the search params that the request must contain.
- Local interceptor
- Remote interceptor
interceptor
.get('/users')
.with({
searchParams: { query: 'u' },
})
.respond({
status: 200,
body: users,
});
await interceptor
.get('/users')
.with({
searchParams: { query: 'u' },
})
.respond({
status: 200,
body: users,
});
Static body restrictions​
Similarly, body restrictions can be declared with the body
property with the body that the request must contain.
Before creating a body restriction, make sure to declare the request body in your
schema. For more details on using body types,
such as JSON, text, and form data, visit our bodies guide.
- Local interceptor
- Remote interceptor
interceptor
.post('/users')
.with({
body: { username: 'me' },
})
.respond({
status: 200,
body: user,
});
await interceptor
.post('/users')
.with({
body: { username: 'me' },
})
.respond({
status: 200,
body: user,
});
Computed restrictions​
Computed restrictions support more complex or dynamic conditions. They are defined by passing a function to the
handler.with()
method. The function receives the
intercepted request as an argument and returns a boolean value indicating whether the request matches the restriction.
- Local interceptor
- Remote interceptor
interceptor
.post('/users')
.with((request) => {
// Expect the request to have an 'accept' header that starts with 'application'
const accept = request.headers.get('accept');
return accept !== null && accept.startsWith('application');
})
.respond({
status: 201,
body: user,
});
await interceptor
.post('/users')
.with((request) => {
// Expect the request to have an 'accept' header that starts with 'application'
const accept = request.headers.get('accept');
return accept !== null && accept.startsWith('application');
})
.respond({
status: 201,
body: user,
});
A restriction acts as a self-contained condition and can be combined in a single handler. Each restriction can also have multiple properties at once.
- Local interceptor
- Remote interceptor
interceptor
.post('/users')
.with({
headers: { accept: 'application/json' },
searchParams: { query: 'u' },
})
.with((request) => {
return request.body.username.length > 0;
})
.respond({
status: 201,
body: user,
});
await interceptor
.post('/users')
.with({
headers: { accept: 'application/json' },
searchParams: { query: 'u' },
})
.with((request) => {
return request.body.username.length > 0;
})
.respond({
status: 201,
body: user,
});
Exact matching​
By default, restrictions are partial (exact: false
), so requests having additional properties still match the handler
as long as at least the restrictions are met. If you want to enforce exact matching, use exact: true
in the
handler.with()
method.
- Local interceptor
- Remote interceptor
interceptor
.get('/users')
.with({
headers: { authorization: `Bearer ${token}` },
exact: true, // If other headers are present, the request will not match
})
.respond({
status: 200,
body: users,
});
await interceptor
.get('/users')
.with({
headers: { authorization: `Bearer ${token}` },
exact: true, // If other headers are present, the request will not match
})
.respond({
status: 200,
body: users,
});
Number of requests​
Another form of checking the behavior of your application is to assert the number of requests it makes. To achieve this,
use handler.times()
, which takes a number or a
range of numbers as an argument and specifies how many requests the handler should match. If the application makes more
requests than expected, they may be handled by the next handler in the chain or unhandled. To learn more about how
@zimic/interceptor
matches requests and how unhandled requests are processed, check our
unhandled requests guide.
- Local interceptor
- Remote interceptor
interceptor
.get('/users')
.respond({
status: 200,
body: users,
})
.times(1); // Expect exactly one request
await interceptor
.get('/users')
.respond({
status: 200,
body: users,
})
.times(1); // Expect exactly one request
handler.times()
on its own only restricts how
many requests the handler should match. To really validate the number of requests, use
handler.checkTimes()
or
interceptor.checkTimes()
at a point when
your application should have made the requests, such as after a specific action or at the end of a test.
- Local interceptor
- Remote interceptor
const handler = interceptor
.get('/users')
.respond({
status: 200,
body: users,
})
.times(1);
// Run application...
// Check that exactly one request was made
handler.checkTimes();
const handler = await interceptor
.get('/users')
.respond({
status: 200,
body: users,
})
.times(1);
// Run application...
// Check that exactly one request was made
await handler.checkTimes();
A common strategy is to call
interceptor.checkTimes()
in an afterEach
test hook. This way, you can declare how many requests you expect in each handler and have them validated automatically
without needing to call
handler.checkTimes()
in each test.
- Local interceptor
- Remote interceptor
afterEach(() => {
interceptor.checkTimes();
});
afterEach(async () => {
await interceptor.checkTimes();
});