diff --git a/README.md b/README.md index ec7106a3..1cc982ad 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Mock http requests made using fetch (or isomorphic-fetch) *notes* -- When using isomorphic-fetch or node-fetch `fetch` should be added as a global +- 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 @@ -81,6 +81,29 @@ Often your application/module will need a mocked response for some http requests ### `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 ```javascript diff --git a/src/fetch-mock.js b/src/fetch-mock.js index 547a8da2..42e55202 100644 --- a/src/fetch-mock.js +++ b/src/fetch-mock.js @@ -99,6 +99,12 @@ var FetchMock = function (opts) { 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) { @@ -145,6 +151,7 @@ FetchMock.prototype.unregisterRoute = function (names) { FetchMock.prototype.getRouter = function (config) { debug('building router'); + var routes; if (config.routes) { @@ -219,45 +226,65 @@ FetchMock.prototype.push = function (name, call) { FetchMock.prototype.mock = function (config) { debug('mocking fetch'); - var self = this; + if (this.isMocking) { throw 'fetch-mock is already mocking routes. Call .restore() before mocking again or use .reMock() if this is intentional'; } this.isMocking = true; + var mock = this.constructMock(config); + + 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); + } + return mock; + } +}; + +FetchMock.prototype.constructMock = function (config) { + debug('constructing mock function'); config = config || {}; - var defaultFetch = theGlobal.fetch; + var self = this; + var defaultFetch = this.usesGlobalFetch ? theGlobal.fetch : this.fetch; var router = this.getRouter(config); config.greed = config.greed || 'none'; - debug('applying sinon.stub to fetch') - sinon.stub(theGlobal, 'fetch', function (url, opts) { - var response = router(url, opts); - if (response) { - debug('response found for ' + url); - return mockResponse(url, response); + 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}); } 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}); - } else { - debug('forwarding to default fetch'); - return defaultFetch(url, opts); - } + debug('forwarding to default fetch'); + return defaultFetch && defaultFetch(url, opts); } - }); + } + } }; FetchMock.prototype.restore = function () { debug('restoring fetch'); this.isMocking = false; this.reset(); - theGlobal.fetch.restore(); + if (this.usesGlobalFetch) { + theGlobal.fetch.restore(); + } else if (this.fetch) { + this.fetch.restore(); + } debug('fetch restored'); }; @@ -269,7 +296,11 @@ FetchMock.prototype.reMock = function (config) { FetchMock.prototype.reset = function () { debug('resetting call logs'); this._calls = {}; - theGlobal.fetch.reset(); + if (this.usesGlobalFetch) { + theGlobal.fetch.reset(); + } else if (this.fetch) { + this.fetch.reset(); + } debug('call logs reset'); }; diff --git a/test/server.js b/test/server.js index 7c015b2f..e41ddf83 100644 --- a/test/server.js +++ b/test/server.js @@ -3,5 +3,66 @@ require('es6-promise').polyfill(); var fetchMock = require('../server.js'); +var fetchCalls = []; +var expect = require('chai').expect; +var sinon = require('sinon'); + +// 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 = function () { + fetchCalls.push([].slice.call(arguments)); + return Promise.resolve(arguments); +}; + +var err = function (err) { + console.log(error); +} + +require('./spec')(fetchMock, GLOBAL); + +describe('non-global use', function () { + + before(function () { + try { + 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); + 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(); + expect(fetchMock.called('__unmatched')).to.be.true; + expect(fetchCalls.length).to.equal(1); + expect(fetchCalls[0]).to.eql(['url', {prop: 'val'}]); + + fetchMock.restore(); + fetchMock.usesGlobalFetch = true; + }); +}); + + + + -require('./spec')(fetchMock, GLOBAL); \ No newline at end of file