-
Notifications
You must be signed in to change notification settings - Fork 3
api‐zimic‐interceptor‐http
-
HttpInterceptor
-
HttpRequestHandler
- Intercepted HTTP resources
- Guides
zimic/interceptor/http
exports resources to create HTTP interceptors and mock HTTP responses.
HTTP interceptors provide the main API to handle HTTP requests and return mock responses. The methods, paths, status codes, parameters, and responses are statically-typed based on the service schema.
Each interceptor represents a service and can be used to mock its paths and methods.
Creates an HTTP interceptor, the main interface to intercept HTTP requests and return responses. Learn more about declaring interceptor schemas.
Tip
If you are using TypeScript and have an OpenAPI v3 schema, you can use
zimic-http typegen
to automatically generate types for your interceptor schema!
A local interceptor is configured with type: 'local'
. The baseURL
represents the URL should be matched by this
interceptor. Any request starting with the baseURL
will be intercepted if a matching handler
exists.
import { createHttpInterceptor } from '@zimic/interceptor/http';
interface User {
username: string;
}
const interceptor = createHttpInterceptor<{
'/users/:id': {
GET: {
response: {
200: { body: User };
};
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
A remote interceptor is configured with type: 'remote'
. The baseURL
points to an
interceptor server. Any request starting with the baseURL
will be intercepted if a matching
handler exists.
import { createHttpInterceptor } from '@zimic/interceptor/http';
interface User {
username: string;
}
const interceptor = createHttpInterceptor<{
'/users/:id': {
GET: {
response: {
200: { body: User };
};
};
};
}>({
// The interceptor server is at http://localhost:4000
// `/my-service` is a path to differentiate from other
// interceptors using the same server
type: 'remote',
baseURL: 'http://localhost:4000/my-service',
});
A single interceptor server is perfectly capable of handling multiple interceptors and requests. Thus, additional paths are supported and might be necessary to differentiate between conflicting interceptors. If you may have multiple threads or processes applying mocks concurrently to the same interceptor server, it's important to keep the interceptor base URLs unique. Also, make sure that your application is considering the correct URL when making requests.
const interceptor = createHttpInterceptor<{
// ...
}>({
type: 'remote',
// Declaring a base URL with a unique identifier to prevent conflicts
baseURL: `http://localhost:4000/my-service-${crypto.randomUUID()}`,
});
// Your application should use this base URL when making requests
console.log(interceptor.baseURL);
When a request is not matched by any interceptor handlers, it is considered unhandled and will be logged to the console by default.
Tip
If you expected a request to be handled, but it was not, make sure that the interceptor base URL, path, method, and restrictions correctly match the request. Additionally, confirm that no errors occurred while creating the response.
In a local interceptor, unhandled requests can be either bypassed or rejected. Bypassed requests reach the real network, whereas rejected requests fail with an network error. The default behavior in local interceptors is to reject unhandled requests.
Remote interceptors and interceptor server always reject unhandled requests. This is because the unhandled requests have already reached the interceptor server, so there would be no way of bypassing them at this point.
You can override the logging behavior per interceptor with onUnhandledRequest
in
createHttpInterceptor(options)
or by setting interceptor.onUnhandledRequest
.
onUnhandledRequest
also accepts a function to dynamically determine which strategy to use for an unhandled request.
Example 1: Ignore unhandled requests in an interceptor without logging:
import { createHttpInterceptor } from '@zimic/interceptor/http';
const interceptor = createHttpInterceptor<Schema>({
type: 'local',
baseURL: 'http://localhost:3000',
onUnhandledRequest: {
action: 'bypass', // Allow unhandled requests to reach the real network
log: false, // Do not log warnings about unhandled requests
},
});
Example 2: Reject unhandled requests in an interceptor with logging:
import { createHttpInterceptor } from '@zimic/interceptor/http';
const interceptor = createHttpInterceptor<Schema>({
type: 'local',
baseURL: 'http://localhost:3000',
onUnhandledRequest: {
action: 'reject', // Do not allow unhandled requests to reach the real network
log: true, // Log warnings about unhandled requests
},
});
Example 3: Dynamically ignore or reject unhandled requests in an interceptor:
import { createHttpInterceptor } from '@zimic/interceptor/http';
const interceptor = createHttpInterceptor<Schema>({
type: 'local',
baseURL: 'http://localhost:3000',
onUnhandledRequest: async (request) => {
const url = new URL(request.url);
// Ignore only unhandled requests to /assets
if (url.pathname.startsWith('/assets')) {
// Remember: 'bypass' is only available for local interceptors!
// Use 'reject' for remote interceptors.
return { action: 'bypass', log: false };
}
// Reject all other unhandled requests
return { action: 'reject', log: true };
},
});
Note
When a request is unhandled, Zimic looks for a running interceptor whose base URL is the prefix of the unhandled
request URL. If such interceptor is found, its strategy is used, or the default strategy if none was defined. If
multiple interceptors match the request URL, the last one started with await interceptor.start()
will be used,
regardless of existing another interceptor with a more specific base URL.
If no running interceptor matches the request, one of two things may happen:
- If it was targeted to an interceptor server, it will be rejected with a network error. In this case, the logging
behavior is configured with the
--log-unhandled-requests
option in the interceptor server. - If it was not targeted to an interceptor server, it will be bypassed and reach the real network.
The requestSaving
option configures if the intercepted requests are saved and how they are handled. It supports the
following properties:
Property | Description | Default |
---|---|---|
enabled |
Whether request handlers should save their intercepted requests in memory and make them accessible through handler.requests . If you are using interceptor.checkTimes() or handler.checkTimes(), consider enabling this option to get more detailed information in TimesCheckError errors. |
process.env.NODE_ENV === 'test' |
safeLimit |
The safe number of requests to save in memory before logging warnings in the console. If requestSaving.enabled is true and the interceptor is not regularly cleared with interceptor.clear() , the requests may accumulate in memory and cause performance issues. This option does not limit the number of saved requests, only when to log warnings. |
1000 |
Important
If requestSaving.enabled
is true
, make sure to regularly clear the interceptor to avoid requests accumulating in
memory. A common practice is to call interceptor.clear()
after each test.
See Testing for an example of how to manage the lifecycle of interceptors in your tests.
Using a local interceptorimport { createHttpInterceptor } from '@zimic/interceptor/http';
const interceptor = createHttpInterceptor<Schema>({
type: 'local',
baseURL: 'http://localhost:3000',
requestSaving: { enabled: true, safeLimit: 1000 },
});
// Recommended: Clear the interceptor after each test.
// Use the equivalent of `afterEach` in your test framework.
afterEach(() => {
interceptor.clear();
}); |
Using a remote interceptorimport { createHttpInterceptor } from '@zimic/interceptor/http';
const interceptor = createHttpInterceptor<Schema>({
type: 'remote',
baseURL: 'http://localhost:3000',
requestSaving: { enabled: true, safeLimit: 1000 },
});
// Recommended: Clear the interceptor after each test.
// Use the equivalent of `afterEach` in your test framework.
afterEach(async () => {
await interceptor.clear();
}); |
Starts the interceptor. Only interceptors that are running will intercept requests.
await interceptor.start();
When targeting a browser environment with a local interceptor, make sure to follow the client-side post-install guide before starting your interceptors.
Stops the interceptor, preventing it from intercepting HTTP requests. Stopped interceptors are automatically cleared,
exactly as if
interceptor.clear()
had been
called.
await interceptor.stop();
Whether the interceptor is currently running and ready to use.
const isRunning = interceptor.isRunning;
The base URL of the interceptor.
console.log(interceptor.baseURL);
The platform used by the interceptor (browser
or node
).
console.log(interceptor.platform);
Creates an HttpRequestHandler
for the given method and path. The path and method must be
declared in the interceptor schema. The supported methods are: get
, post
, put
, patch
, delete
, head
, and
options
.
When using a remote interceptor, creating a handler is an
asynchronous operation, so you need to await
it. You can also chain any number of operations and apply them by
awaiting the handler.
After a request is intercepted, Zimic tries to find a handler that matches it, considering the base URL of the
interceptor, and the method, path, restrictions, and
limits on the number of requests of the handler. The handlers are checked from the last one
created to the first one, so new handlers have preference over older ones. This allows you to declare generic and
specific handlers based on their order of creation. For example, a generic handler for GET /users
can return an empty
list, while a specific handler in a test case can return a list with some users. In this case, the specific handler will
be considered first as long as it is created after the generic one.
Using a local interceptorimport { createHttpInterceptor } from '@zimic/interceptor/http';
const interceptor = createHttpInterceptor<{
'/users': {
GET: {
response: {
200: { body: User[] };
};
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
const listHandler = interceptor.get('/users').respond({
status: 200
body: [{ username: 'me' }],
}); |
Using a remote interceptorimport { createHttpInterceptor } from '@zimic/interceptor/http';
const interceptor = createHttpInterceptor<{
'/users': {
GET: {
response: {
200: { body: User[] };
};
};
};
}>({
type: 'remote',
baseURL: 'http://localhost:4000/my-service',
});
const listHandler = await interceptor.get('/users').respond({
status: 200
body: [{ username: 'me' }],
}); |
Paths with parameters are supported, such as /users/:id
. Even when using a computed path (e.g. /users/1
), the
original path is automatically inferred, guaranteeing type safety.
import { createHttpInterceptor } from '@zimic/interceptor/http';
const interceptor = createHttpInterceptor<{
'/users/:id': {
PUT: {
request: {
body: { username: string };
};
response: {
204: {};
};
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
interceptor.get('/users/:id'); // Matches any id
interceptor.get(`/users/${1}`); // Only matches id 1
request.pathParams
contains the parsed path parameters of a request and have their type automatically inferred from
the path string. For example, the path /users/:userId
will result in a request.pathParams
of type
{ userId: string }
.
Using a local interceptorconst updateHandler = interceptor.put('/users/:id').respond((request) => {
console.log(request.pathParams); // { id: '1' }
return {
status: 200,
body: { username: 'me' },
};
});
await fetch('http://localhost:3000/users/1', { method: 'PUT' }); |
Using a remote interceptorconst updateHandler = await interceptor.put('/users/:id').respond((request) => {
console.log(request.pathParams); // { id: '1' }
return {
status: 200,
body: { username: 'me' },
};
});
await fetch('http://localhost:3000/users/1', { method: 'PUT' }); |
Checks if all handlers created by this interceptor have matched the number of requests declared with
handler.times()
.
If some handler has matched fewer or more requests than expected, this method will throw a TimesCheckError
error,
including a stack trace to the handler.times()
that was not satisfied.
Tip
When requestSaving.enabled
is true
in your interceptor, the TimesCheckError
errors will also
list each unmatched request with diff of the expected and received data. This is useful for debugging requests that
did not match a handler with restrictions.
Using a local interceptorinterceptor.checkTimes(); |
Using a remote interceptorawait interceptor.checkTimes(); |
This is useful in an afterEach
hook (or equivalent) to make sure that all expected requests were made at the end of
each test.
Using a local interceptorafterEach(() => {
interceptor.checkTimes();
}); |
Using a remote interceptorafterEach(async () => {
await interceptor.checkTimes();
}); |
See Testing for an example of how to manage the lifecycle of interceptors in your tests.
Clears the interceptor and all of its HttpRequestHandler
instances, including their registered
responses and intercepted requests. After calling this method, the interceptor will no longer intercept any requests
until new mock responses are registered.
This method is useful to reset the interceptor mocks between tests.
Using a local interceptorinterceptor.clear(); |
Using a remote interceptorawait interceptor.clear(); |
Infers the schema of an HTTP interceptor.
import { type InferHttpInterceptorSchema } from '@zimic/interceptor/http';
const interceptor = createHttpInterceptor<{
'/users': {
GET: {
response: { 200: { body: User[] } };
};
};
}>({
type: 'local',
baseURL: 'http://localhost:3000',
});
type Schema = InferHttpInterceptorSchema<typeof interceptor>;
// {
// '/users': {
// GET: {
// response: { 200: { body: User[] } };
// };
// };
// }
HTTP request handlers allow declaring HTTP responses to return for intercepted requests. They also keep track of the intercepted requests and their responses, which can be used to check if the requests your application has made are correct.
When multiple handlers match the same method and path, the last created with
interceptor.<method>(path)
will be used.
Returns the method that matches a handler.
Using a local interceptorconst handler = interceptor.post('/users');
console.log(handler.method); // 'POST' |
Using a remote interceptorconst handler = await interceptor.post('/users');
console.log(handler.method); // 'POST' |
Returns the path that matches a handler. The base URL of the interceptor is not included, but it is used when matching requests.
Using a local interceptorconst handler = interceptor.get('/users');
console.log(handler.path); // '/users' |
Using a remote interceptorconst handler = await interceptor.get('/users');
console.log(handler.path); // '/users' |
Declares a restriction to intercepted requests. headers
, searchParams
, and body
are supported to limit which
requests will match the handler and receive the mock response. If multiple restrictions are declared, either in a single
object or with multiple calls to handler.with()
, all of them must be met, essentially creating an AND condition.
Declaring restrictions for headers:
Using a local interceptorconst creationHandler = interceptor
.get('/users')
.with({
headers: { authorization: `Bearer ${token}` },
})
.respond({
status: 200,
body: [{ username: 'me' }],
}); |
Using a remote interceptorconst creationHandler = await interceptor
.get('/users')
.with({
headers: { authorization: `Bearer ${token}` },
})
.respond({
status: 200,
body: [{ username: 'me' }],
}); |
An equivalent alternative using HttpHeaders
:
Using a local interceptorimport { type HttpSchema, HttpHeaders } from '@zimic/http';
type UserListHeaders = HttpSchema.Headers<{
authorization: string;
}>;
const headers = new HttpHeaders<UserListHeaders>({
authorization: `Bearer ${token}`,
});
const creationHandler = interceptor
.get('/users')
.with({ headers })
.respond({
status: 200,
body: [{ username: 'me' }],
}); |
Using a remote interceptorimport { type HttpSchema, HttpHeaders } from '@zimic/http';
type UserListHeaders = HttpSchema.Headers<{
authorization: string;
}>;
const headers = new HttpHeaders<UserListHeaders>({
authorization: `Bearer ${token}`,
});
const creationHandler = await interceptor
.get('/users')
.with({ headers })
.respond({
status: 200,
body: [{ username: 'me' }],
}); |
Declaring restrictions for search params:
Using a local interceptorconst creationHandler = interceptor
.get('/users')
.with({
searchParams: { query: 'u' },
})
.respond({
status: 200,
body: [{ username: 'me' }],
}); |
Using a remote interceptorconst creationHandler = await interceptor
.get('/users')
.with({
searchParams: { query: 'u' },
})
.respond({
status: 200,
body: [{ username: 'me' }],
}); |
An equivalent alternative using HttpSearchParams
:
Using a local interceptorimport { type HttpSchema, HttpSearchParams } from '@zimic/http';
type UserListSearchParams = HttpSchema.SearchParams<{
query?: string;
}>;
const searchParams = new HttpSearchParams<UserListSearchParams>({
query: 'u',
});
const creationHandler = interceptor
.get('/users')
.with({ searchParams })
.respond({
status: 200,
body: [{ username: 'me' }],
}); |
Using a remote interceptorimport { type HttpSchema, HttpSearchParams } from '@zimic/http';
type UserListSearchParams = HttpSchema.SearchParams<{
query?: string;
}>;
const searchParams = new HttpSearchParams<UserListSearchParams>({
query: 'u',
});
const creationHandler = await interceptor
.get('/users')
.with({ searchParams })
.respond({
status: 200,
body: [{ username: 'me' }],
}); |
Declaring restrictions for a JSON body:
Using a local interceptorconst creationHandler = interceptor
.post('/users')
.with({
body: { username: 'me' },
})
.respond({
status: 201,
body: { username: 'me' },
}); |
Using a remote interceptorconst creationHandler = await interceptor
.post('/users')
.with({
body: { username: 'me' },
})
.respond({
status: 201,
body: { username: 'me' },
}); |
For JSON bodies to be correctly parsed, make sure that the intercepted requests have the header
content-type: application/json
.
Declaring restrictions for a form data body:
Using a local interceptorimport { type HttpSchema, HttpFormData } from '@zimic/http';
type UserCreationData = HttpSchema.FormData<{
username: string;
profilePicture: Blob;
}>;
const formData = new HttpFormData<UserCreationData>();
formData.append('username', 'me');
formData.append(
'profilePicture',
new File(['content'], 'profile.png', {
type: 'image/png',
}),
);
const creationHandler = interceptor
.post('/users')
.with({
body: formData,
})
.respond({
status: 201,
body: { username: 'me' },
}); |
Using a remote interceptorimport { type HttpSchema, HttpFormData } from '@zimic/http';
type UserCreationData = HttpSchema.FormData<{
username: string;
profilePicture: Blob;
}>;
const formData = new HttpFormData<UserCreationData>();
formData.append('username', 'me');
formData.append(
'profilePicture',
new File(['content'], 'profile.png', {
type: 'image/png',
}),
);
const creationHandler = await interceptor
.post('/users')
.with({
body: formData,
})
.respond({
status: 201,
body: { username: 'me' },
}); |
For form data bodies to be correctly parsed, make sure that the intercepted requests have the header
content-type: multipart/form-data
.
Declaring restrictions for a blob body:
Using a local interceptorconst creationHandler = interceptor
.post('/users')
.with({
body: new Blob(['content'], {
type: 'application/octet-stream',
}),
})
.respond({
status: 201,
body: { username: 'me' },
}); |
Using a remote interceptorconst creationHandler = await interceptor
.post('/users')
.with({
body: new Blob(['content'], {
type: 'application/octet-stream',
}),
})
.respond({
status: 201,
body: { username: 'me' },
}); |
For blob bodies to be correctly parsed, make sure that the intercepted requests have the header content-type
indicating a binary data, such as application/octet-stream
, image/png
, audio/mp3
, etc.
Declaring restrictions for a plain text body:
Using a local interceptorconst creationHandler = interceptor
.post('/users')
.with({
body: 'content',
})
.respond({
status: 201,
body: { username: 'me' },
}); |
Using a remote interceptorconst creationHandler = await interceptor
.post('/users')
.with({
body: 'content',
})
.respond({
status: 201,
body: { username: 'me' },
}); |
Declaring restrictions for search params (x-www-form-urlencoded
) body:
Using a local interceptorimport { type HttpSchema, HttpSearchParams } from '@zimic/http';
type UserGetByIdSearchParams = HttpSchema.SearchParams<{
username: string;
}>;
const searchParams = new HttpSearchParams<UserGetByIdSearchParams>({
query: 'u',
});
const creationHandler = interceptor
.post('/users')
.with({
body: searchParams,
})
.respond({
status: 201,
body: { username: 'me' },
}); |
Using a remote interceptorimport { type HttpSchema, HttpSearchParams } from '@zimic/http';
type UserGetByIdSearchParams = HttpSchema.SearchParams<{
username: string;
}>;
const searchParams = new HttpSearchParams<UserGetByIdSearchParams>({
query: 'u',
});
const creationHandler = await interceptor
.post('/users')
.with({
body: searchParams,
})
.respond({
status: 201,
body: { username: 'me' },
}); |
For plain text bodies to be correctly parsed, make sure that the intercepted requests have the header content-type
indicating a plain text, such as text/plain
.
By default, restrictions use exact: false
, meaning that any request containing the declared restrictions will
match the handler, regardless of having more properties or values. In the examples above, requests with more properties
in the headers, search params, or body would still match the restrictions.
If you want to match only requests with the exact values declared, you can use exact: true
:
Using a local interceptorconst creationHandler = interceptor
.post('/users')
.with({
headers: { 'content-type': 'application/json' },
body: { username: 'me' },
exact: true, // Only requests with these exact headers and body will match
})
.respond({
status: 201,
body: { username: 'me' },
}); |
Using a remote interceptorconst creationHandler = await interceptor
.post('/users')
.with({
headers: { 'content-type': 'application/json' },
body: { username: 'me' },
exact: true, // Only requests with these exact headers and body will match
})
.respond({
status: 201,
body: { username: 'me' },
}); |
A function is also supported to declare restrictions in case they are dynamic. Learn more about the request
object at
Intercepted HTTP resources.
Using a local interceptorconst creationHandler = interceptor
.post('/users')
.with((request) => {
const accept = request.headers.get('accept');
return accept !== null && accept.startsWith('application');
})
.respond({
status: 201,
body: [{ username: 'me' }],
}); |
Using a remote interceptorconst creationHandler = await interceptor
.post('/users')
.with((request) => {
const accept = request.headers.get('accept');
return accept !== null && accept.startsWith('application');
})
.respond({
status: 201,
body: [{ username: 'me' }],
}); |
The function should return a boolean: true
if the request matches the handler and should receive the mock response;
false
otherwise.
Declares a response to return for matched intercepted requests.
When the handler matches a request, it will respond with the given declaration. The response type is statically validated against the schema of the interceptor.
Declaring responses with JSON body:
Using a local interceptorconst listHandler = interceptor.get('/users').respond({
status: 200,
body: [{ username: 'me' }],
}); |
Using a remote interceptorconst listHandler = await interceptor.get('/users').respond({
status: 200,
body: [{ username: 'me' }],
}); |
Declaring responses with form data body:
Using a local interceptorimport { type HttpSchema, HttpFormData } from '@zimic/http';
type UserGetByIdData = HttpSchema.FormData<{
username: string;
profilePicture: Blob;
}>;
const formData = new HttpFormData<UserGetByIdData>();
formData.append('username', 'me');
formData.append(
'profilePicture',
new File(['content'], 'profile.png', {
type: 'image/png',
}),
);
const listHandler = interceptor.get('/users/:id').respond({
status: 200,
body: formData,
}); |
Using a remote interceptorimport { type HttpSchema, HttpFormData } from '@zimic/http';
type UserGetByIdData = HttpSchema.FormData<{
username: string;
profilePicture: Blob;
}>;
const formData = new HttpFormData<UserGetByIdData>();
formData.append('username', 'me');
formData.append(
'profilePicture',
new File(['content'], 'profile.png', {
type: 'image/png',
}),
);
const listHandler = await interceptor.get('/users/:id').respond({
status: 200,
body: formData,
}); |
Declaring responses with blob body:
Using a local interceptorconst listHandler = interceptor.get('/users').respond({
status: 200,
body: new Blob(['content'], {
type: 'application/octet-stream',
}),
}); |
Using a remote interceptorconst listHandler = await interceptor.get('/users').respond({
status: 200,
body: new Blob(['content'], {
type: 'application/octet-stream',
}),
}); |
Declaring responses with plain text body:
Using a local interceptorconst listHandler = interceptor.get('/users').respond({
status: 200,
body: 'content',
}); |
Using a remote interceptorconst listHandler = await interceptor.get('/users').respond({
status: 200,
body: 'content',
}); |
Declaring responses with search params (x-www-form-urlencoded
) body:
Using a local interceptorimport { type HttpSchema, HttpSearchParams } from '@zimic/http';
type UserGetByIdSearchParams = HttpSchema.SearchParams<{
username: string;
}>;
const searchParams = new HttpSearchParams<UserGetByIdSearchParams>({
query: 'u',
});
const listHandler = interceptor.get('/users').respond({
status: 200,
body: searchParams,
}); |
Using a remote interceptorimport { type HttpSchema, HttpSearchParams } from '@zimic/http';
type UserGetByIdSearchParams = HttpSchema.SearchParams<{
username: string;
}>;
const searchParams = new HttpSearchParams<UserGetByIdSearchParams>({
query: 'u',
});
const listHandler = await interceptor.get('/users').respond({
status: 200,
body: searchParams,
}); |
A function is also supported to declare a response in case it is dynamic. Learn more about the request
object at
Intercepted HTTP resources.
Using a local interceptorconst listHandler = interceptor.get('/users').respond((request) => {
const username = request.searchParams.get('username');
if (!username) {
return { status: 400 };
}
return {
status: 200,
body: [{ username }],
};
}); |
Using a remote interceptorconst listHandler = await interceptor.get('/users').respond((request) => {
const username = request.searchParams.get('username');
if (!username) {
return { status: 400 };
}
return {
status: 200,
body: [{ username }],
};
}); |
Declares a number of intercepted requests that the handler will be able to match and return its response.
If only one argument is provided, the handler will match exactly that number of requests. In case of two arguments, the handler will consider an inclusive range, matching at least the minimum (first argument) and at most the maximum (second argument) number of requests.
Once the handler receives more requests than the maximum number declared, it will stop matching requests and returning
its response. In this case, Zimic will try other handlers until one eligible is found, otherwise the request will be
either bypassed or rejected. Learn more about how Zimic decides which handler to use for an intercepted request in the
interceptor.<method>(path)
API reference.
Using a local interceptorconst exactListHandler = interceptor
.get('/users')
.respond({
status: 200,
body: [{ username: 'me' }],
})
.times(1); // Matches exactly one request
const rangeListHandler = interceptor
.get('/users')
.respond({
status: 200,
body: [{ username: 'me' }],
})
.times(0, 3); // Matches at least 0 and at most 3 requests |
Using a remote interceptorconst exactListHandler = await interceptor
.get('/users')
.respond({
status: 200,
body: [{ username: 'me' }],
})
.times(1); // Matches exactly one request
const rangeListHandler = await interceptor
.get('/users')
.respond({
status: 200,
body: [{ username: 'me' }],
})
.times(0, 3); // Matches at least 0 and at most 3 requests |
Important
To make sure that all expected requests were made, use interceptor.checkTimes()
or
handler.checkTimes()
. interceptor.checkTimes()
is
generally preferred, as it checks all handlers created by the interceptor with a single call.
Tip
Prior to v0.12.0, a common strategy to check the number of requests was to assert the length of handler.requests
.
handler.times()
, combined with handler.checkTimes()
or
interceptor.checkTimes()
, archives the same purpose in a shorter and more declarative
way. In most cases, these methods are preferred over manually checking the length of handler.requests
.
Checks if the handler has matched the expected number of requests declared with handler.times()
.
If the handler has matched fewer or more requests than expected, this method will throw a TimesCheckError
error,
including a stack trace to the handler.times()
that was not satisfied.
Using a local interceptorconst listHandler = interceptor
.get('/users')
.respond({
status: 200,
body: [],
})
.times(1);
// Run application...
// Check that exactly 1 request was made
handler.checkTimes(); |
Using a remote interceptorconst listHandler = await interceptor
.get('/users')
.respond({
status: 200,
body: [],
})
.times(1);
// Run application...
// Check that exactly 1 request was made
await handler.checkTimes(); |
Clears any response declared with handler.respond(declaration)
, restrictions
declared with handler.with(restriction)
, and intercepted requests, making the handler
stop matching requests. The next handler, created before this one, that matches the same method and path will be used if
present. If not, the requests of the method and path will not be intercepted.
To make the handler match requests again, register a new response with handler.respond()
.
Using a local interceptorconst genericHandler = interceptor.get('/users').respond({
status: 200,
body: [],
});
const specificHandler = interceptor.get('/users').respond({
status: 200,
body: [{ username: 'me' }],
});
specificHandler.clear();
// Now, requests GET /users will match `genericHandler` and receive an empty array
console.log(specificHandler.requests); // [] |
Using a remote interceptorconst genericHandler = await interceptor.get('/users').respond({
status: 200,
body: [],
});
const specificHandler = await interceptor.get('/users').respond({
status: 200,
body: [{ username: 'me' }],
});
await specificHandler.clear();
// Now, requests GET /users will match `genericHandler` and receive an empty array
console.log(specificHandler.requests); // [] |
Returns the intercepted requests that matched this handler, along with the responses returned to each of them. This is
useful for testing that the correct requests were made by your application. Learn more about the request
and
response
objects at Intercepted HTTP resources.
Important
This method can only be used if requestSaving.enabled
is true
when creating the interceptor. See
Saving intercepted requests for more information.
Using a local interceptorconst updateHandler = interceptor.put('/users/:id').respond((request) => {
const newUsername = request.body.username;
return {
status: 200,
body: [{ username: newUsername }],
};
});
await fetch(`http://localhost:3000/users/${1}`, {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ username: 'new' }),
});
expect(updateHandler.requests).toHaveLength(1);
expect(updateHandler.requests[0].pathParams).toEqual({ id: '1' });
expect(updateHandler.requests[0].body).toEqual({ username: 'new' });
expect(updateHandler.requests[0].response.status).toBe(200);
expect(updateHandler.requests[0].response.body).toEqual([{ username: 'new' }]); |
Using a remote interceptorconst updateHandler = await interceptor.put('/users/:id').respond((request) => {
const newUsername = request.body.username;
return {
status: 200,
body: [{ username: newUsername }],
};
});
await fetch(`http://localhost:3000/users/${1}`, {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ username: 'new' }),
});
expect(updateHandler.requests).toHaveLength(1);
expect(updateHandler.requests[0].pathParams).toEqual({ id: '1' });
expect(updateHandler.requests[0].body).toEqual({ username: 'new' });
expect(updateHandler.requests[0].response.status).toBe(200);
expect(updateHandler.requests[0].response.body).toEqual([{ username: 'new' }]); |
The intercepted requests and responses are typed based on their
interceptor schema. They are available as simplified objects based on the
Request
and
Response
web APIs. body
contains the parsed body, while typed
headers, path params and search params are in headers
, pathParams
, and searchParams
, respectively.
The body is automatically parsed based on the header content-type
of the request or response. The following table
shows how each type is parsed, where *
indicates any other resource that does not match the previous types:
content-type |
Parsed to |
---|---|
application/json |
JSON |
application/xml |
String |
application/x-www-form-urlencoded |
HttpSearchParams |
application/* (others) |
Blob |
multipart/form-data |
HttpFormData |
multipart/* (others) |
Blob |
text/* |
String |
image/* |
Blob |
audio/* |
Blob |
font/* |
Blob |
video/* |
Blob |
*/* (others) |
JSON if possible, otherwise String
|
If no content-type
exists or it is unknown, Zimic tries to parse the body as JSON and falls back to plain text if it
fails.
If you need access to the original Request
and Response
objects, you can use the request.raw
property:
console.log(request.raw); // Request{}
console.log(request.response.raw); // Response{}
© Zimic
Docs |
Issues | Examples |
Roadmap
Help us improve these docs!
Report an issue or
edit on GitHub.