From 7487485559299e8561d400060cd22dc41fdbb74a Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Wed, 21 Oct 2015 23:18:49 +0100 Subject: [PATCH 1/2] added shorthands for the simplest use cases --- .travis.yml | 2 - README.md | 216 +++++++++++++++++++++++++--------------------- src/fetch-mock.js | 41 ++++++++- test/spec.js | 33 +++++++ 4 files changed, 188 insertions(+), 104 deletions(-) diff --git a/.travis.yml b/.travis.yml index af191574..b36a4a10 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,8 +14,6 @@ before_deploy: deploy: provider: npm email: wheresrhys@gmail.com - api_key: - secure: VzENYjzeS/EG6FKLGzapkYij22gCI8x8FPypp2u/9i/e+MZVI0V9AXWGuTu+M8ZObt3j5/VW9jKbc2ns8+Zc/8qZKUQUsVWHwB951IfQD/vZ6w2RHe+fEs8UAAsKhRW+69cy4MLzS7jc6oX+ZkdRUfpkrmxl16tILGFN4AeesgA= on: all_branches: true tags: true diff --git a/README.md b/README.md index 4f02fe0d..02a5ab0c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,24 @@ # 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) +## What does it do? +Provides a versatile range of http mocking utilities, provided all your http calls are done using fetch (or isomorphic-fetch in nodejs). 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); + 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) - fetch-mock doesn't declare `fetch` or `Promise` as dependencies; as you're testing `fetch` it's assumed you're already taking care of these globals @@ -9,7 +27,106 @@ 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 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(); +}); + +``` + + +## More complex examples ```javascript var fetchMock = require('fetch-mock'); @@ -137,100 +254,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 e8faf1cb..02169c7b 100644 --- a/src/fetch-mock.js +++ b/src/fetch-mock.js @@ -232,7 +232,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) { @@ -280,9 +313,9 @@ class FetchMock { debug('fetch restored'); } - reMock (config) { + reMock () { this.restore(); - this.mock(config); + this.mock.apply(this, [].slice.apply(arguments)); } reset () { @@ -291,7 +324,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 92f97601..d1fc016c 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(); @@ -310,6 +342,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(); From 9a04b518ebd3870ef2d74b8127ce31c681c2cc3b Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Wed, 21 Oct 2015 23:24:37 +0100 Subject: [PATCH 2/2] more docs --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 02a5ab0c..73a452a8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@ # 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) - -## What does it do? -Provides a versatile range of http mocking utilities, provided all your http calls are done using fetch (or isomorphic-fetch in nodejs). The simplest case is: +A versatile mocking library for http requests made using fetch (or isomorphic-fetch). The simplest case is: ``` const fetchMock = require('fetch-mock'); @@ -12,6 +9,7 @@ it('should pretend to be Rambo', done => { .then(res => { expect(fetchMock.calls().length).to.equal(1); expect(res.status).to.equal(301); + fetchMock.restore(); done(); }); }) @@ -34,7 +32,14 @@ Requests can be mocked based on a wide range of criteria (method, headers, url), `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. +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