diff --git a/.travis.yml b/.travis.yml index 1b36f00d..af191574 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ sudo: false language: node_js script: npm test node_js: -- '0.12' +- '4' before_install: - export CHROME_BIN=chromium-browser - export DISPLAY=:99.0 @@ -19,4 +19,4 @@ deploy: on: all_branches: true tags: true - repo: wheresrhys/fetch-mock \ No newline at end of file + repo: wheresrhys/fetch-mock diff --git a/Makefile b/Makefile index 56e4b0d3..e556a54f 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,3 @@ test: mocha test/server.js ./node_modules/karma/bin/karma start --single-run - -coverage: -# mocha test/server.js -R mocha-lcov-reporter | ./node_modules/coveralls/bin/coveralls.js diff --git a/README.md b/README.md index b0a4c079..4f02fe0d 100644 --- a/README.md +++ b/README.md @@ -1,117 +1,20 @@ # 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) -*notes* +*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 - fetch-mock uses [npm debug](https://www.npmjs.com/package/debug). To output useful messages for debugging set the environment variable `DEBUG=fetch-mock` -- If you prefer documentation by example skip to the bottom of this README -- To use fetch-mock on the server simply `npm install fetch-mock` and `require('fetch-mock)`. In the browser either +- To use fetch-mock on the server simply `npm install fetch-mock` and `require('fetch-mock)`. In the browser either - 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)` -## 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(); -}); - -``` - -## Example +## Example ```javascript var fetchMock = require('fetch-mock'); -// Set up some routes you will always want to mock +// Optionally set up some routes you will always want to mock // Accepts an array of config objects or three parameters, // name, matcher and response, to add a single route fetchMock.registerRoute([ @@ -149,12 +52,12 @@ it('should do A', function () { // none: all unmatched calls get sent straight through to the default fetch // bad: all unmatched calls result in a rejected promise // good: all unmatched calls result in a resolved promise with a 200 status - greed: 'none' + greed: 'none' }); - + thingToTest.exec(); - // returns an array of calls to the session service, + // returns an array of calls to the session service, // each item in the array is an array of the arguments passed to fetch // similar to sinon.spy.args fetchMock.calls('session') // non empty array @@ -165,7 +68,7 @@ it('should do A', function () { fetchMock.calls('session') // undefined fetchMock.called('geo') // false - + // fetch itself is just an ordinary sinon.stub fetch.calledWith('thing') @@ -186,11 +89,11 @@ describe('content', function () { }) it('should do B', function () { - - + + fetchMock.mock({ // you can choose to mock a subset of the registered routes - // and even add one to be mocked for this test only + // and even add one to be mocked for this test only // - the route will exist until fetchMock.restore() is called routes: ['session', 'content', { name: 'enhanced-content', @@ -202,29 +105,29 @@ describe('content', function () { } }] }); - + thingToTest.exec(); fetchMock.calls('content') // non empty array fetchMock.called('enhanced-content') // Boolean - + // restores fetch and resets all data fetchMock.restore(); }) it('should do C', function () { - - + + fetchMock.mock({ // you can override the response for a service for this test only - // this means e.g. you can configure an authentication service to return + // this means e.g. you can configure an authentication service to return // a valid user normally, but only return invalid for the one test // where you're testing authentication responses: { 'session': 'invalid-user' } }); - + thingToTest.exec(); // restores fetch and resets all data @@ -233,3 +136,101 @@ 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/client.js b/client.js index 49c1bd38..502d5c8d 100644 --- a/client.js +++ b/client.js @@ -1,10 +1,11 @@ 'use strict'; -var FetchMock = require('./src/fetch-mock'); +const FetchMock = require('./src/fetch-mock'); module.exports = new FetchMock({ theGlobal: window, Response: window.Response, Headers: window.Headers, - Blob: window.Blob + Blob: window.Blob, + debug: function () {} }); diff --git a/karma.conf.js b/karma.conf.js index 23ff55b2..44e5ecc5 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -12,10 +12,10 @@ module.exports = function(karma) { 'test/client.js': ['browserify'] }, browserify: { - transform: ['debowerify'], + transform: ['babelify', 'debowerify'], debug: true }, - browsers: ['PhantomJS', 'Chrome'], + browsers: ['Chrome'], customLaunchers: { Chrome_travis_ci: { base: 'Chrome', @@ -25,7 +25,7 @@ module.exports = function(karma) { }; if(process.env.TRAVIS){ - configuration.browsers = ['PhantomJS', 'Chrome_travis_ci']; + configuration.browsers = ['Chrome_travis_ci']; } karma.set(configuration); diff --git a/package.json b/package.json index 64ade9a7..2815dccc 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Mock http requests made using fetch (or isomorphic-fetch)", "main": "server.js", "scripts": { - "test": "make test coverage" + "test": "make test" }, "repository": { "type": "git", @@ -25,10 +25,10 @@ "homepage": "https://github.com/wheresrhys/fetch-mock", "dependencies": { "node-fetch": "^1.2.0", - "sinon": "^1.14.1", "debug": "^2.2.0" }, "devDependencies": { + "babelify": "^6.3.0", "browserify": "^10.0.0", "chai": "^2.3.0", "coveralls": "^2.11.2", @@ -39,9 +39,9 @@ "karma-chai": "^0.1.0", "karma-chrome-launcher": "^0.1.8", "karma-mocha": "^0.1.10", - "karma-phantomjs-launcher": "^0.1.4", "mocha": "^2.2.4", "mocha-lcov-reporter": "0.0.2", - "npm-prepublish": "^1.2.0" + "npm-prepublish": "^1.2.0", + "sinon": "^1.17.0" } } diff --git a/server.js b/server.js index 2d58e738..a1726829 100644 --- a/server.js +++ b/server.js @@ -1,13 +1,14 @@ 'use strict'; -var Response = require('node-fetch').Response; -var Headers = require('node-fetch').Headers; -var stream = require('stream'); -var FetchMock = require('./src/fetch-mock'); +const Response = require('node-fetch').Response; +const Headers = require('node-fetch').Headers; +const stream = require('stream'); +const FetchMock = require('./src/fetch-mock'); module.exports = new FetchMock({ theGlobal: GLOBAL, Response: Response, Headers: Headers, - stream: stream + stream: stream, + debug: require('debug')('fetch-mock') }); diff --git a/src/fetch-mock.js b/src/fetch-mock.js index 5e8cded4..e8faf1cb 100644 --- a/src/fetch-mock.js +++ b/src/fetch-mock.js @@ -1,12 +1,11 @@ 'use strict'; -var sinon = require('sinon'); -var Headers; -var Response; -var stream; -var Blob; -var theGlobal; -var debug = require('debug')('fetch-mock') +let Headers; +let Response; +let stream; +let Blob; +let theGlobal; +let debug; function mockResponse (url, config) { debug('mocking response for ' + url); @@ -29,25 +28,31 @@ function mockResponse (url, config) { debug('mocking failed request for ' + url); return Promise.reject(config.throws); } - var opts = config.opts || {}; + + const opts = config.opts || {}; opts.url = url; opts.status = config.status || 200; + // the ternary oprator is to cope with new Headers(undefined) throwing in chrome + // (unclear to me if this is a bug or if the specification says this is correct behaviour) opts.headers = config.headers ? new Headers(config.headers) : new Headers(); - var body = config.body; + let body = config.body; + if (config.body != null && typeof body === 'object') { body = JSON.stringify(body); } debug('sending body "' + body + '"" for ' + url); + if (stream) { - var s = new stream.Readable(); + let s = new stream.Readable(); if (body != null) { s.push(body, 'utf-8'); } s.push(null); body = s; } + return Promise.resolve(new Response(body, opts)); } @@ -68,7 +73,7 @@ function compileRoute (route) { } if (typeof route.matcher === 'string') { - var expectedUrl = route.matcher; + let expectedUrl = route.matcher; if (route.matcher.indexOf('^') === 0) { debug('constructing starts with string matcher for route: ' + route.name); expectedUrl = expectedUrl.substr(1); @@ -83,7 +88,7 @@ function compileRoute (route) { } } else if (route.matcher instanceof RegExp) { debug('constructing regex matcher for route: ' + route.name); - var urlRX = route.matcher; + const urlRX = route.matcher; route.matcher = function (url) { return urlRX.test(url); }; @@ -91,228 +96,210 @@ function compileRoute (route) { return route; } -var FetchMock = function (opts) { - Headers = opts.Headers; - Response = opts.Response; - stream = opts.stream; - Blob = opts.Blob; - theGlobal = opts.theGlobal; - this.routes = []; - this._calls = {}; - this.usesGlobalFetch = true; -}; - -FetchMock.prototype.useNonGlobalFetch = function (func) { - this.usesGlobalFetch = false; - this.fetch = func; -}; - -FetchMock.prototype.registerRoute = function (name, matcher, response) { - debug('registering routes'); - var routes; - if (name instanceof Array) { - routes = name; - } else if (arguments.length === 3 ) { - routes = [{ - name: name, - matcher: matcher, - response: response - }]; - } else { - routes = [name]; +class FetchMock { + constructor (opts) { + Headers = opts.Headers; + Response = opts.Response; + stream = opts.stream; + Blob = opts.Blob; + theGlobal = opts.theGlobal; + debug = opts.debug; + this.routes = []; + this._calls = {}; + this.mockedContext = theGlobal; + this.realFetch = theGlobal.fetch; } - debug('registering routes: ' + routes.map(function (r) {return r.name})); + useNonGlobalFetch (func) { + this.mockedContext = this; + this.realFetch = func; + } - this.routes = this.routes.concat(routes.map(compileRoute)); -}; + registerRoute (name, matcher, response) { + debug('registering routes'); + let routes; + if (name instanceof Array) { + routes = name; + } else if (arguments.length === 3 ) { + routes = [{ + name, + matcher, + response, + }]; + } else { + routes = [name]; + } -FetchMock.prototype.unregisterRoute = function (names) { + debug('registering routes: ' + routes.map(r => r.name)); - if (!names) { - debug('unregistering all routes'); - this.routes = []; - return; - } - if (!(names instanceof Array)) { - names = [names]; + this.routes = this.routes.concat(routes.map(compileRoute)); } - debug('unregistering routes: ' + names); + unregisterRoute (names) { - this.routes = this.routes.filter(function (route) { - var keep = names.indexOf(route.name) === -1; - if (!keep) { - debug('unregistering route ' + route.name); + if (!names) { + debug('unregistering all routes'); + this.routes = []; + return; } - return keep; - }); -}; - -FetchMock.prototype.getRouter = function (config) { - debug('building router'); - - var routes; - - if (config.routes) { - debug('applying one time only routes'); - if (!(config.routes instanceof Array)) { - config.routes = [config.routes]; + if (!(names instanceof Array)) { + names = [names]; } - var preRegisteredRoutes = {}; - this.routes.forEach(function (route) { - preRegisteredRoutes[route.name] = route; - }); - routes = config.routes.map(function (route) { - if (typeof route === 'string') { - debug('applying preregistered route ' + route); - return preRegisteredRoutes[route]; - } else { - debug('applying one time route ' + route.name); - return compileRoute(route); + debug('unregistering routes: ' + names); + + this.routes = this.routes.filter(route => { + const keep = names.indexOf(route.name) === -1; + if (!keep) { + debug('unregistering route ' + route.name); } + return keep; }); - } else { - debug('no one time only routes defined. Using preregistered routes only'); - routes = this.routes; } + getRouter (config) { + debug('building router'); - var routeNames = {}; - routes.forEach(function (route) { - if (routeNames[route.name]) { - throw 'Route names must be unique'; - } - routeNames[route.name] = true; - }); - - config.responses = config.responses || {}; - - return function (url, opts) { - var response; - debug('searching for matching route for ' + url); - routes.some(function (route) { + let routes; - if (route.matcher(url, opts)) { - debug('Found matching route (' + route.name + ') for ' + url); - this.push(route.name, [url, opts]); + if (config.routes) { + debug('applying one time only routes'); + if (!(config.routes instanceof Array)) { + config.routes = [config.routes]; + } - if (config.responses[route.name]) { - debug('Overriding response for ' + route.name); - response = config.responses[route.name]; + const preRegisteredRoutes = {}; + this.routes.forEach(route => { + preRegisteredRoutes[route.name] = route; + }); + routes = config.routes.map(route => { + if (typeof route === 'string') { + debug('applying preregistered route ' + route); + return preRegisteredRoutes[route]; } else { - debug('Using default response for ' + route.name); - response = route.response; + debug('applying one time route ' + route.name); + return compileRoute(route); } + }); + } else { + debug('no one time only routes defined. Using preregistered routes only'); + routes = this.routes; + } - if (typeof response === 'function') { - debug('Constructing dynamic response for ' + route.name); - response = response(url, opts); - } - return true; - } - }.bind(this)); - debug('returning response for ' + url); - return response; - }.bind(this); -}; + const routeNames = {}; + routes.forEach(route => { + if (routeNames[route.name]) { + throw 'Route names must be unique'; + } + routeNames[route.name] = true; + }); -FetchMock.prototype.push = function (name, call) { - this._calls[name] = this._calls[name] || []; - this._calls[name].push(call); -}; + config.responses = config.responses || {}; + + return (url, opts) => { + let response; + debug('searching for matching route for ' + url); + routes.some(route => { + + if (route.matcher(url, opts)) { + debug('Found matching route (' + route.name + ') for ' + url); + this.push(route.name, [url, opts]); + + if (config.responses[route.name]) { + debug('Overriding response for ' + route.name); + response = config.responses[route.name]; + } else { + debug('Using default response for ' + route.name); + response = route.response; + } + + if (typeof response === 'function') { + debug('Constructing dynamic response for ' + route.name); + response = response(url, opts); + } + return true; + } + }); -FetchMock.prototype.mock = function (config) { - debug('mocking fetch'); + debug('returning response for ' + url); + return response; + }; + } - if (this.isMocking) { - throw 'fetch-mock is already mocking routes. Call .restore() before mocking again or use .reMock() if this is intentional'; + push (name, call) { + this._calls[name] = this._calls[name] || []; + this._calls[name].push(call); } - this.isMocking = true; - var mock = this.constructMock(config); + mock (config) { + debug('mocking fetch'); - if (this.usesGlobalFetch) { - debug('applying sinon.stub to fetch'); - sinon.stub(theGlobal, 'fetch', mock); - } else { - if (this.fetch) { - debug('applying sinon.stub to fetch'); - sinon.stub(this, 'fetch', mock); + if (this.isMocking) { + throw 'fetch-mock is already mocking routes. Call .restore() before mocking again or use .reMock() if this is intentional'; } - return mock; + + this.isMocking = true; + + return this.mockedContext.fetch = this.constructMock(config); } -}; - -FetchMock.prototype.constructMock = function (config) { - debug('constructing mock function'); - config = config || {}; - var self = this; - var defaultFetch = this.usesGlobalFetch ? theGlobal.fetch : this.fetch; - var router = this.getRouter(config); - config.greed = config.greed || 'none'; - - return function (url, opts) { - var response = router(url, opts); - if (response) { - debug('response found for ' + url); - return mockResponse(url, response); - } else { - debug('response not found for ' + url); - self.push('__unmatched', [url, opts]); - if (config.greed === 'good') { - debug('sending default good response'); - return mockResponse(url, {body: 'unmocked url: ' + url}); - } else if (config.greed === 'bad') { - debug('sending default bad response'); - return mockResponse(url, {throws: 'unmocked url: ' + url}); + + constructMock (config) { + debug('constructing mock function'); + config = config || {}; + const router = this.getRouter(config); + config.greed = config.greed || 'none'; + + return (url, opts) => { + const response = router(url, opts); + if (response) { + debug('response found for ' + url); + return mockResponse(url, response); } else { - debug('forwarding to default fetch'); - return defaultFetch && defaultFetch(url, opts); + debug('response not found for ' + url); + this.push('__unmatched', [url, opts]); + if (config.greed === 'good') { + debug('sending default good response'); + return mockResponse(url, {body: 'unmocked url: ' + url}); + } else if (config.greed === 'bad') { + debug('sending default bad response'); + return mockResponse(url, {throws: 'unmocked url: ' + url}); + } else { + debug('forwarding to default fetch'); + return this.realFetch(url, opts); + } } - } + }; } -}; - -FetchMock.prototype.restore = function () { - debug('restoring fetch'); - this.isMocking = false; - this.reset(); - if (this.usesGlobalFetch) { - theGlobal.fetch.restore(); - } else if (this.fetch) { - this.fetch.restore(); + + restore () { + debug('restoring fetch'); + this.isMocking = false; + this.mockedContext.fetch = this.realFetch; + this.reset(); + debug('fetch restored'); } - debug('fetch restored'); -}; - -FetchMock.prototype.reMock = function (config) { - this.restore(); - this.mock(config); -}; - -FetchMock.prototype.reset = function () { - debug('resetting call logs'); - this._calls = {}; - if (this.usesGlobalFetch) { - theGlobal.fetch.reset(); - } else if (this.fetch) { - this.fetch.reset(); + + reMock (config) { + this.restore(); + this.mock(config); } - debug('call logs reset'); -}; -FetchMock.prototype.calls = function (name) { - return this._calls[name] || []; -}; + reset () { + debug('resetting call logs'); + this._calls = {}; + } -FetchMock.prototype.called = function (name) { - if (!name) { - return !!Object.keys(this._calls).length; + calls (name) { + return this._calls[name] || []; } - return !!(this._calls[name] && this._calls[name].length); -}; + + called (name) { + if (!name) { + return !!Object.keys(this._calls).length; + } + return !!(this._calls[name] && this._calls[name].length); + } +} module.exports = FetchMock; diff --git a/test/server.js b/test/server.js index e41ddf83..a7285ee4 100644 --- a/test/server.js +++ b/test/server.js @@ -28,28 +28,12 @@ describe('non-global use', function () { fetchMock.restore(); } catch (e) {} }); - - it('returns mock function when non global fetch used', function () { - fetchMock.useNonGlobalFetch(); - var mock = fetchMock.mock(); - expect(typeof mock).to.equal('function'); - expect(typeof fetchMock.fetch).to.equal('undefined'); - expect(function () { - mock('url', {prop: 'val'}) - }).not.to.throw(); - expect(fetchMock.called('__unmatched')).to.be.true; - fetchMock.restore(); - fetchMock.usesGlobalFetch = true; - }); - it('stubs non global fetch if function passed in', function () { - fetch('url', {prop: 'val'}); fetchMock.useNonGlobalFetch(dummyFetch); - expect(fetchMock.fetch).to.equal(dummyFetch); + expect(fetchMock.realFetch).to.equal(dummyFetch); var mock = fetchMock.mock(); expect(typeof mock).to.equal('function'); - expect(typeof fetchMock.fetch.called).to.equal('boolean'); expect(function () { mock('url', {prop: 'val'}) }).not.to.throw(); diff --git a/test/spec.js b/test/spec.js index 104bbbdc..92f97601 100644 --- a/test/spec.js +++ b/test/spec.js @@ -8,7 +8,7 @@ module.exports = function (fetchMock, theGlobal) { // we can't use sinon to spy on fetch in these tests as fetch-mock // uses it internally and sinon doesn't allow spying on a previously // stubbed function, so just use this very basic stub - var dummyFetch = theGlobal.fetch = function () { + var dummyFetch = theGlobal.fetch = fetchMock.realFetch = function () { fetchCalls.push([].slice.call(arguments)); return Promise.resolve(arguments); }; @@ -57,12 +57,6 @@ module.exports = function (fetchMock, theGlobal) { }).not.to.throw(); }); - it('turn fetch into a sinon stub', function () { - fetchMock.mock(); - expect(typeof fetch.called).to.equal('boolean'); - fetchMock.restore(); - }); - }); describe('mocking fetch calls', function () {