diff --git a/README.md b/README.md index 4f02fe0d..73a452a8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,21 @@ # fetch-mock [![Build Status](https://travis-ci.org/wheresrhys/fetch-mock.svg?branch=master)](https://travis-ci.org/wheresrhys/fetch-mock) [![Coverage Status](https://coveralls.io/repos/wheresrhys/fetch-mock/badge.svg)](https://coveralls.io/r/wheresrhys/fetch-mock) -Mock http requests made using fetch (or isomorphic-fetch) +A versatile mocking library for http requests made using fetch (or isomorphic-fetch). The simplest case is: + +``` +const fetchMock = require('fetch-mock'); +it('should pretend to be Rambo', done => { + fetchMock.mock('http://rambo.was.ere', 301); + fetch('http://rambo.was.ere') + .then(res => { + expect(fetchMock.calls().length).to.equal(1); + expect(res.status).to.equal(301); + fetchMock.restore(); + done(); + }); +}) +``` + +Requests can be mocked based on a wide range of criteria (method, headers, url), and return any response needed. Routes can also be persisted over a series of tests and identified by name, with some useful shorthand syntaxes to make common use cases easier to write. See the docs below. *notes* - When using isomorphic-fetch or node-fetch ideally `fetch` should be added as a global. If not possible to do so you can still use fetch-mock in combination with [mockery](https://github.com/mfncooper/mockery) in nodejs (see `useNonGlobalFetch(func)` below) @@ -9,7 +25,113 @@ Mock http requests made using fetch (or isomorphic-fetch) - use browserify + debowerify and both `npm install fetch-mock` and `bower install fetch-mock`, then use `require('fetch-mock)` - use browserify and `npm install fetch-mock` and `require('fetch-mock/client)` -## Example + + +## API + +`require('fetch-mock')` exports a singleton with the following methods + +### `mock(config)` +Replaces `fetch()` with a stub which records it's calls, grouped by route, and optionally returns a stub response or passes the call through to `fetch()`. `config` is an optional* object with the following properties. + +#### *Shorthand notation for simplest use cases* +The following are also accepted by mock() +* `mock(name, matcher, response)` - configuration for a single named route to be mocked +* `mock(matcher, response)` - configuration for a single unnamed route to be mocked. To access details of its calls `fetchMock.calls()` should be called without passing a parameter +* `mock(response)` - configuration object for a single route +* `mock(responses` - array of route configuration objects + +* `routes`: Either a single object or an array of similar objects each defining how the mock handles a given request. If multiple routes are specified the first matching route will be used to define the response. Each route object must have the following properties. + * `name`: A unique string naming the route + * `matcher`: The rule for matching calls to `fetch()`. Accepts any of the following + * `string`: Either an exact url to match e.g. 'http://www.site.com/page.html' or, if the string begins with a `^`, the string following the `^` must begin the url e.g. '^http://www.site.com' would match 'http://www.site.com' or 'http://www.site.com/page.html' + * `RegExp`: A regular expression to test the url against + * `Function(url, opts)`: A function that is passed the url and opts `fetch()` is called with and that returns a Boolean + * `response`: Configures the response object returned by the mock. Can take any of the following values + * `number`: creates a response with the number as the response status + * `string`: creates a 200 response with the string as the response body + * `object`: If the object contains any of the properties body, status, headers, throws; then these properties - all of them optional - are used to construct a response as follows + * `body`: Returned in the response body + * `status`: Returned in the response status + * `headers`: Returned in the response headers. They should be defined as an object literal (property names case-insensitive) which will be converted to a `Headers` instance + * `throws`: If this property is present then a `Promise` rejected with the value of `throws` is returned + + As long as the object does not contain any of the above properties it is converted into a json string and this is returned as the body of a 200 response + * `Function(url, opts)`: A function that is passed the url and opts `fetch()` is called with and that returns any of the responses listed above +* `responses`: When `registerRoute()` has already been used to register some routes then `responses` can be used to override the default response. Its value should be an object mapping route names to responses, which should be similar to those listed immediately above e.g. + +```javascript + responses: { + session: function (url, opts) { + if (opts.headers.authorized) { + return {user: 'dummy-authorized-user'}; + } else { + return {user: 'dummy-unauthorized-user'}; + } + } + } +``` + +* `greed`: Determines how the mock handles unmatched requests + * 'none': all unmatched calls get passed through to `fetch()` + * 'bad': all unmatched calls result in a rejected promise + * 'good': all unmatched calls result in a resolved promise with a 200 status + + +\* `config` is optional only when preconfigured routes have already been setup + + +### `restore()` +Restores `fetch()` to its unstubbed state and clears all data recorded for its calls + +### `reset()` +Clears all data recorded for `fetch()`'s calls + +### `calls(routeName)` +Returns an array of arrays of the arguments passed to `fetch()` that matched the given route. '__unmatched' can be passed in to return results for calls not matching any route. + +### `called(routeName)` +Returns a Boolean denoting whether any calls matched the given route. '__unmatched' can be passed in to return results for calls not matching any route. If no routeName is passed it returns `true` if any fetch calls were made + +### `reMock()` +Normally calling `mock()` twice without restoring inbetween will throw an error. `reMock()` calls `restore()` internally before calling `mock()` again. This allows you to put a generic call to `mock()` in a `beforeEach()` while retaining the flexibility to vary the responses for some tests + +### `registerRoute(name, matcher, response)` +Often your application/module will need a mocked response for some http requests in order to initialise properly, even if the content of those calls are not the subject of a given test e.g. a mock response from an authentication service and a multi-variant testing service might be necessary in order to test the UI for a version of a log in form. It's helpful to be able to define some default responses for these services which will exist throughout all or a large subset of your tests. `registerRoute()` aims to fulfil this need. All these predefined routes can be overridden when `mock(config)` is called. + +`registerRoute()` takes either of the following parameters +* `object`: An object similar to the route objects accepted by `mock()` +* `array`: An array of the above objects +* `name`, `matcher`, `response`: The 3 properties of the route object spread across 3 parameters + +### `unregisterRoute(name)` +Unregisters one or more previously registered routes. Accepts either a string or an array of strings + +### `useNonGlobalFetch(func)` +To use fetch-mock with with [mockery](https://github.com/mfncooper/mockery) you will need to use this function to prevent fetch-mock trying to mock the function globally. +* `func` Optional reference to `fetch` (or any other function you may want to substitute for `fetch` in your tests). This will be converted to a `sinon.stub` and can be accessed via `fetchMock.fetch` + +#### Mockery example +```javascript +var fetch = require('node-fetch'); +var fetchMock = require('fetch-mock'); +var mockery = require('mockery'); +fetchMock.useNonGlobalFetch(fetch); + +fetchMock.registerRoute([ + ... +]) +it('should make a request', function (done) { + mockery.registerMock('fetch', fetchMock.mock()); + // test code goes in here + mockery.deregisterMock('fetch'); + done(); +}); + +``` + + +## More complex examples ```javascript var fetchMock = require('fetch-mock'); @@ -137,100 +259,3 @@ describe('content', function () { }); ``` - - -## API - -`require('fetch-mock')` exports a singleton with the following methods - -### `mock(config)` -Replaces `fetch()` with a sinon stub which, in addition to the default sinon behaviour, records it's calls, grouped by route, and optionally returns a stub response or passes the call through to `fetch()`. `config` is an optional* object with the following properties. - -* `routes`: Either a single object or an array of similar objects each defining how the mock handles a given request. If multiple routes are specified the first matching route will be used to define the response. Each route object must have the following properties. - * `name`: A unique string naming the route - * `matcher`: The rule for matching calls to `fetch()`. Accepts any of the following - * `string`: Either an exact url to match e.g. 'http://www.site.com/page.html' or, if the string begins with a `^`, the string following the `^` must begin the url e.g. '^http://www.site.com' would match 'http://www.site.com' or 'http://www.site.com/page.html' - * `RegExp`: A regular expression to test the url against - * `Function(url, opts)`: A function that is passed the url and opts `fetch()` is called with and that returns a Boolean - * `response`: Configures the response object returned by the mock. Can take any of the following values - * `number`: creates a response with the number as the response status - * `string`: creates a 200 response with the string as the response body - * `object`: If the object contains any of the properties body, status, headers, throws; then these properties - all of them optional - are used to construct a response as follows - * `body`: Returned in the response body - * `status`: Returned in the response status - * `headers`: Returned in the response headers. They should be defined as an object literal (property names case-insensitive) which will be converted to a `Headers` instance - * `throws`: If this property is present then a `Promise` rejected with the value of `throws` is returned - - As long as the object does not contain any of the above properties it is converted into a json string and this is returned as the body of a 200 response - * `Function(url, opts)`: A function that is passed the url and opts `fetch()` is called with and that returns any of the responses listed above -* `responses`: When `registerRoute()` has already been used to register some routes then `responses` can be used to override the default response. Its value should be an object mapping route names to responses, which should be similar to those listed immediately above e.g. - -```javascript - responses: { - session: function (url, opts) { - if (opts.headers.authorized) { - return {user: 'dummy-authorized-user'}; - } else { - return {user: 'dummy-unauthorized-user'}; - } - } - } -``` - -* `greed`: Determines how the mock handles unmatched requests - * 'none': all unmatched calls get passed through to `fetch()` - * 'bad': all unmatched calls result in a rejected promise - * 'good': all unmatched calls result in a resolved promise with a 200 status - - -\* `config` is optional only when preconfigured routes have already been setup - - -### `restore()` -Restores `fetch()` to its unstubbed state and clears all data recorded for its calls - -### `reset()` -Clears all data recorded for `fetch()`'s calls - -### `calls(routeName)` -Returns an array of arrays of the arguments passed to `fetch()` that matched the given route. '__unmatched' can be passed in to return results for calls not matching any route. - -### `called(routeName)` -Returns a Boolean denoting whether any calls matched the given route. '__unmatched' can be passed in to return results for calls not matching any route. If no routeName is passed it returns `true` if any fetch calls were made - -### `reMock()` -Normally calling `mock()` twice without restoring inbetween will throw an error. `reMock()` calls `restore()` internally before calling `mock()` again. This allows you to put a generic call to `mock()` in a `beforeEach()` while retaining the flexibility to vary the responses for some tests - -### `registerRoute(name, matcher, response)` -Often your application/module will need a mocked response for some http requests in order to initialise properly, even if the content of those calls are not the subject of a given test e.g. a mock response from an authentication service and a multi-variant testing service might be necessary in order to test the UI for a version of a log in form. It's helpful to be able to define some default responses for these services which will exist throughout all or a large subset of your tests. `registerRoute()` aims to fulfil this need. All these predefined routes can be overridden when `mock(config)` is called. - -`registerRoute()` takes either of the following parameters -* `object`: An object similar to the route objects accepted by `mock()` -* `array`: An array of the above objects -* `name`, `matcher`, `response`: The 3 properties of the route object spread across 3 parameters - -### `unregisterRoute(name)` -Unregisters one or more previously registered routes. Accepts either a string or an array of strings - -### `useNonGlobalFetch(func)` -To use fetch-mock with with [mockery](https://github.com/mfncooper/mockery) you will need to use this function to prevent fetch-mock trying to mock the function globally. -* `func` Optional reference to `fetch` (or any other function you may want to substitute for `fetch` in your tests). This will be converted to a `sinon.stub` and can be accessed via `fetchMock.fetch` - -#### Mockery example -```javascript -var fetch = require('node-fetch'); -var fetchMock = require('fetch-mock'); -var mockery = require('mockery'); -fetchMock.useNonGlobalFetch(fetch); - -fetchMock.registerRoute([ - ... -]) -it('should make a request', function (done) { - mockery.registerMock('fetch', fetchMock.mock()); - // test code goes in here - mockery.deregisterMock('fetch'); - done(); -}); - -``` diff --git a/src/fetch-mock.js b/src/fetch-mock.js index ed8b6e9a..a5698ad5 100644 --- a/src/fetch-mock.js +++ b/src/fetch-mock.js @@ -244,7 +244,40 @@ class FetchMock { this._calls[name].push(call); } - mock (config) { + mock (name, matcher, response) { + + let config; + if (response) { + + config = { + routes: [{ + name, + matcher, + response + }] + } + + } else if (matcher) { + config = { + routes: [{ + name: '_mock', + matcher: name, + response: matcher + }] + } + + } else if (name instanceof Array) { + config = { + routes: name + } + } else if (name && name.matcher) { + config = { + routes: [name] + } + } else { + config = name; + } + debug('mocking fetch'); if (this.isMocking) { @@ -292,9 +325,9 @@ class FetchMock { debug('fetch restored'); } - reMock (config) { + reMock () { this.restore(); - this.mock(config); + this.mock.apply(this, [].slice.apply(arguments)); } reset () { @@ -303,7 +336,7 @@ class FetchMock { } calls (name) { - return this._calls[name] || []; + return name ? (this._calls[name] || []) : (this._calls._mock || this._calls); } called (name) { diff --git a/test/spec.js b/test/spec.js index 9c6d2240..271a6a9b 100644 --- a/test/spec.js +++ b/test/spec.js @@ -118,6 +118,38 @@ module.exports = function (fetchMock, theGlobal) { }); }); + describe('shorthand notation', function () { + it('accepts name, matcher, route triples', function () { + expect(function () { + fetchMock.mock('route', 'http://it.at.there', 'ok'); + }).not.to.throw(); + fetch('http://it.at.there'); + expect(fetchMock.calls('route').length).to.equal(1); + }); + + it('accepts matcher, route pairs', function () { + expect(function () { + fetchMock.mock('http://it.at.there', 'ok'); + }).not.to.throw(); + fetch('http://it.at.there'); + expect(fetchMock.calls().length).to.equal(1); + }); + + it('accepts array of routes', function () { + expect(function () { + fetchMock.mock([ + {name: 'route1', matcher: 'http://it.at.there', response: 'ok'}, + {name: 'route2', matcher: 'http://it.at.where', response: 'ok'} + ]); + }).not.to.throw(); + fetch('http://it.at.there'); + fetch('http://it.at.where'); + expect(fetchMock.calls('route1').length).to.equal(1); + expect(fetchMock.calls('route2').length).to.equal(1); + }); + + }); + describe('unmatched routes', function () { it('record history of unmatched routes', function (done) { fetchMock.mock(); @@ -336,6 +368,7 @@ module.exports = function (fetchMock, theGlobal) { .then(function (res) { expect(fetchMock.called()).to.be.true; expect(fetchMock.called('route')).to.be.true; + expect(fetchMock.calls().route).to.exist; expect(fetchMock.calls('route')[0]).to.eql(['http://it.at.there', undefined]); expect(fetchMock.calls('route')[1]).to.eql(['http://it.at.thereabouts', {headers: {head: 'val'}}]); done();