Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@

[index.test.js](https://raw.githubusercontent.com/Netflix/pollyjs/master/examples/jest-node-fetch/__tests__/index.test.js ':include :type=code')

## TypeScript + Jest + Node Fetch

**[Full Source](https://github.com/Netflix/pollyjs/tree/master/examples/typescript-jest-node-fetch)**

[auto-setup-polly.ts](https://raw.githubusercontent.com/Netflix/pollyjs/master/examples/typescript-jest-node-fetch/src/utils/auto-setup-polly.ts ':include :type=code')

[github-api.test.ts](https://raw.githubusercontent.com/Netflix/pollyjs/master/examples/typescript-jest-node-fetch/src/github-api.test.ts ':include :type=code')

## Jest + Puppeteer

**[Full Source](https://github.com/Netflix/pollyjs/tree/master/examples/jest-puppeteer)**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
{
"log": {
"_recordingName": "github-api client/getUser",
"creator": {
"comment": "persister:fs",
"name": "Polly.JS",
"version": "5.1.1"
},
"entries": [
{
"_id": "daab17694c1a59a0f3781977d2bf32d7",
"_order": 0,
"cache": {},
"request": {
"bodySize": 0,
"cookies": [],
"headers": [
{
"_fromType": "array",
"name": "accept",
"value": "application/json+vnd.github.v3.raw"
},
{
"_fromType": "array",
"name": "content-type",
"value": "application/json"
},
{
"_fromType": "array",
"name": "user-agent",
"value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)"
},
{
"_fromType": "array",
"name": "accept-encoding",
"value": "gzip,deflate"
},
{
"_fromType": "array",
"name": "connection",
"value": "close"
},
{
"name": "host",
"value": "api.github.com"
}
],
"headersSize": 269,
"httpVersion": "HTTP/1.1",
"method": "GET",
"queryString": [],
"url": "https://api.github.com/users/netflix"
},
"response": {
"bodySize": 525,
"content": {
"_isBinary": true,
"mimeType": "application/json; charset=utf-8",
"size": 525,
"text": "[\"1f8b080000000000000395935f6f9b3014c5bf0af2338981aceb8a146dd2fe5493d666d25aa9ca4b64c081bb1adbb20d2945fdeebb06b2647998c41360dddfb987eb7b7a2254\",\"0992a4e49ebbbd8017121228487a13afaede5f8744aa82effc01b9fbf2b5db3cdf74dbe45bc39e7455dc8a36fbfdfc72f7fa98dc1fd66b0459cb1c33bbc608acaf9cd336a5743cb4cb125cd5648de52657d271e996b9aa6943c74e1fdbf53b5428cda431b4c4830b2d0d93ce08a398a527e395abc545f7b1eb507daadb2b21d401d94babff91a77f217435be832ce70b20d453e52a8e7342fb6ffea7c1ba595606a0a7fe8177e3252cceddf0628e9d09413307893e7a6ab856835693d9dc8076a0e42c5bff8028a44cc924bcb2d942085ae4bda15906060041dee27acd2247a2a7da40cbf2ce8fc1f09c438b339daf7681a298eb34c785de9c4dc44f1a1cdfb1a2f6f9db336139e68dd5be70dad530f82ef32556e2fe6a263b92ca4688906418da296298303926f7980c501409a1f261f058f643d9e0963965c3e03313b0574602f363aa19f8a44ebcb2f6d3510afb61410586b34ca0a1a92da893b760a3b90c7ea9c6e43cf82998435d0fb90338376df7f83723ac9b4c40be1bef284da20f21998e8675266974cc1506f3ec0b03337ce5e8c5e17530871e92288e17d1f5225e3d24118aa551bcc5de8d2ece6b926811278b287948927415a7abab2d79fb03b458ebc7f1040000\"]"
},
"cookies": [],
"headers": [
{
"name": "server",
"value": "GitHub.com"
},
{
"name": "date",
"value": "Sat, 19 Feb 2022 05:56:54 GMT"
},
{
"name": "content-type",
"value": "application/json; charset=utf-8"
},
{
"name": "cache-control",
"value": "public, max-age=60, s-maxage=60"
},
{
"name": "vary",
"value": "Accept, Accept-Encoding, Accept, X-Requested-With"
},
{
"name": "etag",
"value": "W/\"e7c5ada389fe724c41d8b2248ede8e368c0fdd159f907ca79d9c0a78fa3344cb\""
},
{
"name": "last-modified",
"value": "Wed, 02 Dec 2020 22:31:35 GMT"
},
{
"name": "x-github-media-type",
"value": "github.v3; format=vnd.github.v3.raw"
},
{
"name": "access-control-expose-headers",
"value": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset"
},
{
"name": "access-control-allow-origin",
"value": "*"
},
{
"name": "strict-transport-security",
"value": "max-age=31536000; includeSubdomains; preload"
},
{
"name": "x-frame-options",
"value": "deny"
},
{
"name": "x-content-type-options",
"value": "nosniff"
},
{
"name": "x-xss-protection",
"value": "0"
},
{
"name": "referrer-policy",
"value": "origin-when-cross-origin, strict-origin-when-cross-origin"
},
{
"name": "content-security-policy",
"value": "default-src 'none'"
},
{
"name": "content-encoding",
"value": "gzip"
},
{
"name": "x-ratelimit-limit",
"value": "60"
},
{
"name": "x-ratelimit-remaining",
"value": "59"
},
{
"name": "x-ratelimit-reset",
"value": "1645253814"
},
{
"name": "x-ratelimit-resource",
"value": "core"
},
{
"name": "x-ratelimit-used",
"value": "1"
},
{
"name": "accept-ranges",
"value": "bytes"
},
{
"name": "content-length",
"value": "525"
},
{
"name": "x-github-request-id",
"value": "F38D:0289:2486BB0:45D7297:621086A6"
},
{
"name": "connection",
"value": "close"
}
],
"headersSize": 1283,
"httpVersion": "HTTP/1.1",
"redirectURL": "",
"status": 200,
"statusText": "OK"
},
"startedDateTime": "2022-02-19T05:56:54.149Z",
"time": 454,
"timings": {
"blocked": -1,
"connect": -1,
"dns": -1,
"receive": 0,
"send": 0,
"ssl": -1,
"wait": 454
}
}
],
"pages": [],
"version": "1.2"
}
}
18 changes: 18 additions & 0 deletions examples/typescript-jest-node-fetch/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Config } from "@jest/types";

const config: Config.InitialOptions = {
rootDir: ".",
preset: "ts-jest",
testEnvironment: "setup-polly-jest/jest-environment-jsdom",
verbose: true,
testPathIgnorePatterns: ["node_modules", "dist"],
resetModules: true,
globals: {
"ts-jest": {
useESM: true,
},
},
transform: { },
};

export default config;
49 changes: 49 additions & 0 deletions examples/typescript-jest-node-fetch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "typescript-jest-node-fetch",
"version": "1.0.0",
"private": true,
"main": "./dist/index.js",
"type": "commonjs",
"exports": "./dist/index.js",
"scripts": {
"test": "jest --runInBand",
"test:record": "POLLY_MODE=record jest --runInBand --verbose"
},
"keywords": [
"pollyjs",
"test-mocking",
"jest",
"node",
"typescript",
"fetch"
],
"author": "",
"license": "Apache-2.0",
"dependencies": {
"node-fetch": "^2.6.6"
},
"devDependencies": {
"@pollyjs/adapter-fetch": "^5.1.1",
"@pollyjs/adapter-node-http": "^5.1.1",
"@pollyjs/core": "^5.1.1",
"@pollyjs/node-server": "^5.1.1",
"@pollyjs/persister-fs": "^5.1.1",
"@types/jest": "^26.0.0",
"@types/node": "^16.11.11",
"@types/node-fetch": "^2.5.12",
"@types/pollyjs__adapter": "^4.3.1",
"@types/pollyjs__adapter-fetch": "^2.0.1",
"@types/pollyjs__adapter-node-http": "^2.0.1",
"@types/pollyjs__core": "^4.3.3",
"@types/pollyjs__persister": "^4.3.1",
"@types/pollyjs__persister-fs": "^2.0.1",
"@types/pollyjs__utils": "^2.6.1",
"@types/setup-polly-jest": "^0.5.1",
"jest": "^26.6.0",
"nodemon": "^2.0.15",
"setup-polly-jest": "^0.10.0",
"ts-jest": "^26.5.6",
"ts-node": "^10.4.0",
"typescript": "^4.5.2"
}
}
31 changes: 31 additions & 0 deletions examples/typescript-jest-node-fetch/src/github-api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/** @jest-environment setup-polly-jest/jest-environment-node */
import autoSetupPolly from './utils/auto-setup-polly';
import { getUser } from './github-api';

describe('github-api client', () => {
let pollyContext = autoSetupPolly();
Copy link

@mcmire mcmire Mar 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm testing this library in our test suite. My feeling is that Polly should be as automatic as possible and devs shouldn't have to think about it (unless they want to mock a request). I found that I could set up Polly without having to add anything manually to tests using this:

// tests/setupTestsAfterEnv.ts

import path from 'path';
import { setupPolly } from 'setup-polly-jest';
import NodeHttpAdapter from '@pollyjs/adapter-node-http';
import FSPersister from '@pollyjs/persister-fs';

global.pollyContext = setupPolly({
  adapters: [NodeHttpAdapter],
  persister: FSPersister,
  persisterOptions: {
    fs: {
      recordingsDir: path.resolve(__dirname, '__recordings__'),
    },
  },
  // logLevel: 'info',
  recordIfMissing: false,
  recordFailedRequests: true,
  expiresIn: '30d',
});

// tests/global.d.ts
// note that you will have to include this in your tsconfig.json somehow (automatically or manually)

import { setupPolly } from 'setup-polly-jest';

declare global {
  /* eslint-disable-next-line @typescript-eslint/no-namespace */
  namespace NodeJS {
    interface Global {
      pollyContext: ReturnType<typeof setupPolly>;
    }
  }
}

// jest.config.js
module.exports = {
  // ...
  setupFilesAfterEnv: ['./tests/setupTestsAfterEnv.ts'],
  // ...
}

This way pollyContext is magically available in any test.

Obviously this is a little bit specific, but what do you think about this approach?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love where you're going @mcmire - I think Polly is 🔥 when it's transparent, so your code doesn't change depending on if you want to mock or not.

I was going to include something similar to this pattern! The reason I didn't is because I think an opt-in design can meet more use cases.
As it still allows you to wire it up in setupFilesAfterEnv scripts:

// `./tests/setupFilesAfterEnv.ts`
import { autoSetupPolly } from './src/utils/auto-setup-polly';

declare global {
  namespace NodeJS {
    interface Global {
      pollyContext: ReturnType<typeof autoSetupPolly>;
    }
  }
}

global.pollyContext = autoSetupPolly();

export default global.pollyContext;

Maybe another example or some comments in the README is warranted?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah that's a fair point. This technique is also useful if you're slowly introducing Polly into a test suite, I'd imagine. In that case, an extra example would be nice, but not necessary.


beforeEach(() => {
// Intercept /ping healthcheck requests (example)
pollyContext.polly.server
.any("/ping")
.intercept((req, res) => void res.sendStatus(200));
});

it('getUser', async () => {
const user: any = await getUser('netflix');
expect(typeof user).toBe('object');
expect(user?.login).toBe('Netflix');
});

it('getUser: custom interceptor', async () => {
expect.assertions(1);
pollyContext.polly.server
.get('https://api.github.com/users/failing_request_trigger')
.intercept((req, res) => void res.sendStatus(500));

await expect(getUser('failing_request_trigger')).rejects.toThrow(
'Http Error: 500'
);
});
});
18 changes: 18 additions & 0 deletions examples/typescript-jest-node-fetch/src/github-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import fetch from "node-fetch";
import type { Response } from "node-fetch";

export const getUser = async (username: string): Promise<unknown> => {
return fetch(`https://api.github.com/users/${username}`, {
headers: {
"Accept": "application/json+vnd.github.v3.raw",
"Content-type": "application/json",
},
})
.then(checkErrorAndReturnJson);
};

function checkErrorAndReturnJson(response: Response) {
return response.ok
? response.json()
: Promise.reject(new Error(`Http Error: ${response.status}`));
}
48 changes: 48 additions & 0 deletions examples/typescript-jest-node-fetch/src/utils/auto-setup-polly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import path from "path";
import { setupPolly } from "setup-polly-jest";
import { Polly, PollyConfig } from "@pollyjs/core";
import NodeHttpAdapter from "@pollyjs/adapter-node-http";
import FSPersister from "@pollyjs/persister-fs";

Polly.register(NodeHttpAdapter);
Polly.register(FSPersister);

let recordIfMissing = true;
let mode: PollyConfig['mode'] = 'replay';

switch (process.env.POLLY_MODE) {
case 'record':
mode = 'record';
break;
case 'replay':
mode = 'replay';
break;
case 'offline':
mode = 'replay';
recordIfMissing = false;
break;
}

export default function autoSetupPolly() {
/**
* This persister can be adapted for both Node.js and Browser environments.
*
* TODO: Customize your config.
*/
return setupPolly({
// 🟡 Note: In node, most `fetch` like libraries use the http/https modules.
// `node-fetch` is handled by `NodeHttpAdapter`, NOT the `FetchAdapter`.
adapters: ["node-http"],
mode,
recordIfMissing,
flushRequestsOnStop: true,
logging: false,
recordFailedRequests: true,
persister: "fs",
persisterOptions: {
fs: {
recordingsDir: path.resolve(__dirname, "../../__recordings__"),
},
},
});
}
Loading