Skip to content

Instantly share code, notes, and snippets.

@pedro-mass
Last active March 3, 2024 20:05
Show Gist options
  • Save pedro-mass/71f7b2a16e70349d45a8224ea221f36f to your computer and use it in GitHub Desktop.
Save pedro-mass/71f7b2a16e70349d45a8224ea221f36f to your computer and use it in GitHub Desktop.
Test Helpers
// @ts-check
// ref: https://github.com/pedro-mass/learn/blob/b07f49b56802646e4e05137a0ff77219508c29c8/javascript/code-katas/src/test-helpers.js
import React from "react";
import partial from "lodash/partial";
import { Provider } from "react-redux";
import { createStore } from "./redux/store";
import { arrayify } from "./utils/array";
export { createStore };
/**
* Gives provider for the redux store.
*
* Ideally we can migrate to better practices, but we're not there yet:
* https://redux.js.org/usage/writing-tests#setting-up-a-reusable-test-render-function
*
* @param {import("react").PropsWithChildren<{
* store: ReturnType<import("./redux/store").createStore>;
* }>} props
*/
export function StoreProvider({ children, store }) {
store = store ?? createStore();
// @ts-expect-error - doesn't think <Provider /> is valid JSX
return <Provider store={store}>{children}</Provider>;
}
/** @param {{ name?: string; input?: any; arrange?: () => { input?: any } }} testCase */
export const getTestName = (testCase) => {
if (testCase.name) {
return testCase.name;
}
const input = testCase.input ?? testCase.arrange?.().input;
return "input: " + JSON.stringify(input)?.slice(0, 150);
};
/**
* Sets up a describe block, of the function name, with the testCases set up to
* support .only() & .skip()
*
* Usefully primarily for successful cases.
*
* Do normal flow for error cases
*
* @template {any[]} FnArgTypes
* @template FnReturnType
* @param {TestModifiers & {
* fnUnderTest: (...args: [...FnArgTypes]) => FnReturnType;
* assertWithSnapshot?: boolean;
* testCases: TestCase<FnArgTypes, FnReturnType>[];
* } & { name?: string }} input
*/
export const runTestCases = ({
testCases,
fnUnderTest,
name,
assertWithSnapshot,
...config
}) => {
getDescribeFn(config)?.each(testCases)(
`${name ?? fnUnderTest.name}()`,
(testCase) => {
const name = getTestName(/** @type {TestCase} */ (testCase));
const test = getItFn(testCase);
const arranged = testCase.arrange?.();
const input = /** @type {FnArgTypes} */ (
arrayify(testCase.input ?? arranged?.input)
);
const expected = /** @type {FnReturnType} */ (
testCase.expected ?? arranged?.expected
);
const assertStep = testCase.assert ?? arranged?.assert;
test(name, () => {
if (assertWithSnapshot) {
return expect(fnUnderTest(...input)).toMatchSnapshot();
}
if (assertStep) {
return assertStep({
actual: fnUnderTest(...input),
expected,
});
}
expect(fnUnderTest(...input)).toEqual(expected);
});
}
);
};
/**
* @template [FnType=typeof it] Default is `typeof it`
* @param {FnType} fn
* @param {TestModifiers} [config]
* @returns {FnType}
*/
const getTestFn = (fn, config = {}) => {
const command = config?.only ? "only" : config?.skip ? "skip" : undefined;
return command ? fn[command] : fn;
};
const getDescribeFn = partial(getTestFn, describe);
const getItFn = partial(getTestFn, it);
/**
* Filters down to .only(), and removes .skip()
*
* @param {(TestModifiers & import("immer/dist/internal").AnyObject)[]} cases
*/
export const getTestCases = (cases = []) => {
const filteredCases = cases.filter((c) => c.only);
/** @param {typeof cases} _cases */
const removeSkipped = (_cases) => _cases.filter((c) => c.skip !== true);
return filteredCases?.length > 0
? removeSkipped(filteredCases)
: removeSkipped(cases);
};
/** @param {TestCase} testCase */
export function assertWithSnapshot(testCase) {
return {
...testCase,
assert: ({ actual }) => {
expect(actual).toMatchSnapshot();
},
};
}
export function createMockEnv() {
const OLD_ENV = process.env;
const init = () => {
jest.resetModules(); // Most important - it clears the cache
process.env = { ...OLD_ENV }; // Make a copy
};
const reset = () => {
process.env = OLD_ENV; // Restore old environment
};
return { OLD_ENV, init, reset, beforeEach: init, afterEach: reset };
}
// types:
/** @typedef {{ only?: boolean; skip?: boolean }} TestModifiers */
/**
* @template [T=any] Default is `any`
* @typedef {Record<string | number | symbol, T>} AnyObject
*/
/**
* @template [TestCaseInputs=any[]] Default is `any[]`
* @template [TestCaseExpected=any] Default is `any`
* @typedef {TestModifiers & {
* name?: string;
* expected?: TestCaseExpected;
* input?: TestCaseInputs;
* arrange?: () => {
* input?: TestCaseInputs;
* expected?: TestCaseExpected;
* assert?: (input: {
* actual: TestCaseExpected;
* expected: TestCaseExpected;
* testCase?: TestCase<TestCaseInputs, TestCaseExpected>;
* }) => void;
* };
* assert?: (input: {
* actual: TestCaseExpected;
* expected: TestCaseExpected;
* testCase?: TestCase<TestCaseInputs, TestCaseExpected>;
* }) => void;
* }} TestCase
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment