From fa46d774d1a908e6e849f83d7285b6676887340d Mon Sep 17 00:00:00 2001 From: Anil Anar Date: Wed, 23 Sep 2020 00:48:42 +0200 Subject: [PATCH] feat: add mockAll affects: @userlike/babel-plugin-joke, @userlike/joke Add mockAll function that is equivalent to 2-arg variant of \`jest.mock\`. ISSUES CLOSED: #11 --- README.md | 34 +++++++-- packages/babel-plugin-joke/src/index.test.ts | 76 ++++++++++++++++---- packages/babel-plugin-joke/src/index.ts | 31 ++++---- packages/joke/package.json | 3 + packages/joke/src/index.ts | 9 +++ yarn.lock | 8 +++ 6 files changed, 131 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 1bf651c..8d4c3ea 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ fetchUser.mockReturnValue(Promise.resolve({ id: 1, name: "Jane Doe" })); ## Full mocking -Mock the whole module. +Auto-mock the whole module. ```typescript import { mock } from "@userlike/joke"; @@ -53,7 +53,7 @@ fetchUser.mockReturnValue(Promise.resolve({ id: 1, name: "Jane Doe" })); ### Full mocking with partial implementation -Use the second argument of `mock` to provide some implementation. +Use the second argument of `mock` to provide a partial implementation. Behind the scenes, it extends auto-mocked module with the given implementation using `Object.assign`. ```typescript import { mock } from "@userlike/joke"; @@ -67,7 +67,7 @@ const { fetchUser } = mock(import("./service"), () => ({ ## Partial mocking -When you need to mock a module partially, but to keep the rest of the module unmocked, you can use `mockSome`. +When you need to mock a module partially and to keep the rest of the module unmocked, you can use `mockSome`. Behind the scenes, it uses `jest.requireActual`; extends actual implementation with the given implementation using `Object.assign`. ```typescript import { mockSome } from "@userlike/joke"; @@ -88,6 +88,32 @@ test(async () => { }); ``` +--- + +## Full replacement + +When you want to skip auto-mocking, you can use `mockAll`. It's equivalent to `jest.mock(module, moduleFactory)`. + +```typescript +import { mockAll } from "@userlike/joke"; +import { renderUser } from "./my-component"; + +const { fetchUser } = mockAll(import("./service"), () => ({ + fetchUser: jest.fn() +})); + +test(async () => { + const user = { id: 1, name: "Jane Doe" }; + fetchUser.mockReturnValue(Promise.resolve(user)); + + await renderUser(); + + expect(document.getElementById("#user-id").innerText).toBe(getUserId(user)); +}); +``` + +--- + ## Usage with `ts-jest` If you use [`ts-jest`](https://www.npmjs.com/package/ts-jest) instead of Babel, you need to additionally ensure each of the following: @@ -111,7 +137,7 @@ Example Typescript configuration for tests: } ``` -To enable Babel preprocessing in `ts-jest`, as well as to configure the `tsconfig` file you want use for tests, add or update the `globals` section in your jest config. +To enable Babel preprocessing in `ts-jest`, as well as to configure the `tsconfig` file you want use for tests, add or update the `globals` section in your jest config. Example with separate Babel and Typescript configuration files: diff --git a/packages/babel-plugin-joke/src/index.test.ts b/packages/babel-plugin-joke/src/index.test.ts index 5d41689..b991042 100644 --- a/packages/babel-plugin-joke/src/index.test.ts +++ b/packages/babel-plugin-joke/src/index.test.ts @@ -43,6 +43,7 @@ it("handles mock import as a namespace", async () => { import * as M from '@userlike/joke'; const { foo } = M.mock(import('foobar')); `); + expect(result).toMatchInlineSnapshot(` "import * as _foobar from \\"foobar\\"; import * as M from '@userlike/joke'; @@ -59,6 +60,7 @@ it("handles assigning return value to a namespace variable", async () => { const F = mock(import('foobar')); F.foo.mockReturnValue(5); `); + expect(result).toMatchInlineSnapshot(` "import * as _foobar from \\"foobar\\"; import { mock } from '@userlike/joke'; @@ -74,6 +76,7 @@ it("handles member expressions", async () => { const bar = mock(import('foobar')).foo.bar; bar.mockReturnValue(5); `); + expect(result).toMatchInlineSnapshot(` "import * as _foobar from \\"foobar\\"; import { mock } from '@userlike/joke'; @@ -88,6 +91,7 @@ it("handles just a call expression", async () => { import { mock } from '@userlike/joke'; mock(import('foobar')); `); + expect(result).toMatchInlineSnapshot(` "import * as _foobar from \\"foobar\\"; import { mock } from '@userlike/joke'; @@ -97,24 +101,24 @@ it("handles just a call expression", async () => { }); it("throws error if mock is called inside closures", async () => { - const result = assert(` + const promise = assert(` import { mock } from '@userlike/joke'; beforeEach(() => { const { foo } = mock(import('foo')); }); `); - expect(result).rejects.toMatchInlineSnapshot( + await expect(promise).rejects.toMatchInlineSnapshot( `[Error: /example.ts: Can only use \`mock\` at the top-level scope.]` ); }); it("works with rest params", async () => { - const promise = assert(` + const result = await assert(` import { mock } from '@userlike/joke'; const { foo, ...bar } = mock(import('foobar')); `); - expect(promise).resolves.toMatchInlineSnapshot(` + expect(result).toMatchInlineSnapshot(` "import * as _foobar from \\"foobar\\"; import { mock } from '@userlike/joke'; jest.mock(\\"foobar\\"); @@ -126,14 +130,14 @@ it("works with rest params", async () => { }); it("allows custom module implementation to be passed", async () => { - const promise = assert(` + const result = await assert(` import { mock } from '@userlike/joke'; const { foo } = mock(import('foobar'), () => ({ foo: 5 })); `); - expect(promise).resolves.toMatchInlineSnapshot(` + expect(result).toMatchInlineSnapshot(` "import * as _foobar from \\"foobar\\"; import { mock } from '@userlike/joke'; jest.mock(\\"foobar\\", () => global.Object.assign({}, jest.genMockFromModule(\\"foobar\\"), (() => ({ @@ -151,18 +155,62 @@ it("throws a sensible error on invalid usage", async () => { mock('foo'); `); - expect(promise).rejects.toMatchInlineSnapshot(` - [Error: /example.ts: - \`mock\` must be used like: + await expect(promise).rejects.toMatchInlineSnapshot(` + [Error: /example.ts: + \`mock\` must be used like: - mock(import('moduleName')) + mock(import('moduleName')) - Instead saw: + Instead saw: - mock('foo') + mock('foo') - ] - `); + ] + `); +}); + +describe("mockSome", () => { + it("extends requireActual'ed original impl with provided mock", async () => { + const result = await assert(` + import { mockSome } from '@userlike/joke'; + const { bar } = mockSome(import('foo'), () => ({ + bar: jest.fn() + })); + `); + + expect(result).toMatchInlineSnapshot(` + "import * as _foo from \\"foo\\"; + import { mockSome } from '@userlike/joke'; + jest.mock(\\"foo\\", () => global.Object.assign({}, jest.requireActual(\\"foo\\"), (() => ({ + bar: jest.fn() + }))())); + const { + bar + } = _foo;" + `); + }); +}); + +describe("mockAll", () => { + it("uses plain jest.mock with no extends", async () => { + const result = await assert(` + import { mockAll } from '@userlike/joke'; + const { bar } = mockAll(import('foo'), () => ({ + bar: jest.fn() + })); + `); + + expect(result).toMatchInlineSnapshot(` + "import * as _foo from \\"foo\\"; + import { mockAll } from '@userlike/joke'; + jest.mock(\\"foo\\", () => ({ + bar: jest.fn() + })); + const { + bar + } = _foo;" + `); + }); }); async function assert(code: string): Promise { diff --git a/packages/babel-plugin-joke/src/index.ts b/packages/babel-plugin-joke/src/index.ts index 0551457..365986e 100644 --- a/packages/babel-plugin-joke/src/index.ts +++ b/packages/babel-plugin-joke/src/index.ts @@ -11,11 +11,13 @@ import { JSXNamespacedName, SpreadElement, ArrowFunctionExpression, + FunctionExpression, } from "@babel/types"; const JOKE_MODULE = "@userlike/joke"; const JOKE_MOCK = "mock"; const JOKE_MOCK_SOME = "mockSome"; +const JOKE_MOCK_ALL = "mockAll"; const JEST = "jest"; const JEST_MOCK = "mock"; const JEST_GEN_MOCK_FROM_MODULE = "genMockFromModule"; @@ -24,7 +26,8 @@ const JEST_REQUIRE_ACTUAL = "requireActual"; type B = typeof import("@babel/core"); type T = B["types"]; -enum MockExtendType { +enum MockType { + MockAll, ExtendMocked, ExtendActual, } @@ -35,14 +38,13 @@ export default function UserlikeJoke({ types: t }: B): PluginObj { visitor: { Program(path): void { const mockCalls = getJokeMockCalls(t, path, JOKE_MOCK); - mockCalls.forEach( - convertMockCalls(t, path, MockExtendType.ExtendMocked) - ); + mockCalls.forEach(convertMockCalls(t, path, MockType.ExtendMocked)); const mockSomeCalls = getJokeMockCalls(t, path, JOKE_MOCK_SOME); - mockSomeCalls.forEach( - convertMockCalls(t, path, MockExtendType.ExtendActual) - ); + mockSomeCalls.forEach(convertMockCalls(t, path, MockType.ExtendActual)); + + const mockAllCalls = getJokeMockCalls(t, path, JOKE_MOCK_ALL); + mockAllCalls.forEach(convertMockCalls(t, path, MockType.MockAll)); }, }, }; @@ -111,7 +113,7 @@ function getJokeMockCalls( function convertMockCalls( t: typeof import("@babel/types"), path: NodePath, - mockExtendType: MockExtendType + mockType: MockType ): (mockRef: NodePath) => void { return (mockPath): void => { const callPath = mockPath.parentPath; @@ -150,7 +152,7 @@ function convertMockCalls( t, moduleName, moduleImplementation, - mockExtendType + mockType ), ] ) @@ -172,18 +174,23 @@ function mockImplementation( t: T, moduleName: string, impl: Expression | SpreadElement | JSXNamespacedName | ArgumentPlaceholder, - mockType: MockExtendType -): ArrowFunctionExpression { + mockType: MockType +): FunctionExpression | ArrowFunctionExpression { invariant( t.isFunctionExpression(impl) || t.isArrowFunctionExpression(impl), `Unexpected second argument to \`mock\` of type ${impl.type}, expected FunctionExpression of ArrowFunctionExpression.` ); + + if (mockType === MockType.MockAll) { + return impl; + } + const iife = t.callExpression(impl, []); const requireMock = t.callExpression( t.memberExpression( t.identifier(JEST), t.identifier( - mockType === MockExtendType.ExtendMocked + mockType === MockType.ExtendMocked ? JEST_GEN_MOCK_FROM_MODULE : JEST_REQUIRE_ACTUAL ) diff --git a/packages/joke/package.json b/packages/joke/package.json index b5eaea8..e16f9a6 100644 --- a/packages/joke/package.json +++ b/packages/joke/package.json @@ -24,6 +24,9 @@ "lint": "eslint src --ext .ts", "test": "exit 0" }, + "devDependencies": { + "@types/jest": "^26.0.14" + }, "peerDependencies": { "@types/jest": ">=15.0.0 <=26" }, diff --git a/packages/joke/src/index.ts b/packages/joke/src/index.ts index aab6b0c..f5c0e7c 100644 --- a/packages/joke/src/index.ts +++ b/packages/joke/src/index.ts @@ -28,6 +28,15 @@ export function mockSome( return unsafeCoerce(mockSafetyNet()); } +export function mockAll( + _: Promise, + _impl: () => { + [_K in K]: Mocked[_K]; + } +): Omit & Pick, K> { + return unsafeCoerce(mockSafetyNet()); +} + function mockSafetyNet(): unknown { const safetyObj = {}; const safetyProxy = new Proxy(safetyObj, { diff --git a/yarn.lock b/yarn.lock index 18c4827..fbc2b34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2364,6 +2364,14 @@ jest-diff "^25.2.1" pretty-format "^25.2.1" +"@types/jest@^26.0.14": + version "26.0.14" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.14.tgz#078695f8f65cb55c5a98450d65083b2b73e5a3f3" + integrity sha512-Hz5q8Vu0D288x3iWXePSn53W7hAjP0H7EQ6QvDO9c7t46mR0lNOLlfuwQ+JkVxuhygHzlzPX+0jKdA3ZgSh+Vg== + dependencies: + jest-diff "^25.2.1" + pretty-format "^25.2.1" + "@types/json-schema@^7.0.3": version "7.0.4" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"