Skip to content

HTTP request mocking library for Puppeteer and Playwright

License

Notifications You must be signed in to change notification settings

HLTech/mockiavelli

Repository files navigation

Mockiavelli

Request mocking for Puppeteer and Playwright

npm Node.js CI

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

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> method

    mockiavelli.mockGET({
        url: '/api/users',
        query: { age: '30' }
    }, {status: 200, body: [....]})
  • providing full matcher object mockiavelli.mock method

    mockiavelli.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 Page
  • options (object) configuration options
    • baseUrl: string specify the API base url, which will be added to every mocked request url
    • debug: 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 method
    • 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 object
    • prority (number) when intercepted request matches multiple mock, mockiavelli will use the one with highest priority
    • once (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 object
    • prority?: 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 from querystring.parse. Example: {name: 'foo'}
  • body: any - JSON deserialized request's post body, if any
  • type: string - request's resource type. Possible values are xhr and fetch
  • params: object - object with path parameters specified in url
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.