Skip to content

Commit

Permalink
Merge pull request #22 from wheresrhys/simple-use-ase-shorthands
Browse files Browse the repository at this point in the history
Simple use case shorthands
  • Loading branch information
wheresrhys committed Oct 21, 2015
2 parents df64a1d + 9a04b51 commit b5dd496
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 103 deletions.
223 changes: 124 additions & 99 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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');
Expand Down Expand Up @@ -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();
});

```
41 changes: 37 additions & 4 deletions src/fetch-mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 () {
Expand All @@ -303,7 +336,7 @@ class FetchMock {
}

calls (name) {
return this._calls[name] || [];
return name ? (this._calls[name] || []) : (this._calls._mock || this._calls);
}

called (name) {
Expand Down
33 changes: 33 additions & 0 deletions test/spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit b5dd496

Please sign in to comment.