Request mocking for Puppeteer and Playwright
Mockiavelli is HTTP request mocking library for Puppeteer and Playwright. It was created to enable effective testing of Single Page Apps in isolation and independently from API services.
Main features
- simple, minimal API
- mock network requests directly in the test case
- inspect and assert requests payload
- match request by method, url, path parameters and query strings
- support for cross-origin requests
- works with every testing framework running in node.js
- fully typed in Typescript and well tested
- lightweight - only 4 total dependencies (direct and indirect)
Docs
Installation
npm install mockiavelli -D
or if you are using yarn:
yarn add mockiavelli -D
- Mockiavelli requires one of the following to be installed separately:
- Puppeteer (in versions 2.x - 8.x)
- Playwright (in version 1.x)
- If you're using jest we also recommend to install jest-puppeteer or jest-playwright
Getting started
To start using Mockiavelli, you need to instantiate it by providing it a page
- instance of Puppeteer Page or Playwright Page
import { Mockiavelli } from 'mockiavelli';
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
const mockiavelli = await Mockiavelli.setup(page);
Mockiavelli will start to intercept all HTTP requests issued from this page.
To define response for a given request, call mockiavelli.mock<HTTP_METHOD>
with request URL and response object:
const getUsersMock = mockiavelli.mockGET('/api/users', {
status: 200,
body: [
{ id: 123, name: 'John Doe' },
{ id: 456, name: 'Mary Jane' },
],
});
Now every GET /api/users
request issued from this page will receive 200 OK
response with provided body.
const users = await page.evaluate(() => {
return fetch('/api/users').then((res) => res.json());
});
console.log(users); // [{id: 123, name: 'John Doe' }, {id: 456, name: 'Mary Jane'}]
Full example
The example below is a Jest test case (with jest-puppeteer preset) verifies a sign-up form in a locally running application.
Mockiavelli is used to mock and assert request that the app makes to REST API upon form submission.
import { Mockiavelli } from 'mockiavelli';
test('Sign-up form', async () => {
// Enable mocking on instance of puppeteer Page (provided by jest-puppeteer)
const mockiavelli = await Mockiavelli.setup(page);
// Navigate to application
await page.goto('http://localhost:8000/');
// Configure mocked response
const postUserMock = mockiavelli.mockPOST('/api/user', {
status: 201,
body: {
userId: '123',
},
});
// Perform interaction
await page.type('input.name', 'John Doe');
await page.type('input.email', '[email protected]');
await page.click('button.submit');
// Verify request payload
const postUserRequest = await postUserMock.waitForRequest();
expect(postUserRequest.body).toEqual({
user_name: 'John Doe',
user_email: '[email protected]',
});
// Verify message shown on the screen
await expect(page).toMatch('Created account ID: 123');
});
Usage guide
URL and method matching
Request can be matched by:
-
providing URL string to
mockiavelli.mock<HTTP_METHOD>
method:mockiavelli.mockGET('/api/users?age=30', {status: 200, body: [....]})
-
providing matcher object to
mockiavelli.mock<HTTP_METHOD>
methodmockiavelli.mockGET({ url: '/api/users', query: { age: '30' } }, {status: 200, body: [....]})
-
providing full matcher object
mockiavelli.mock
methodmockiavelli.mock({ method: 'GET', url: '/api/users', query: { age: '30' } }, {status: 200, body: [...]})
Path parameters matching
Path parameters in the URL can be matched using :param
notation, thanks to path-to-regexp library.
If mock matches the request, those params are exposed in request.params
property.
const getUserMock = mockiavelli.mockGET('/api/users/:userId', { status: 200 });
// GET /api/users/1234 => 200
// GET /api/users => 404
// GET /api/users/1234/categories => 404
console.log(await getUserMock.waitForRequest());
// { params: {userId : "12345"}, path: "/api/users/12345", ... }
Mockiavelli uses
Query params matching
Mockiavelli supports matching requests by query parameters. All defined params are then required to match the request, but excess params are ignored:
mockiavelli.mockGET('/api/users?city=Warsaw&sort=asc', { status: 200 });
// GET /api/users?city=Warsaw&sort=asc => 200
// GET /api/users?city=Warsaw&sort=asc&limit=10 => 200
// GET /api/users?city=Warsaw => 404
It is also possible to define query parameters as object. This notation works great for matching array query params:
mockiavelli.mockGET(
{ url: '/api/users', query: { status: ['active', 'blocked'] } },
{ status: 200 }
);
// GET /api/users?status=active&status=blocked => 200
Request assertion
mockiavelli.mock<HTTP_METHOD>
and mockiavelli.mock
methods return an instance of Mock
class that records all requests the matched given mock.
To assert details of request made by application use async mock.waitForRequest()
method. It will throw an error if no matching request was made.
const postUsersMock = mockiavelli.mockPOST('/api/users', { status: 200 });
// ... perform interaction on tested page ...
const postUserRequest = await postUsersMock.waitForRequest(); // Throws if POST /api/users request was not made
expect(postUserRequest.body).toBe({
name: 'John',
email: '[email protected]',
});
One-time mocks
By default mock are persistent, meaning that they will respond to multiple matching requests:
mockiavelli.mockGET('/api/users', { status: 200 });
// GET /api/users => 200
// GET /api/users => 200
To change this behaviour and disable mock once it matched a request use once
option:
mockiavelli.mockGET('/api/users', { status: 200 }, { once: true });
// GET /api/users => 200
// GET /api/users => 404
Matching order
Mocks are matched in the "newest first" order. To override previously defined mock simply define new one:
mockiavelli.mockGET('/api/users', { status: 200 });
mockiavelli.mockGET('/api/users', { status: 401 });
// GET /api/users => 401
mockiavelli.mockGET('/api/users', { status: 500 });
// GET /api/users => 500
Matching priority
To change the default "newest first" matching order, you define mocks with combination of once
and priority
parameters:
mockiavelli.mockGET(
'/api/users',
{ status: 404 },
{ once: true, priority: 10 }
);
mockiavelli.mockGET('/api/users', { status: 500 }, { once: true, priority: 5 });
mockiavelli.mockGET('/api/users', { status: 200 });
// GET /api/users => 404
// GET /api/users => 500
// GET /api/users => 200
Specifying API base url
It is possible to initialize Mockiavelli instance with specified API base url. This API base url is added to every mocked request url.
const mockiavelli = await Mockiavelli.setup(page, { baseUrl: '/api/v1' });
mockiavelli.mockGET('/users', { status: 200 });
// GET /api/v1/users => 200
Cross-origin (cross-domain) API requests
Mockiavelli has built-in support for cross-origin requests. If application and API are not on the same origin (domain) just provide the full request URL to mockiavelli.mock<HTTP_METHOD>
mockiavelli.mockGET('http://api.example.com/api/users', { status: 200 });
// GET http://api.example.com/api/users => 200
// GET http://another-domain.example.com/api/users => 404
Stop mocking
To stop intercept requests you can call mockiavelli.disable
method (all requests will send to real services).
Then you can enable mocking again by mockiavelli.enable
method.
mockiavelli.mockGET('/api/users/:userId', {
status: 200,
body: { name: 'John Doe' },
});
// GET /api/users/1234 => 200 { name: 'John Doe' }
mockiavelli.disable();
// GET /api/users/1234 => 200 { name: 'Jacob Kowalski' } <- real data from backend
mockiavelli.enable();
// GET /api/users/1234 => 200 { name: 'John Doe' }
Dynamic responses
It is possible to define mocked response in function of incoming request. This is useful if you need to use some information from request URL or body in the response:
mockiavelli.mockGET('/api/users/:userId', (request) => {
return {
status: 200,
body: {
id: request.params.userId,
name: 'John',
email: '[email protected]',
...
},
};
});
// GET /api/users/123 => 200 {"id": "123", ... }
Not matched requests
In usual scenarios, you should mock all requests done by your app.
Any XHR or fetched request done by the page not matched by any mock will be responded with 404 Not Found
. Mockiavelli will also log this event to console:
Mock not found for request: type=fetch method=GET url=http://example.com
Debug mode
Passing {debug: true}
to Mockiavelli.setup
enables rich debugging in console:
await Mockiavelli.setup(page, { debug: true });
API
class Mockiavelli
Mockiavelli.setup(page, options): Promise<Mockiavelli>
Factory method used to set-up request mocking on provided Puppeteer or Playwright Page. It creates and returns an instance of Mockiavelli
Once created, mockiavelli will intercept all requests made by the page and match them with defined mocks.
If request does not match any mocks, it will be responded with 404 Not Found
.
Arguments
page
(Page) instance of Puppeteer Page or Playwright Pageoptions
(object) configuration optionsbaseUrl: string
specify the API base url, which will be added to every mocked request urldebug: boolean
turns debug mode with logging to console (default:false
)
Example
import { puppeteer } from 'puppeteer';
import { Mockiavelli } from 'mockiavelli';
const browser = await puppeteer.launch();
const page = await browser.newPage();
const mockiavelli = await Mockiavelli.setup(page);
Returns
Promise resolved with instance of Mockiavelli
once request mocking is established.
mockiavelli.mock(matcher, response, options?)
Respond all requests of matching matcher
with provided response
.
Arguments
matcher
(object) matches request with mock.method: string
- any valid HTTP methodurl: string
- can be provided as path (/api/endpoint
) or full URL (http://example.com/endpoint
) for CORS requests. Supports path parameters (/api/users/:user_id
)query?: object
object literal which accepts strings and arrays of strings as values, transformed to queryString
response
(object | function) content of mocked response. Can be a object or a function returning object with following properties:status: number
headers?: object
body?: any
options?
(object) optional config objectprority
(number) when intercepted request matches multiple mock, mockiavelli will use the one with highest priorityonce
(boolean) (default: false) when set to true intercepted request will be matched only once
Returns
Instance of Mock
.
Example
mockiavelli.mock(
{
method: 'GET',
url: '/api/clients',
query: {
city: 'Bristol',
limit: 10,
},
},
{
status: 200,
headers: {...},
body: [{...}],
}
);
mockiavelli.mock<HTTP_METHOD>(matcher, response, options?)
Shorthand method for mockiavelli.mock
. Matches all request with HTTP_METHOD
method. In addition to matcher object, it also accepts URL string as first argument.
matcher: (string | object)
URL string or object with following properties:url: string
- can be provided as path (/api/endpoint
) or full URL (http://example.com/endpoint
) for CORS requests. Supports path parameters (/api/users/:user_id
)query?: object
object literal which accepts strings and arrays of strings as values, transformed to queryString
response: (object | function)
content of mocked response. Can be a object or a function returning object with following properties:status: number
headers?: object
body?: any
options?: object
optional config objectprority?: number
when intercepted request matches multiple mock, mockiavelli will use the one with highest priority. Default:0
once: boolean
when set to true intercepted request will be matched only once. Default:false
Available methods are:
mockiavelli.mockGET
mockiavelli.mockPOST
mockiavelli.mockDELETE
mockiavelli.mockPUT
mockiavelli.mockPATCH
Examples
// Basic example
mockiavelli.mockPOST('/api/clients', {
status: 201,
body: {...},
});
// Match by query parameters passed in URL
mockiavelli.mockGET('/api/clients?city=Bristol&limit=10', {
status: 200,
body: [{...}],
});
// Match by path params
mockiavelli.mockGET('/api/clients/:clientId', {
status: 200,
body: [{...}],
});
// CORS requests
mockiavelli.mockGET('http://example.com/api/clients/', {
status: 200,
body: [{...}],
});
mockiavelli.disable()
Stop mocking of requests by Mockiavelli.
After that all requests pass to real endpoints.
This method does not reset set mocks or possibility to set mocks, so when we then enable again mocking by mockiavelli.enable()
, all set mocks works again.
mockiavelli.enable()
To enable mocking of requests by Mockiavelli when previously mockiavelli.diable()
was called.
class Mock
waitForRequest(index?: number): Promise<MatchedRequest>
Retrieve n-th request matched by the mock. The method is async - it will wait 100ms for requests to be intercepted to avoid race condition issue. Throws if mock was not matched by any request.
Arguments
index
(number) index of request to return. Default: 0.
Returns
Promise resolved with MatchedRequest
- object representing request that matched the mock:
method: string
- request's method (GET, POST, etc.)url: string
- request's full URL. Example:http://example.com/api/clients?name=foo
hostname: string
- request protocol and host. Example:http://example.com
headers: object
- object with HTTP headers associated with the request. All header names are lower-case.path: string
- request's url path, without query string. Example:'/api/clients'
query: object
- request's query object, as returned fromquerystring.parse
. Example:{name: 'foo'}
body: any
- JSON deserialized request's post body, if anytype: string
- request's resource type. Possible values arexhr
andfetch
params: object
- object with path parameters specified inurl
Example
const patchClientMock = mockiavelli.mockPATCH('/api/client/:clientId', { status: 200 });
// .. send request from page ...
const patchClientRequest = await patchClientMock.waitForRequest();
expect(patchClientRequest).toEqual({
method: 'PATCH',
url: 'http://example.com/api/client/1020',
hostname: 'http://example.com',
headers: {...},
path: '/api/client/1020',
query: {},
body: {name: 'John', email: '[email protected]'}
rawBody: '{\"name\":\"John\",\"email\":\"[email protected]\"}',
type: 'fetch',
params: { clientId: '' }
})
waitForRequestCount(n: number): Promise<void>
Waits until mock is matched my n
requests. Throws error when timeout (equal to 100ms) is exceeded.