Skip to content
This repository has been archived by the owner on May 6, 2024. It is now read-only.

Commit

Permalink
feat: add mockAll
Browse files Browse the repository at this point in the history
affects: @userlike/babel-plugin-joke, @userlike/joke

Add mockAll function that is equivalent to 2-arg variant of \`jest.mock\`.

ISSUES CLOSED: #11
  • Loading branch information
anilanar committed Sep 23, 2020
1 parent 8328bdc commit fa46d77
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 30 deletions.
34 changes: 30 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand All @@ -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";
Expand All @@ -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:
Expand All @@ -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:

Expand Down
76 changes: 62 additions & 14 deletions packages/babel-plugin-joke/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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';
Expand All @@ -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';
Expand All @@ -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\\");
Expand All @@ -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\\"), (() => ({
Expand All @@ -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<string | null | undefined> {
Expand Down
31 changes: 19 additions & 12 deletions packages/babel-plugin-joke/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
}
Expand All @@ -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));
},
},
};
Expand Down Expand Up @@ -111,7 +113,7 @@ function getJokeMockCalls(
function convertMockCalls(
t: typeof import("@babel/types"),
path: NodePath<Program>,
mockExtendType: MockExtendType
mockType: MockType
): (mockRef: NodePath) => void {
return (mockPath): void => {
const callPath = mockPath.parentPath;
Expand Down Expand Up @@ -150,7 +152,7 @@ function convertMockCalls(
t,
moduleName,
moduleImplementation,
mockExtendType
mockType
),
]
)
Expand All @@ -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
)
Expand Down
3 changes: 3 additions & 0 deletions packages/joke/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
9 changes: 9 additions & 0 deletions packages/joke/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ export function mockSome<M, K extends keyof M>(
return unsafeCoerce(mockSafetyNet());
}

export function mockAll<M, K extends keyof M>(
_: Promise<M>,
_impl: () => {
[_K in K]: Mocked<M>[_K];
}
): Omit<M, K> & Pick<Mocked<M>, K> {
return unsafeCoerce(mockSafetyNet());
}

function mockSafetyNet(): unknown {
const safetyObj = {};
const safetyProxy = new Proxy(safetyObj, {
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit fa46d77

Please sign in to comment.