Skip to content

Instantly share code, notes, and snippets.

@alshdavid
Last active June 1, 2023 02:00
Show Gist options
  • Save alshdavid/2ece61b591cade5cad3830e31a3cea0f to your computer and use it in GitHub Desktop.
Save alshdavid/2ece61b591cade5cad3830e31a3cea0f to your computer and use it in GitHub Desktop.

Jest Dynamic Mock

Used to recursively dynamically generate an object where the properties are jest.fn() functions.

Behind the scenes it uses a JavaScript Proxy to detect if a property on the created object is being accessed or being invoked. If it's being invoked it generates a jest.fn() and uses that, if it's being accessed it creates a new mock proxy. This allows for mocks to be generated for objects dynamically and deeply.

import { DynamicMock } from 'jest-dynamic-mock';

const target = new DynamicMock();

// "baz" is automatically converted into a jest.fn() when it's called
target.baz('buzz');
console.log(target.baz.mock.calls[0][0] === 'buzz');

// "foo" is accessed as a property so it becomes another proxy
// "foo.bar" is invoked so it becomes a jest.fn()
target.foo.bar('foobar');
console.log(target.foo.bar.mock.calls[0][0] === 'foobar');

Normally this would be paired with a TypeScript interface

import { DynamicMock } from 'jest-dynamic-mock';

interface Foobar {
    foo(): string;
    bar(): string;
}

const target = new DynamicMock<Foobar>();
target.foo();
target.bar();

This is the expected usage in a jest test

// target.ts
export interface TestFuncOptions {
    bar(): number;
    foo: {
        bar(): number;
    };
}

export function targetFunc(options: TestFuncOptions) {
    const a = options.bar();
    const b = options.foo.bar();
    return a + b;
}
// target.spec.ts
import { DynamicMock } from 'jest-dynamic-mock';
import { targetFunc, TestFuncOptions } from './target';

describe('targetFunc', () => {
    let options: DynamicMock<TestFuncOptions>;

    beforeEach(() => {
        options = new DynamicMock();
    });

    it('Should call functions', async () => {
        targetFunc(options);
        expect(options.bar).toBeCalledTimes(1);
        expect(options.foo.bar).toBeCalledTimes(1);
    });

    it('Should call functions', async () => {
        options.bar.mockReturnValue(40);
        options.foo.bar.mockReturnValue(2);

        const result = targetFunc(options);

        expect(result).toBe(42);
    });
});
/// <reference types="jest" />
export type DynamicMock<T, D extends any[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]> = {
[K in keyof T]: T[K] &
(T[K] extends (...args: infer A) => infer B
? jest.Mock<B, A>
: D extends [any, ...infer R]
? DynamicMock<T[K], R>
: any);
};
declare type AnyObject = {
[key: string | number | symbol]: unknown;
};
declare type DynamicMockConstructor = {
new <T>(base?: AnyObject | Partial<T>): DynamicMock<T>;
extend<T>(constructor: unknown): new <U extends T>() => DynamicMock<U>;
};
export declare const DynamicMock: DynamicMockConstructor;
export {};
const JEST_MOCK_METHODS = Object.freeze({
'getMockName': true,
'mock': true,
'mockClear': true,
'mockReset': true,
'mockRestore': true,
'getMockImplementation': true,
'mockImplementation': true,
'mockImplementationOnce': true,
'mockName': true,
'mockReturnThis': true,
'mockReturnValue': true,
'mockReturnValueOnce': true,
'mockResolvedValue': true,
'mockResolvedValueOnce': true,
'mockRejectedValue': true,
'mockRejectedValueOnce': true,
});
function DynamicMock() {
/** @type {Map<string, jest.Mock>} */
const propCache = new Map();
function internalProxy(
/** @type {string} */ propertyPath,
) {
return new Proxy(function () {}, {
apply: (
/** @type {any} */ target,
/** @type {any} */ thisArg,
/** @type {any[]} */ argumentsList,
) => {
let func = propCache.get(propertyPath);
if (!func) {
func = jest.fn();
propCache.set(propertyPath, func);
}
return func(argumentsList);
},
get: (
/** @type {any} */ target,
/** @type {string} */ key,
) => {
const incomingPropertyPath = `${propertyPath}.${key}`;
if (propCache.has(incomingPropertyPath)) {
return propCache.get(incomingPropertyPath);
}
// @ts-ignore
if (key === Symbol.iterator) {
return target[Symbol.iterator] ?? Reflect.ownKeys(target)[Symbol.iterator];
}
if (key === 'then' || key === 'catch') {
return undefined;
}
if (JEST_MOCK_METHODS[key]) {
const func = jest.fn();
propCache.set(propertyPath, func);
return Reflect.get(func, key);
}
return internalProxy(incomingPropertyPath);
},
set: (
/** @type {any} */ target,
/** @type {string} */ prop,
/** @type {any} */ value,
) => {
const incomingPropertyPath = `${propertyPath}.${prop}`;
propCache.set(incomingPropertyPath, value);
return true;
},
});
}
return internalProxy('');
}
module.exports = {
DynamicMock,
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment