Skip to main content

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.

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.

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.

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.

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,
});
TIP: Combining restrictions

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.

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.

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.

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.

const handler = interceptor
.get('/users')
.respond({
status: 200,
body: users,
})
.times(1);

// Run application...

// Check that exactly one request was made
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.

afterEach(() => {
interceptor.checkTimes();
});