Skip to content

Commit 8d6bbce

Browse files
sam-gcavolkovi
authored andcommitted
Add mock fetch helper (#2878)
Add mock fetch library for tests
1 parent b230b1b commit 8d6bbce

File tree

4 files changed

+195
-1
lines changed

4 files changed

+195
-1
lines changed

packages-exp/auth-exp/karma.conf.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
const karmaBase = require('../../config/karma.base');
1919

20-
const files = ['src/**/*.test.ts'];
20+
const files = ['src/**/*.test.ts', 'test/**/*.test.ts'];
2121

2222
module.exports = function(config) {
2323
const karmaConfig = Object.assign({}, karmaBase, {

packages-exp/auth-exp/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"dev": "rollup -c -w",
2020
"test": "yarn type-check && run-p lint test:browser test:node",
2121
"test:browser": "karma start --single-run",
22+
"test:browser:debug": "karma start",
2223
"test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha src/**/*.test.* --opts ../../config/mocha.node.opts",
2324
"type-check": "tsc -p . --noEmit",
2425
"prepare": "yarn build"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect } from 'chai';
19+
import * as mockFetch from './mock_fetch';
20+
21+
async function fetchJson(path: string, req?: object): Promise<object> {
22+
const body = req
23+
? {
24+
body: JSON.stringify(req)
25+
}
26+
: {};
27+
const request: RequestInit = {
28+
method: 'POST',
29+
headers: {
30+
'Content-Type': 'application/json'
31+
},
32+
referrerPolicy: 'no-referrer',
33+
...body
34+
};
35+
36+
const response = await fetch(path, request);
37+
return response.json();
38+
}
39+
40+
describe('mock fetch utility', () => {
41+
beforeEach(mockFetch.setUp);
42+
afterEach(mockFetch.tearDown);
43+
44+
describe('matched requests', () => {
45+
it('returns the correct object for multiple routes', async () => {
46+
mockFetch.mock('/a', { a: 1 });
47+
mockFetch.mock('/b', { b: 2 });
48+
49+
expect(await fetchJson('/a')).to.eql({ a: 1 });
50+
expect(await fetchJson('/b')).to.eql({ b: 2 });
51+
});
52+
53+
it('passes through the status of the mock', async () => {
54+
mockFetch.mock('/not-ok', {}, 500);
55+
expect((await fetch('/not-ok')).status).to.equal(500);
56+
});
57+
58+
it('records calls to the mock', async () => {
59+
const someRequest = {
60+
sentence:
61+
'Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo'
62+
};
63+
64+
const mock = mockFetch.mock('/word', {});
65+
await fetchJson('/word', someRequest);
66+
await fetchJson('/word', { a: 'b' });
67+
await fetch('/word');
68+
69+
expect(mock.calls.length).to.equal(3);
70+
expect(mock.calls[0].request).to.eql(someRequest);
71+
expect(mock.calls[1].request).to.eql({ a: 'b' });
72+
expect(mock.calls[2].request).to.equal(undefined);
73+
});
74+
});
75+
76+
describe('route rejection', () => {
77+
it('if the route is not in the map', () => {
78+
mockFetch.mock('/test', {});
79+
expect(() => fetch('/not-test')).to.throw(
80+
'Unknown route being requested: /not-test'
81+
);
82+
});
83+
84+
it('if call is not a string', () => {
85+
mockFetch.mock('/blah', {});
86+
expect(() => fetch(new Request({} as any))).to.throw(
87+
'URL passed to fetch was not a string'
88+
);
89+
});
90+
});
91+
});
92+
93+
describe('mock fetch utility (no setUp/tearDown)', () => {
94+
it('errors if mock attempted without setup', () => {
95+
expect(() => mockFetch.mock('/test', {})).to.throw(
96+
'Mock fetch is not set up'
97+
);
98+
});
99+
100+
it('routes do not carry to next run', async () => {
101+
mockFetch.setUp();
102+
mockFetch.mock('/test', { first: 'first' });
103+
expect(await fetchJson('/test')).to.eql({ first: 'first' });
104+
mockFetch.tearDown();
105+
mockFetch.setUp();
106+
expect(() => fetch('/test')).to.throw(
107+
'Unknown route being requested: /test'
108+
);
109+
mockFetch.tearDown();
110+
});
111+
});
+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { stub, SinonStub } from 'sinon';
19+
20+
export interface Call {
21+
request?: object;
22+
}
23+
24+
export interface Route {
25+
response: object;
26+
status: number;
27+
calls: Call[];
28+
}
29+
30+
let fetchStub: SinonStub | null = null;
31+
let routes = new Map<string, Route>();
32+
33+
// Using a constant rather than a function to enforce the type matches fetch()
34+
const fakeFetch: typeof fetch = (input: RequestInfo, request?: RequestInit) => {
35+
if (typeof input !== 'string') {
36+
throw new Error('URL passed to fetch was not a string');
37+
}
38+
39+
if (!routes.has(input)) {
40+
throw new Error(`Unknown route being requested: ${input}`);
41+
}
42+
43+
// Bang-assertion is fine since we check for routes.has() above
44+
const { response, status, calls } = routes.get(input)!;
45+
46+
calls.push({
47+
request: request?.body ? JSON.parse(request.body as string) : undefined
48+
});
49+
50+
const blob = new Blob([JSON.stringify(response)]);
51+
return Promise.resolve(
52+
new Response(blob, {
53+
status
54+
})
55+
);
56+
};
57+
58+
export function setUp(): void {
59+
fetchStub = stub(self, 'fetch');
60+
fetchStub.callsFake(fakeFetch);
61+
}
62+
63+
export function mock(url: string, response: object, status = 200): Route {
64+
if (!fetchStub) {
65+
throw new Error('Mock fetch is not set up. Call setUp() first');
66+
}
67+
68+
const route: Route = {
69+
response,
70+
status,
71+
calls: []
72+
};
73+
74+
routes.set(url, route);
75+
return route;
76+
}
77+
78+
export function tearDown(): void {
79+
fetchStub?.restore();
80+
fetchStub = null;
81+
routes = new Map();
82+
}

0 commit comments

Comments
 (0)