Skip to content

Commit d11dcff

Browse files
VikalpPDavertMik
authored andcommitted
feat: Mocking network requests/response using PollyJS (codeceptjs#1733)
* feat: add method to start mocking * feat: add method to mock requests * add test cases for I.startMocking & I.mock * refactor: abstract Polly setup * refactor: separate out Polly helper * refactor: add acceptance test for Polly * test: add more test cases for different scenarios * refactor: start mocking automatically fix: disconnect Puppeteer when stop mocking. * test: add test for stopMocking refactor: change testCases according to new method name I.mockRequest() * style: remove line-breaks * test: add timeout for waiting response * test: remove waiting for response * test: remove redundant package requirement * fix: set by default let pass-through for all requests * test: remove wait for response
1 parent b6030dd commit d11dcff

File tree

6 files changed

+308
-0
lines changed

6 files changed

+308
-0
lines changed

lib/helper/Polly.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
const Helper = require('../helper');
2+
const requireg = require('requireg');
3+
const { appendBaseUrl } = require('../utils');
4+
5+
let PollyJS;
6+
7+
class Polly extends Helper {
8+
constructor(config) {
9+
super(config);
10+
this._setConfig(config);
11+
PollyJS = requireg('@pollyjs/core').Polly;
12+
}
13+
14+
static _checkRequirements() {
15+
try {
16+
requireg('@pollyjs/core');
17+
} catch (e) {
18+
return ['@pollyjs/core@^2.5.0'];
19+
}
20+
}
21+
22+
/**
23+
* Start mocking network requests/responses
24+
*/
25+
async _startMocking(title = 'Test') {
26+
if (!this.helpers && !this.helpers.Puppeteer) {
27+
throw new Error('Puppeteer is the only supported helper right now');
28+
}
29+
await this._connectPuppeteer(title);
30+
}
31+
32+
/**
33+
* Connect Puppeteer helper to mock future requests.
34+
*/
35+
async _connectPuppeteer(title) {
36+
const adapter = require('@pollyjs/adapter-puppeteer');
37+
38+
PollyJS.register(adapter);
39+
const { page } = this.helpers.Puppeteer;
40+
await page.setRequestInterception(true);
41+
42+
this.polly = new PollyJS(title, {
43+
adapters: ['puppeteer'],
44+
adapterOptions: {
45+
puppeteer: { page },
46+
},
47+
});
48+
49+
// By default let pass through all network requests
50+
if (this.polly) this.polly.server.any().passthrough();
51+
}
52+
53+
/**
54+
* Mock response status
55+
*
56+
* ```js
57+
* I.mockRequest('GET', '/api/users', 200);
58+
* I.mockRequest('ANY', '/secretsRoutes/*', 403);
59+
* I.mockRequest('POST', '/secrets', { secrets: 'fakeSecrets' });
60+
* ```
61+
*
62+
* Multiple requests
63+
*
64+
* ```js
65+
* I.mockRequest('GET', ['/secrets', '/v2/secrets'], 403);
66+
* ```
67+
*/
68+
async mockRequest(method, oneOrMoreUrls, dataOrStatusCode) {
69+
await this._checkAndStartMocking();
70+
const handler = this._getRouteHandler(
71+
method,
72+
oneOrMoreUrls,
73+
this.options.url,
74+
);
75+
76+
if (typeof dataOrStatusCode === 'number') {
77+
const statusCode = dataOrStatusCode;
78+
return handler.intercept((_, res) => res.sendStatus(statusCode));
79+
}
80+
const data = dataOrStatusCode;
81+
return handler.intercept((_, res) => res.send(data));
82+
}
83+
84+
_checkIfMockingStarted() {
85+
return this.polly && this.polly.server;
86+
}
87+
88+
/**
89+
* Starts mocking if it's not started yet.
90+
*/
91+
async _checkAndStartMocking() {
92+
if (!this._checkIfMockingStarted()) {
93+
await this._startMocking();
94+
}
95+
}
96+
97+
/**
98+
* Get route-handler of Polly for different HTTP methods
99+
* @param {string} method HTTP request methods(e.g., 'GET', 'POST')
100+
* @param {string|array} oneOrMoreUrls URL or array of URLs
101+
* @param {string} baseUrl hostURL
102+
*/
103+
_getRouteHandler(method, oneOrMoreUrls, baseUrl) {
104+
const { server } = this.polly;
105+
106+
oneOrMoreUrls = appendBaseUrl(baseUrl, oneOrMoreUrls);
107+
method = method.toLowerCase();
108+
109+
if (httpMethods.includes(method)) {
110+
return server[method](oneOrMoreUrls);
111+
}
112+
return server.any(oneOrMoreUrls);
113+
}
114+
115+
/**
116+
* Stop mocking requests.
117+
*/
118+
async stopMocking() {
119+
if (!this._checkIfMockingStarted()) return;
120+
121+
await this._disconnectPuppeteer();
122+
await this.polly.flush();
123+
await this.polly.stop();
124+
this.polly = undefined;
125+
}
126+
127+
async _disconnectPuppeteer() {
128+
const { page } = this.helpers.Puppeteer;
129+
await page.setRequestInterception(false);
130+
}
131+
}
132+
133+
const httpMethods = [
134+
'get',
135+
'put',
136+
'post',
137+
'patch',
138+
'delete',
139+
'merge',
140+
'head',
141+
'options',
142+
];
143+
144+
module.exports = Polly;

lib/utils.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,34 @@ module.exports.beautify = function (code) {
306306
return format(code, { indent_size: 2, space_in_empty_paren: true });
307307
};
308308

309+
function shouldAppendBaseUrl(url) {
310+
return !/^\w+\:\/\//.test(url);
311+
}
312+
313+
function trimUrl(url) {
314+
const firstChar = url.substr(1);
315+
if (firstChar === '/') {
316+
url = url.slice(1);
317+
}
318+
return url;
319+
}
320+
321+
function joinUrl(baseUrl, url) {
322+
return shouldAppendBaseUrl(url) ? `${baseUrl}/${trimUrl(url)}` : url;
323+
}
324+
325+
module.exports.appendBaseUrl = function (baseUrl, oneOrMoreUrls) {
326+
// Remove '/' if it's at the end of baseUrl
327+
const lastChar = baseUrl.substr(-1);
328+
if (lastChar === '/') {
329+
baseUrl = baseUrl.slice(0, -1);
330+
}
331+
332+
if (!Array.isArray(oneOrMoreUrls)) {
333+
return joinUrl(baseUrl, oneOrMoreUrls);
334+
}
335+
return oneOrMoreUrls.map(url => joinUrl(baseUrl, url));
336+
309337
/**
310338
* Recursively search key in object and replace it's value.
311339
*
@@ -333,4 +361,5 @@ module.exports.replaceValueDeep = function replaceValueDeep(obj, key, value) {
333361
}
334362
}
335363
return obj;
364+
336365
};

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
"sprintf-js": "^1.1.1"
6464
},
6565
"devDependencies": {
66+
"@pollyjs/adapter-puppeteer": "^2.5.0",
67+
"@pollyjs/core": "^2.5.0",
6668
"@types/inquirer": "^0.0.35",
6769
"@types/node": "^8.10.48",
6870
"@wdio/sauce-service": "^5.8.0",

test/acceptance/codecept.Puppeteer.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ module.exports.config = {
1515
],
1616
},
1717
},
18+
Polly: {
19+
url: TestHelper.siteUrl(),
20+
},
1821
},
1922
include: {},
2023
bootstrap: false,

test/acceptance/mock_test.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
Feature('Mocking');
2+
3+
const fetchPost = response => response.url() === 'https://jsonplaceholder.typicode.com/posts/1';
4+
5+
const fetchComments = response => response.url() === 'https://jsonplaceholder.typicode.com/comments/1';
6+
7+
const fetchUsers = response => response.url() === 'https://jsonplaceholder.typicode.com/users/1';
8+
9+
Scenario('change statusCode @Puppeteer', (I) => {
10+
I.amOnPage('/form/fetch_call');
11+
I.click('GET POSTS');
12+
I.waitForResponse(fetchPost, 3);
13+
I.mockRequest('GET', 'https://jsonplaceholder.typicode.com/*', 404);
14+
I.click('GET POSTS');
15+
I.see('Can not load data!');
16+
I.stopMocking();
17+
});
18+
19+
Scenario('change response data @Puppeteer', (I) => {
20+
I.amOnPage('/form/fetch_call');
21+
I.click('GET COMMENTS');
22+
I.waitForResponse(fetchComments, 3);
23+
I.mockRequest('GET', 'https://jsonplaceholder.typicode.com/*', {
24+
modified: 'This is modified from mocking',
25+
});
26+
I.click('GET COMMENTS');
27+
I.see('This is modified from mocking', '#data');
28+
I.stopMocking();
29+
});
30+
31+
Scenario('change response data for multiple requests @Puppeteer', (I) => {
32+
I.amOnPage('/form/fetch_call');
33+
I.click('GET USERS');
34+
I.waitForResponse(fetchUsers, 3);
35+
I.mockRequest(
36+
'GET',
37+
[
38+
'https://jsonplaceholder.typicode.com/posts/*',
39+
'https://jsonplaceholder.typicode.com/comments/*',
40+
'https://jsonplaceholder.typicode.com/users/*',
41+
],
42+
{
43+
modified: 'MY CUSTOM DATA',
44+
},
45+
);
46+
I.click('GET POSTS');
47+
I.see('MY CUSTOM DATA', '#data');
48+
I.click('GET COMMENTS');
49+
I.see('MY CUSTOM DATA', '#data');
50+
I.click('GET USERS');
51+
I.see('MY CUSTOM DATA', '#data');
52+
I.stopMocking();
53+
});
54+
55+
Scenario(
56+
'should request for original data after mocking stopped @Puppeteer',
57+
(I) => {
58+
I.amOnPage('/form/fetch_call');
59+
I.click('GET COMMENTS');
60+
I.mockRequest('GET', 'https://jsonplaceholder.typicode.com/*', {
61+
comment: 'CUSTOM',
62+
});
63+
I.click('GET COMMENTS');
64+
I.see('CUSTOM', '#data');
65+
I.stopMocking();
66+
67+
I.click('GET COMMENTS');
68+
I.dontSee('CUSTOM', '#data');
69+
},
70+
);
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
7+
<title>Fetch JSON data</title>
8+
<style>
9+
td {
10+
padding: 4px;
11+
border: 1px solid #333333;
12+
}
13+
</style>
14+
</head>
15+
<body>
16+
<h3>JSON data</h3>
17+
<button onclick="getPostData()">GET POSTS</button>
18+
<button onclick="getCommentsData()">GET COMMENTS</button>
19+
<button onclick="getUsersData()">GET USERS</button>
20+
<div id="data">
21+
<h4>No data here</h4>
22+
</div>
23+
</body>
24+
25+
<script type="text/javascript">
26+
const tableData = data =>
27+
Object.entries(data).reduce(
28+
(html, [key, value]) => `${html}
29+
<tr>
30+
<td>${key}</td>
31+
<td>${value}</td>
32+
</tr>
33+
`,
34+
""
35+
);
36+
37+
const data = document.querySelector("#data");
38+
39+
const getData = url =>
40+
fetch(url)
41+
.then(response => response.json())
42+
.then(json => {
43+
data.innerHTML = `<table>
44+
${tableData(json)}
45+
</table>`;
46+
47+
console.log(json);
48+
})
49+
.catch(() => {
50+
data.innerHTML = "Can not load data!";
51+
});
52+
53+
const getPostData = () =>
54+
getData("https://jsonplaceholder.typicode.com/posts/1");
55+
const getCommentsData = () =>
56+
getData("https://jsonplaceholder.typicode.com/comments/1");
57+
const getUsersData = () =>
58+
getData("https://jsonplaceholder.typicode.com/users/1");
59+
</script>
60+
</html>

0 commit comments

Comments
 (0)