Giter VIP home page Giter VIP logo

mockiavelli's Introduction

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.

mockiavelli's People

Contributors

fernard avatar fiszcz avatar lukaszfiszer avatar pawfa avatar semantic-release-bot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mockiavelli's Issues

TypeScript: set type for body of reponse

We could add the possibility to set type for body content of response.

For example for this example mock:

const getUsersMock = mockiavelli.mockGET('/api/users', {
    status: 200,
    body: [
        { id: 123, name: 'John Doe' },
        { id: 456, name: 'Mary Jane' },
    ],
});

programmer would set type of body content:

const getUsersMock = mockiavelli.mockGET<User[]>('/api/users', {
    status: 200,
    body: [
        { id: 123, name: 'John Doe' },
        { id: 456, name: 'Mary Jane' },
    ],
});

It will help find some changes in interfaces and fast fix changes in mocked responses.

Of course generic parameter will be optional.

How do I stop mocking in one session?

Hi!
If i need to mock a request and then use a real one, then if there is any way to do this, other than once?
My workaround for Puppeteer: page.setRequestInterception(false)

Problem with waitForRequest() timeout(?)

Hi guys,
First: I'd like to thanks for your work on this mocking tool for pptr.
Second: I have a question regarding waitForRequest: we use it after clicking save button to add some data. After the click, page is sending request to fetch new list of data on the other screen after navigating. We often get No request matching mock [(5) GET...
Perhaps it's something to do with 100 ms timeout. Is it possible to increase this value from default?

Allow to specify API base url

To avoid adding the same prefix to URLs when defining mocks:

mockiavelli.mockGET('/mysuperapi/users`, {...})
mockiavelli.mockGET('/mysuperapi/orders`, {...})

it would be useful to be able to define it when instantiating mockiavelli

const mockiavelli = await Mockiavelli.setup(page, {baseUrl: '/mysuperapi'});
mockiavelli.mockGET('/users`, {...})
mockiavelli.mockGET('/orders`, {...})

This should also work for CORS requests:

const mockiavelli = await Mockiavelli.setup(page, {baseUrl: 'http://example.com/api'});
await page.goTo('http://someapp.com')
mockiavelli.mockGET('/users`, {...}) // mocks requests to http://example.com/api/users

Is it possible to mock Apollo GQL requests?

It looks like Mockiavelli is not catching these at all - I get no errors or warnings about unhandled API calls, and when I do try to handle them they are hitting the endpoint and failing as I've mocked out the Auth part of my app. Is there some additional config I need to make this work, or is it simply not possible?

Documentation for wildcards in url

Hello, just thought having documentation around this #31 would be nice. I was trying to figure out how to add a wildcard to the mock and found this PR in your repository. Thought it would be good for users to be able to know that you support this.

Compatibility with puppeteer v10

Hi,

Puppeteer 10 turns out to be incompatible with types defined in Mockiavelli (PuppeteerController.ts file). When both are used in project, following TypeScript error is shown:

TS2345: Argument of type 'Page' is not assignable to parameter of type 'BrowserPage'. 
Type 'Page' is not assignable to type 'PuppeteerPage'. 
Types of property 'on' are incompatible. 
Type '<K extends keyof PageEventObject>(eventName: K, handler: (event: PageEventObject[K]) => void) => EventEmitter' is not assignable to type '(eventName: "request", handler: (e: PuppeteerRequest) => void) => any'. 
Types of parameters 'handler' and 'handler' are incompatible. 
Types of parameters 'e' and 'event' are incompatible. 
Type 'HTTPRequest' is not assignable to type 'PuppeteerRequest'. 
The types returned by 'method()' are incompatible between these types. 
Type 'string' is not assignable to type '"GET" | "POST" | "PATCH" | "PUT" | "DELETE" | "OPTIONS"'.

Probably method() function return type should be widened to string to match type defs in Puppeteer

Cross-origin requests blocked in Puppeteer 8 + Chrome

Sample log (with debug:true and dumpio: true. The request to /auth/oauth/token is not replied correctly

[2021-04-16T10:15:22.133Z] 2021-04-16T10:15:21.098Z mockiavelli:main > req: type=other method=OPTIONS url=https://example.com/auth/oauth/token 
[2021-04-16T10:15:22.133Z] [0416/101521.106580:INFO:CONSOLE(0)] "Access to XMLHttpRequest at 'https://example.com/auth/oauth/token' from origin 'http://localhost:9000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.", source: http://localhost:9000/ (0)

Invalid resolved paths to dependencies in package-lock.json file

In package-lock.json we have a lot of paths to hl tech nexus, for example:

        "jsonfile": {
            "version": "6.0.1",
            "resolved": "https://nexus.tech.hl.uk/repository/npm/jsonfile/-/jsonfile-6.0.1.tgz",
            "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==",
            "dev": true,
            "requires": {
                "graceful-fs": "^4.1.6",
                "universalify": "^1.0.0"
            }
        },

"resolved" field should start by https://registry.npmjs.org/ url:

            "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz",

We have 295 appearances of nexus.tech.hl.uk, which we should replace.

Trailing slashed handling

The current mock mechanism is "strict" regarding trailing slashes:

mockiavelli.mockGET('/example', {status: 200})
// GET /example/ => 404

This behaviour should be at least configurable to allow ignoring trailing slashes:

const mockiavelli = await Mockiavelli.setup(page, {ignoreTrailingSlashes: true});
mockiavelli.mockGET('/example', {status: 200})
// GET /example/ => 200

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.