Manual Reference Source Test

Build Status Coverage Status semantic-release Documentation Greenkeeper badge PRs Welcome

Fetch-mock-fixtures (FMF)

While most of mockers for fetch are only meant to intercept and define the next response content, FMF goes far beyond and offers a wide range of tools for testing js apps.

What is featured :

  • BDD style syntax to configure server and fixtures in a more readable-friendly way. It's heavily inspired by Chai should assertion library,
  • Easy way to configure response on-the-fly, mutating responses from call to call...
  • Enhanced Request native object to automatically parse url details and query content with url-parse,
  • Powerful response presets and fixtures system to avoid writing the same things again and again and ease functional testing,
  • Easy access to the full history or requests/responses handled by the server since its first start or last reset,
  • Parametrized request thanks to path-to-regexp to enable dynamic fixtures routing in a few lines of code
  • and many more !

For instance, with FMF, you can do such things to quickly configure two fixtures in a mocha test (that will obviously succeed) :

import Server from 'fetch-mock-fixtures';

const server = new Server();

describe('Headers test', function() {
  before(() => server.start()) // Start intercepting fetch calls
  after(() => server.stop()) // Restore to normal behavior

  it('should use json headers', async function() {
    server
      .when // or .on
        .header('content-type').equal(/json/) // use a regexp here to avoid writing full header
        .respond.with.preset('200')
      .fallback.to.preset('400')

    let response = await fetch('/', {
      headers: {'content-type': 'application/json'}
    });

    response.status.should.equal(200);
  })
})

How FMF can ease API outgoing requests unit tests ?

FMF enables really quick response configuration that allows testing the outgoing request to set up different responses (see above example). You only have to check a response property (like status) instead of manually parsing request built by your app to validate it.

Furthermore, you can use the before and after hooks or body as a callback to alter response on very precise expectations.

How FMF can ease functional tests ?

In real life, scripts are often sending multiple requests to do their job. FMF removes the pain of handling multiple responses by easing their management. Let's see this example with a two steps authentication login. A bit verbose for what is actually doing but it aims to illustrate things :

import Server from 'fetch-mock-fixtures';

const server = new Server();

// Define on-the-fly fixtures to handle login tests
server
  .verbose(true) // Enable console login for each request/response/error
  .when
      .pathname.equal('/login')
      .method.equal('POST')
      .body('json').equal(body => body.username === 'foo')
    .respond.with
      .preset(401)
  .when
      .pathname.equal('/session')
      .method.equal('POST')
      .body('json').equal(token => body.authToken === '123')
    .respond.with
      .preset('json')
      .body({success: true, sessionToken: '456'})
  .fallback.to
    .preset(403)

describe('Login test suite', function() {
  before(() => server.start())
  after(() => server.stop())

  it('should login', async function() {
    await triggerTheLoginLogic('foo');
    await sendTheTokenLogic('123');
    logged.should.be.true;
  })

  it('should fail login on username', async function() {
    await triggerTheLoginLogic('bar');
    logged.should.be.false;
  })

  it('should fail login on token', async function() {
    await triggerTheLoginLogic('foo');
    await sendTheTokenLogic('hacked!');
    logged.should.be.false;
  })
})

We're not only sending back data to the app but also checking outgoing requests at the same time because the answer will only be sent if calling the right url with the right method and the right data. with and to are only optional sugars to improve human readability.

Last not least, you can easily deploy url-based routing to use your "real" data inside each tests instead of providing fake data and get rid of on-the-fly fixtures (see dynamic fixtures examples).

When to use FMF ?

At any time :smile:

Nevertheless, FMF will truly give its best with any testing framework (Mocha, Jasmine, Junit...) that allows to automate operations between each tests like start, stop or reset the server.

Installation

Installation can easily be done through NPM or Yarn. Sinon is required by FMF to stub fetch but is not included in the bundle. It must be installed as well if not already present.

npm install sinon fetch-mock-fixtures --save-dev

yarn add sinon fetch-mock-fixtures --dev

FMF should be installed as a dev dependency. It is not meant to be used as an in-app offline mode feature.

Note : FMF is built upon Promise, Proxy and fetch API (Request, Headers, Response) that are available in all modern browsers. If you intend to run tests on older browsers (IE) or versions, you may need to polyfill them. Here's some available tools you can use :

Full documentation and API reference

Please pay visit to the docs pages.

