From 6a2d4498f30a9583bd37f0f6982ee320e092914e Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Sun, 22 Mar 2020 18:15:44 +0000 Subject: [PATCH 1/4] midway through writing cheatsheet content --- docs/cheatsheet.md | 64 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 docs/cheatsheet.md diff --git a/docs/cheatsheet.md b/docs/cheatsheet.md new file mode 100644 index 00000000..215d10d8 --- /dev/null +++ b/docs/cheatsheet.md @@ -0,0 +1,64 @@ +# fetch-mock cheatsheet + +## Installation + +- .sandbox() + +## Lifecycle +- Stub fetch without defining any routes `.mock()` +- Let unmatched calls fall through to the network, and record them `.spy()` +- Respond with the given response to any unmatched calls `.catch(response)` +- Wait for all fetches to respond `.flush()` (pass in `true` to wait for all bodies to be streamed). e.g. `await fetchMock.flush(true)` +- Remove all routes & call history from fetch-mock, and also (if relevant)restore global `fetch` to its initial implementation `.restore()`, `.reset()` +- Discard all recorded calls, but keep defined routes `.resetHistory()` +- Discard all routes, but keep defined recorded calls`.resetBehavior()` + +## Matching requests + +### Reference request +The following request would be matched by all the mocks described below: +```js +fetch('http://example.com/users/bob?q=rita', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: { + prop1: 'val1', + prop2: 'val2' + } +}) +``` + +### Matching urls +All the below can be passed as the first argument into `.mock()`, (or as the `url` property on the first parameter for combining with other matchers). +- Match any url `'*'` +- Match exact url `'http://example.com/users/bob?q=rita'` +- Match beginning of url `'begin:http://example.com'` +- Match end of url `'end:bob?q=rita'` +- Match path of url `'path:/users/bob'` +- Match url using a glob expression `'glob:http://example.{com,gov}/*'` +- Match url using express parameter syntax `'express:/users/:name'` +- Match url using a RegExp `/\/users\/.*/` + +### Matching other parts of the request +The following should be passed as properties of an object as the first argument of `.mock()`. Multiple rules can be combined together. +- Match the method used by the request `{method: 'POST'}` +- Match based on headers sent `{headers: {'Content-Type': 'application/json'}}` +- Match an exact JSON body `{body: {prop1: 'val1', prop2: 'val2'}}` +- Match part of a JSON body `{body: {prop1: 'val1'}, matchPartialBody: true}` +- Match query parameters `{query: {q: 'rita'}}` +- Match express path parameters `{url: 'express:/users/:name', params: {name: 'bob'}}` + +### Custom matching +Match on any condition you like by: +- using a function `{functionMatcher: (url, options, request) => url.length > 100}` +- defining your own declarative matchers with [`addMatcher()`](http://www.wheresrhys.co.uk/fetch-mock/#api-mockingadd-matcher), e.g. something like this would be possible `{isCorsRequest: true, hasBody: true}` + +## Responses +- repeat +- delay +## Inspecting calls +- done +### Naming routes + From 438c8358756b038f2d0f46383aa7fdd2cae3874e Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Sun, 22 Mar 2020 18:29:18 +0000 Subject: [PATCH 2/4] refined set up/teardown section of cheatsheet --- docs/cheatsheet.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/cheatsheet.md b/docs/cheatsheet.md index 215d10d8..ace54479 100644 --- a/docs/cheatsheet.md +++ b/docs/cheatsheet.md @@ -4,16 +4,23 @@ - .sandbox() -## Lifecycle -- Stub fetch without defining any routes `.mock()` +## Mock setup methods +- Stub fetch defining a mock routes `.mock(matcher, response)` +- Stub fetch without defining any mock routes `.mock()` - Let unmatched calls fall through to the network, and record them `.spy()` - Respond with the given response to any unmatched calls `.catch(response)` -- Wait for all fetches to respond `.flush()` (pass in `true` to wait for all bodies to be streamed). e.g. `await fetchMock.flush(true)` +- Add a mock that only responds once `.once(matcher, response)` +- Add a mock that responds to any request `.any(response)` +- Add a mock that responds to any request, but only to that one request `.anyOnce(response)` +- Add a mock that only responds to the given method `.get()`, `.post()`, `.put()`, `.delete()`, `.head()`, `.patch()` +- Combinations of the above behaviours `getAny()`, `getOnce()`, `getAnyOnce()`, `postAny()` ... + +## Tear down methods - Remove all routes & call history from fetch-mock, and also (if relevant)restore global `fetch` to its initial implementation `.restore()`, `.reset()` - Discard all recorded calls, but keep defined routes `.resetHistory()` - Discard all routes, but keep defined recorded calls`.resetBehavior()` -## Matching requests +## Matching rules ### Reference request The following request would be matched by all the mocks described below: @@ -62,3 +69,6 @@ Match on any condition you like by: - done ### Naming routes +- Wait for all fetches to respond `.flush()` (pass in `true` to wait for all bodies to be streamed). e.g. `await fetchMock.flush(true)` + + From 7efa6c5ba5effb3b4b4bed03474fa2801e6824ae Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Sun, 22 Mar 2020 19:26:18 +0000 Subject: [PATCH 3/4] cheatsheet formatting --- docs/cheatsheet.md | 51 +++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/docs/cheatsheet.md b/docs/cheatsheet.md index ace54479..7ce2fa26 100644 --- a/docs/cheatsheet.md +++ b/docs/cheatsheet.md @@ -5,22 +5,23 @@ - .sandbox() ## Mock setup methods -- Stub fetch defining a mock routes `.mock(matcher, response)` -- Stub fetch without defining any mock routes `.mock()` -- Let unmatched calls fall through to the network, and record them `.spy()` -- Respond with the given response to any unmatched calls `.catch(response)` -- Add a mock that only responds once `.once(matcher, response)` -- Add a mock that responds to any request `.any(response)` -- Add a mock that responds to any request, but only to that one request `.anyOnce(response)` -- Add a mock that only responds to the given method `.get()`, `.post()`, `.put()`, `.delete()`, `.head()`, `.patch()` -- Combinations of the above behaviours `getAny()`, `getOnce()`, `getAnyOnce()`, `postAny()` ... +All these methods can be chained e.g. `fetchMock.getAny(200).catch(400)` +- Stub fetch and **define a route** `.mock(matcher, response)` +- Stub fetch **without a route** `.mock()` +- Spy on calls, letting them **fall through to the network** `.spy()` +- Respond with the given response to any **unmatched calls** `.catch(response)` +- Add a mock that only responds **once** `.once(matcher, response)` +- Add a mock that responds to **any** request `.any(response)` +- Add a mock that responds to **any request, but only once** `.anyOnce(response)` +- Add a mock that only responds to the given **method** `.get()`, `.post()`, `.put()`, `.delete()`, `.head()`, `.patch()` +- **Combinations** of the above behaviours `getAny()`, `getOnce()`, `getAnyOnce()`, `postAny()` ... ## Tear down methods -- Remove all routes & call history from fetch-mock, and also (if relevant)restore global `fetch` to its initial implementation `.restore()`, `.reset()` +- Remove all mocks and history `.restore()`, `.reset()` - Discard all recorded calls, but keep defined routes `.resetHistory()` - Discard all routes, but keep defined recorded calls`.resetBehavior()` -## Matching rules +## Request matching ### Reference request The following request would be matched by all the mocks described below: @@ -39,23 +40,23 @@ fetch('http://example.com/users/bob?q=rita', { ### Matching urls All the below can be passed as the first argument into `.mock()`, (or as the `url` property on the first parameter for combining with other matchers). -- Match any url `'*'` -- Match exact url `'http://example.com/users/bob?q=rita'` -- Match beginning of url `'begin:http://example.com'` -- Match end of url `'end:bob?q=rita'` -- Match path of url `'path:/users/bob'` -- Match url using a glob expression `'glob:http://example.{com,gov}/*'` -- Match url using express parameter syntax `'express:/users/:name'` -- Match url using a RegExp `/\/users\/.*/` +- Match **any** url `'*'` +- Match **exact** url `'http://example.com/users/bob?q=rita'` +- Match **beginning** `'begin:http://example.com'` +- Match **end** `'end:bob?q=rita'` +- Match **path** `'path:/users/bob'` +- Match using a **glob** expression `'glob:http://example.{com,gov}/*'` +- Match using **express** syntax `'express:/users/:name'` +- Match using a **RegExp** `/\/users\/.*/` ### Matching other parts of the request The following should be passed as properties of an object as the first argument of `.mock()`. Multiple rules can be combined together. -- Match the method used by the request `{method: 'POST'}` -- Match based on headers sent `{headers: {'Content-Type': 'application/json'}}` -- Match an exact JSON body `{body: {prop1: 'val1', prop2: 'val2'}}` -- Match part of a JSON body `{body: {prop1: 'val1'}, matchPartialBody: true}` -- Match query parameters `{query: {q: 'rita'}}` -- Match express path parameters `{url: 'express:/users/:name', params: {name: 'bob'}}` +- Match the request **method** `{method: 'POST'}` +- Match **headers** `{headers: {'Content-Type': 'application/json'}}` +- Match a **JSON body** `{body: {prop1: 'val1', prop2: 'val2'}}` +- Match **part of a JSON body** `{body: {prop1: 'val1'}, matchPartialBody: true}` +- Match **query** parameters `{query: {q: 'rita'}}` +- Match express **path parameters** `{url: 'express:/users/:name', params: {name: 'bob'}}` ### Custom matching Match on any condition you like by: From 1dfc6c2e8a62132573d90ef894e6d6f2c5fe68ad Mon Sep 17 00:00:00 2001 From: Rhys Evans Date: Sun, 22 Mar 2020 21:17:41 +0000 Subject: [PATCH 4/4] completed cheatsheet --- docs/cheatsheet.md | 111 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 87 insertions(+), 24 deletions(-) diff --git a/docs/cheatsheet.md b/docs/cheatsheet.md index 7ce2fa26..f59a97b0 100644 --- a/docs/cheatsheet.md +++ b/docs/cheatsheet.md @@ -1,10 +1,37 @@ # fetch-mock cheatsheet +*This is a first draft - please feedback in the [issues](https://github.com/wheresrhys/fetch-mock/issues)* + +- [Installation](#installation) +- [Set up and teardown](#setup-and-teardown) +- [Request matching](#request-matching) +- [Response configuration](#response-configuration) +- [Inspecting calls](#inspecting-calls) + ## Installation -- .sandbox() +`npm i -D fetch-mock` (or `npm i -D fetch-mock-jest` if using jest) + +### Global fetch +import/require the fetch-mock/fetch-mock-jest library. For the vast majority of test toolchains this _should_ jsut work without any additional wiring. + +### Local fetch with jest +```js +jest.mock('node-fetch', () => require('fetch-mock-jest').sandbox()) +const fetchMock = require('node-fetch') +``` + +### Local fetch with other test runners +```js +// pass this mock into your module mocking tool of choice +// Example uses https://www.npmjs.com/package/proxyquire +const fetchMock = require('fetch-mock').sandbox() +proxyquire('./my-module', {'node-fetch': fetchMock}) +``` + +## Setup and teardown -## Mock setup methods +### Mock setup methods All these methods can be chained e.g. `fetchMock.getAny(200).catch(400)` - Stub fetch and **define a route** `.mock(matcher, response)` - Stub fetch **without a route** `.mock()` @@ -16,41 +43,42 @@ All these methods can be chained e.g. `fetchMock.getAny(200).catch(400)` - Add a mock that only responds to the given **method** `.get()`, `.post()`, `.put()`, `.delete()`, `.head()`, `.patch()` - **Combinations** of the above behaviours `getAny()`, `getOnce()`, `getAnyOnce()`, `postAny()` ... -## Tear down methods +### Tear down methods - Remove all mocks and history `.restore()`, `.reset()` - Discard all recorded calls, but keep defined routes `.resetHistory()` - Discard all routes, but keep defined recorded calls`.resetBehavior()` ## Request matching -### Reference request The following request would be matched by all the mocks described below: ```js fetch('http://example.com/users/bob?q=rita', { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: { - prop1: 'val1', - prop2: 'val2' - } + headers: {'Content-Type': 'application/json'}, + body: '{"prop1": "val1", "prop2": "val2"}' }) ``` -### Matching urls -All the below can be passed as the first argument into `.mock()`, (or as the `url` property on the first parameter for combining with other matchers). +### Urls +Can be passed as +- the first argument `.mock('blah', 200)` +- a `url` property on the first argument `.mock({url: 'blah'}, 200)` + +#### Patterns - Match **any** url `'*'` - Match **exact** url `'http://example.com/users/bob?q=rita'` - Match **beginning** `'begin:http://example.com'` - Match **end** `'end:bob?q=rita'` - Match **path** `'path:/users/bob'` -- Match using a **glob** expression `'glob:http://example.{com,gov}/*'` -- Match using **express** syntax `'express:/users/:name'` -- Match using a **RegExp** `/\/users\/.*/` +- Match a **glob** expression `'glob:http://example.{com,gov}/*'` +- Match **express** syntax `'express:/users/:name'` +- Match a **RegExp** `/\/users\/.*/` + +#### Naming routes +When defining multiple mocks on the same underlying url (e.g. differing only on headers), set a `name` property on the matcher of each route. -### Matching other parts of the request -The following should be passed as properties of an object as the first argument of `.mock()`. Multiple rules can be combined together. +### Other properties +The following should be passed as properties of the first argument of `.mock()`. - Match the request **method** `{method: 'POST'}` - Match **headers** `{headers: {'Content-Type': 'application/json'}}` - Match a **JSON body** `{body: {prop1: 'val1', prop2: 'val2'}}` @@ -58,18 +86,53 @@ The following should be passed as properties of an object as the first argument - Match **query** parameters `{query: {q: 'rita'}}` - Match express **path parameters** `{url: 'express:/users/:name', params: {name: 'bob'}}` -### Custom matching +### Custom Match on any condition you like by: - using a function `{functionMatcher: (url, options, request) => url.length > 100}` - defining your own declarative matchers with [`addMatcher()`](http://www.wheresrhys.co.uk/fetch-mock/#api-mockingadd-matcher), e.g. something like this would be possible `{isCorsRequest: true, hasBody: true}` -## Responses -- repeat -- delay +## Response configuration +Responses are configured with eth second, and sometimes third, arguments passed to `.mock()` (or the first and second argument of `.any()` or `.catch()`). Where only one code sample is given below, it describes the second argument; otherwise the second and third are given. *[Note - in the next major version these will all be available on the second argument]* + +- **`Response`** instance `new Response('hello world')` +- **status code** `200` +- **text** `hello world` +- **JSON** `{prop: 'val'}` +- **streamed content** `new Blob()`, `{sendAsJson: false}` +- **headers** `{body: 'hello world', status: 200, headers: {'Content-Type': 'text'}` +- **throw** an error `{throws: new Error('fetch failed')}` +- **redirect** `{redirectUrl: 'http://other.site, status: 302}` +- **function** ``(url, options, request) => `Content from ${url}` `` + +### Timing and repetition +- Respond a specified **number of times** `200`, `{repeat: 3}` +- **Delay** by a number of milliseconds `200`, `{delay: 2000}` +- Custom async behaviour using a **Promise** `myPromise.then(200)` + +Functions and Promises can be nested to any depth to implement complex race conditions + ## Inspecting calls -- done -### Naming routes +`.calls()` retrieves a list of all calls matching certain conditions. It return an array of `[url, options]` arrays, which also have a `.request` property containng the original `Request` instance. + +### Filtering +- **all** calls `.calls()` +- **matched** calls `.calls(true)` or `.calls('matched')` +- **unmatched** calls `.calls(false)` or `.calls('unmatched')` +- calls to a **named route** `.calls('My route`) +- calls to a **url pattern** used in a route `.calls('end:/path/bob`) +- calls matching a **matcher** `.calls(anyValidMatcher)` +- calls filtered by **method** `.calls(anyValidMatcher, 'POST')` + +### Shortcut methods +These accept the same filters as above, but give a quicker route to answering common questions +- Do **any calls** match the filter? `.called()` +- What was the **last call** `.lastCall()` +- What was the **last url** `.lastUrl()` +- What was the **last options** `.lastOptions()` +### Completion +- Check if **all routes** have been **called as expected** `.done()` +- Check if **all routes matching the filter** have been **called as expected** `.done(filter)` (filter must be a **route name** or **url pattern**) - Wait for all fetches to respond `.flush()` (pass in `true` to wait for all bodies to be streamed). e.g. `await fetchMock.flush(true)`