Bugs and improvements

Any bugs and issues can be filed on the github repository.

You are free and very welcome to fork the project and submit any PR to fix or improve FMF.

I'm especially interested for good will who wish to improve query matcher processors to provide more tools to evaluate query and choose the right response.

Changelog

  • 2.2.0 : Add global preset configuration within server instance and throw behavior for fixture
  • 2.1.0 : Add history logging and verbose mode
  • 2.0.0 : BREAKING CHANGE - A brand new FMF highly not compatible with previous version
  • 1.0.1 : Add requests history and possibility to set up different responses based on requests order. Add delay as a response parameter into fixture.

Getting started

For installation tips, please consult documentation home.

Note on ES6 syntax : In all examples here, we're using ES6 modules import and, sometimes, the new spread operator. We're also using async/await syntax for asynchronous calls. All are supported natively by modern browsers.

FMF itself have been transpiled with Babel to support only alive browsers that share more than 5% of the market. It means that FMF is really not designed to test apps which are willing to support really old browsers as it will certainly be unsupported by them when testing. Nevertheless, you can still try to tweak the build to increase browsers coverage but with no guarantee.

At its most basic intend, FMF can be simply used as a convenient way to trap a remote fetch call and send a given response. For this to work, the server must have been started and provided with the response. At the end, the server must be stopped in order to resume on native fetch API.

Here's a simple example :

import Server from 'fetch-mock-fixtures';

const server = new Server();

server.start();

server.respond.with.body('Hello world !');

let response = await fetch(/** can have any arguments */);
let data = await response.text();

console.log(data); // will output Hello world !

// What about the next call ?
response = await fetch(/** can have any arguments */);
data = await response.text();

console.log(data); // will output Hello world again !

server.stop();

Nothing too fancy here : we're starting the server, setting it to respond with 'Hello world !', running fetch twice and stop the server.

Behind the scene, we have :

  • overridden window.fetch to intercept calls
  • Set up a single fixture to the server that matches all incoming requests
  • restored window.fetch to its native state

Please read other parts of the documentation for a more advanced usage.

Server control, configuration and history

Before being used, you must create a server instance :

import Server from 'fetch-mock-fixtures'

const server = new Server()

The constructor takes no arguments. Global presets are loaded when server instance is created. From server instance, you can start, stop and reset the server, configure presets and fixtures and access server history.

Server control

Method / Property Description
start() Start the server by mocking native window.fetch with a Sinon stub
stop(reset=false) Stop the server by restoring window.fetch. You can optionally pass true as argument to also reset the server
reset(resetStub=true) Reset the server (clear all fixtures and history) and, optionnally reset the stub history
running If true server is running
stub Direct access to the sinon stub

Adding fixtures to server

Adding fixtures to the server is pretty simple. See fixtures documentation.

Logging and verbose mode

Since v2.1.0

To help go through requests history analysis or to debug fixtures, FMF logs all events in history. You can access logs through server.history.logs.

Logs can also be displayed at runtime to console with the verbose mode of the server. Simply call the chainable verbose method on server instance.

import Server from 'fetch-mock-fixtures'

const server = new Server()

server.verbose(true) // enable verbose mode
server.verbose(false) // disable verbose mode

Error management

When encountering an error during configuration, the server will throw an error.

During request processing, the server will display a warning in console and send back a 500 response with error description. This behavior can be changed with :

  • warnOnError(true|false) : Activate/deactivate warnings in console
  • throwOnError(true|false) : If true, tells the server to throw an error instead of sending back a 500 error.

Server's history

The server keeps track of all incoming requests and responses. As convenience, you can access the last request and response by calling server.request or server.response. For more advanced selection tools, the history is available under server.history. See history tests for available tools.

The request is stored as a FMFRequest that also exposes parsed informations about the url using url-parse).

Response configuration

Fixtures and presets share the same response configurator. It allows to set up response content and/or adapt request processing behavior.

Configure response

From the fixture or the preset, you can either use the set method and/or use the BDD style :

const server = new Server()

server.respond // will register a new Fixture
  .set({
    status: 200
  })
  .headers({'content-type': 'application/json'})
  .body(body => JSON.stringify({message: 'Hellow world !'}));

// Server will now respond to all requests with a JSON response and status 200

The response properties name are the same between object set approach and BDD style. The last assignment (no matter the way you're doing it) will override the previous. You can remove an option by providing false as value :

const server = new Server()

server.respond // will register a single fixture
  .body(body => JSON.stringify({message: 'Hello world !'}));

// Remove body callback
server.respond // will fetch the fixture
  .body(false);

Default response values

There's no default value for a fixture/preset response. You can use set up a preset and use it to automatically populate one or more response options of a fixture.

Response configuration persistence

Fixtures are stored within server and persists until server is reset. If you share the same server between many tests, any changes to fixtures configuration will persist into next tests. This can sometimes be tricky if you're updating an option of the fallback fixture in one test. See fixtures for more informations. This can also be really useful as you can create fixtures only once and share them along all tests.

Available response options

Each response option can be removed by affecting the false value to it. It is evaluated as a strict comparison.

FMF doesn't provide any default values, but Response implementation usually set up a status 200 with 'text/html' encoding to a new Response object when no options is provided.

Option Allowed value(s) Description
body null | Blob | BufferSource | FormData | ReadableStream | URLSearchParams | USVString | Function You can use any of the available types for a native Response object. FMF also accepts a callback that will return the body content or alter the response (see fixture lifecycle)
delay Number The fixture response will be delayed by X ms
headers Object | Headers The object will be used to instantiate the Headers
status Number Status code of the response (2XX - 5XX). Some status code may have some requirements. For instance, trying to set up a body with a 204 status code will fail. It is not a FMF behavior but from native Response object.
statusText String Status text along the status code
wrapper Function Wrapper callback that transforms body. See wrappers
preset String Only available within a fixture. See presets
pattern String Pattern to apply to extract parameters from incoming request url. See patterns
before Function Callback called before the response is built. See fixture lifecycle
after Function Callback called after the response have been built. See fixture lifecycle

Using wrappers

Wrappers are used as body processors when preparing the response. Body stored from fixture is provided and the processed body must be returned. The main goal is to get rid of little transformations when providing the body to the fixture. Only one wrapper is allowed per response.

For instance, let's say we're working on a JSON API that expects the server's response to be always wrapped in the same patterns. You can use wrapper and two global presets to get rid of emulating this behavior each time you're creating a fixture :

import {presets, Server} from 'fetch-mock-fixtures';

// Add the presets to global presets object
// You can do it in your tests bootstrap
presets = Object.assign(presets, {
  'api-success': {
    headers: {'content-type': 'application/json'},
    wrapper: body => JSON.stringify({
      success: true,
      data: body
    })
  },
  'api-failure': {
    headers: {'content-type': 'application/json'},
    wrapper: body => JSON.stringify({
      success: false
      error: body
    })
  }
})

// In tests scripts
const server = new Server()

server.respond.with.preset('api-success').and.body({
  id: 1,
  name: 'foo'
});

// Parsed JSON response will be {success: true, data: {id: 1, name: 'foo'}}

Using patterns

Patterns are a way to automatically extract parameters from the url. They will be provided as an object and first argument to the body callback (see fixtures#body_callback).

To extract params, url is parsed with path-to-regexp. Please refer to this for advanced syntax.

Here's a simple example that use extract user id from url :

import {Server} from 'fetch-mock-fixtures';

users = [
  {id: 1, name: 'foo'},
  {id: 2, name: 'bar'},
  {id: 3, name: 'baz'},
]

server.start().respond
  .with.pattern('/api/users/:id?')
  .and.body({id} => return id ? users.find(user => user.id === id) : users)

Presets

Using presets

You can tell a fixture to use a preset to build its response by simply calling its preset method with the name of the preset :

const server = new Server();

server.respond.with.preset(400);

The preset is only evaluated and merged by the fixture at request time. You can easily override preset options through fixture response configuration.

If the preset is not found in the server instance, an exception will be raised at request time.

Local presets

Adding/editing a local preset

It can be done by providing an object with its set method and/or using BDD style syntax. Alternatively, the configuration object can be provided to server.preset :

const server = new Server();

// In the server.preset call
server.preset('myPreset', {
  status: 250
  statusText: 'weird status'
});

// Another syntax, the same result
server.preset('myPreset').set({status: 250}).statusText('weird')

Server will now have myPreset available in its own presets pool. If the presets was already defined, it will be overridden with new options.

Note : the options that are not overridden will be kept as is.

const server = new Server();

server.preset('myPreset', {status: 250, statusText: 'weird status'});

// Later on
server.preset('myPreset').status(200);
// statusText will still equal 'weird status'

You can have a look at the response configuration for more details about the available options.

Removing preset

Simply call the remove method of the preset :

  server.preset('myPreset').remove();

Using global presets

Aside local presets that can be defined within a server instance, FMF allows the registration of global presets. Global presets can be added/updated/removed in two ways :

Global presets singleton exports

Global presets are stored in a singleton available through the presets export. Object can be updated at will.

import {presets} from 'fetch-mock-fixtures';

presets.myPreset = {
  status: 250
  statusText: 'weird status'
} // myPreset will now be available globally

That way is interesting when bootstrapping presets configuration before running tests suite.

Register/unregister a local preset as global

Since FMF 2.2.0, it is possible to manage global presets from any server instance.

Any local preset can be made global through register method and any global preset can be removed with unregister method.

const server = new Server();
// Create a local preset
const myPreset = server.preset('myPreset', {status: 250, statusText: 'weird status'});
// And register it as global
myPreset.register();
// Remove the preset from global pool
// It will still be available in the server instance
myPreset.unregister();

Methods are chainable :

const server = new Server();

// Register the preset, unregister it and remove it from server presets pool
server.preset('myPreset').status(250).register().unregister().remove();

Global presets availability

To allow easy local tweaking of presets, global presets are cloned into local pool when creating a new server instance. Therefore, server's instance will only access global presets that are already defined before its creation or created within the server instance.

Built-in presets

FMF already have commonly used built-in presets. See presets.js for details.

Fixture in-depth

The fixture is the heart of FMF. Basically, a fixture organizes :

  • the response content (body, headers, status, statusText) and additional options (delay, wrapper, pattern, before and after hooks)
  • Optionally, the conditions that the request must match to allow use of the fixture

Request processing

When the server receives a request, it :

  1. tries to find a fixture with conditions that are matching explicitly the request. If multiple fixtures are found, it uses the first one. If none is found, it uses the fallback fixture if available or raise and error,
  2. passes the request to the fixture and await the response that triggers the fixture lifecycle,
  3. send back the response or an error if a problem occurred (see server error management).

Fixture lifecycle

When provided with the request, the fixture will :

  1. Process the before hook is one have been set,
  2. Extract parameters from the url if a pattern have been set,
  3. Process the body callback or get the body value,
  4. Finalize the response setup from preset (if one have been used) and its own response configuration,
  5. Apply wrapper to body if one is set
  6. Construct a Response instance from response configuration
  7. Process the after hook if one have been set,
  8. Delay the response if asked to,
  9. Return the response to the server instance.

You can use any of hooks and body callback to amend response content or even cut the lifecycle by throwing another response or error.

Fixture hooks and body callback

In each hooks and callbacks, you can throw to stop fixture processing. If you throw :

  • A Response instance, it will be send back to the client immediately
  • A Preset instance, it will be used to send back a response (useful for an HTTP error for instance)
  • An error, it will used accordingly to server error management configuration

before hook

It occurs at the very start of the fixture request processing. It receives the server instance, the request and the actual response configuration of the fixture as arguments.

You can return an updated response object that will be used for the rest of the lifecycle.

import {Server} from 'fetch-mock-fixtures'

const server = new Server();

server.respond.before((server, request, response) => {
  // Detect multiple identical requests
  if(request.url === server.request.url) throw new Error('Duplicate requests');
})

Body callback

The body callback is provided with two arguments :

  • The params parsed from the request url as a key/value object
  • An object exposing request, response and server as properties

This is the best place for building dynamic fixtures as parameterss are directly provided to the callback.

after hook

The after hook is ran at the very end and is provided with the server instance and Response instance as arguments.

It is most likely the place to do some cleanings or data resets between calls.

Hooks scope

The hooks are called within the scope of the fixture and this will refer to the fixture instance only if using regular function declaration. The scope of an arrow function is where the function have been declared, usually your test suite. For instance, to extract parameters from the before hook :

import {Server} from 'fetch-mock-fixtures'

const server = new Server();

// Won't work - Scope problem
server.respond.before((server, request, response) => {
  const params = this.extractParams(request.pathname, response.pattern);
})

// This will work
server.respond.before(function(server, request, response) {
  const params = this.extractParams(request.pathname, response.pattern);
})

Adding fixtures to server

As soon as you have some sample data you are using in many tests, it may be appropriate to stop adding fixtures on-the-fly to the server instance.

For simple datasets, a bunch of presets may be handful but if you're going on using calls count (see requests matcher), you can simply declare all your fixtures in one file that you can import/require into your test script. Then, you can simply add fixtures to the server by importing them :

import {Server} from 'fetch-mock-fixtures'
import fixtures from '../myfixtures';

const server = new Server();
server.import(fixtures);

The fixtures can be response configuration object or fixtures instance.

Requests matcher

Each fixture has a built-in requests matcher. You can configure a fixture to match only some requests properties and/or match only calls count.

Matching request properties

FMF is extending the native Request object with properties build from the parsing of the url with url-parse). Therefore, you can directly access pathname, port, basic authentication...

Such matching evaluation is built in processors. By this time, there's only a common swiss army knife called equal. To add a matching configuration, you can do it with a one-shot call or with BDD style after using on or when getter.

Have a look to the tests for examples on usage.

The next call to respond will set the fixture in response configuration mode.

You can then go on next conditional fixture by calling on or when again.

If you directly call respond or fallback, you will go on the fallback fixture and create or overwrite it.

The fallback fixture is the one (you can have only one obviously) that have no matching conditions. It will only be used if none of the others fixtures is matching the request.

Matching calls count

You can also configure a fixture to match only the nth call to the server or the nth call to itself.

You will find some examples in tests.

Full cheat sheet

Creating a server instance

import {Server} from 'fetch-mock-fixtures';

const server = new Server();

Response setup (preset or fixture)

server.preset(string <preset name>) | server.respond | server.fallback
  .with // Syntactic sugar for human readability
  .and // Syntactic sugar for human readability  
  .after(Function <after>) // After callback
  .before(Function <before>) // Before callback
  .body(String|Function <response body>) // Preset body content
  .delay(Number <delay>) // Preset delay response
  .header(String <name>, String <value> [, Boolean <append=false>]) // Configure single header
  .headers(Object|Headers <headers>) // Configure bunch of headers at once
  .pattern(String <pattern>) // Pattern string for parameters analysis
  .set(Object <set-up object>) // Set up any options at once through an object
  .status(Number <status>) // Response status code
  .statusText(String <statusText>) // Response status text
  .wrapper(Function <wrapper>) // Wrapper function applied to body

Ordered responses (fixture)

server.respond
  .to // Syntactic sugar for human readability
  .firstCall([Boolean <own = false>]) // Target local/global first call
  .secondCall([Boolean <own = false>]) // Target local/global second call
  .thirdCall([Boolean <own = false>]) // Target local/global third call
  .call(Number <n>, [Boolean <own = false>]) // target nth local/global call

Request matcher setup (fixture)

For request parts details available for comparison, see url-parse page.

server.on | server.when
  .and // Syntactic sugar for human readability
  .is // Syntactic sugar for human readability
  .respond // End request matcher setup and switch to response configuration
  .not // Negate result of the next matching evaluation
   // Target the body and, optionnaly, define a type for the body processor
  .body([String <type=text>])
  .header(String <name>) // Target named header for matching
   // Tells processor to evaluate equality based on expected value
  .equal(Array|Boolean|Function|Number|Object|RegExp|String expected)
  .equals(Array|Boolean|Function|Number|Object|RegExp|String expected)
  .headers
  .query
  .slashes
  .auth
  .cache
  .credentials
  .destination
  .hash
  .href
  .host
  .hostname
  .integrity
  .mode
  .method
  .password
  .pathname
  .port
  .protocol
  .redirect
  .referrer
  .referrerPolicy
  .url
  .username

Starting configuration

  • server.preset() will start a preset configuration
  • server.on or server.when will start a request matcher configuration
  • server.respond or server.fallback will start a response configuration

All configuration methods are chainable, though one cannot configure a preset and a fixture in the same chain :

import {Server} from 'fetch-mock-fixtures';

const server = new Server();

// Configure the success preset and register it globally
server  
  .preset('success')
  .header('content-type', 'application/json')
  .wrapper(data => ({success: true, data}))
  .register()

server
  // Configuration of fixture with matching request
  .on
    .pathname.equal('/api/v1/users')
    .and.method.equal('POST')
  .respond
    .with.preset('success')
    .and.body({token: '123'})
  // Configuration of fallback fixture
  .fallback
    .throw('failed')