Skip to content

Instantly share code, notes, and snippets.

@davidlaym
Created December 1, 2023 03:16
Show Gist options
  • Save davidlaym/991e281cecdaaa8afb9c30c79be5d6da to your computer and use it in GitHub Desktop.
Save davidlaym/991e281cecdaaa8afb9c30c79be5d6da to your computer and use it in GitHub Desktop.
Micro factory for tests
import { Faker, faker } from '@faker-js/faker';
const globalPropertySymbol = Symbol('factory:factoryMap');
export type FactoryFunctionDefinition<T> = (faker: Faker) => T;
export type FactoryOverride<T> = Partial<T> | ((generated: T) => T);
/**
* Returns a object generated through a factory.
* Factories are defined (or registered) by `registerFactory` method in this same module.
* You can provide optional overrides to customize the generated object.
* This method always returns a new instance of an object (deep-copy), but caches the property values so is
* repeatable in that sense. Returning always a new instance allows for further customization without affecting
* previously generated objects.
*
* If skipCache is true, the factory will ignore the cached properties and will return a totally new generated object.
* Note: at the factory registration stage, the developer can also indicate that the result should not be cached,
* in this case, skipCache has no effect and will always honor the factory registration.
*
* @param {string} factoryName - The name of the factory function to use.
* @param {FactoryOverride<T>} [overrides] - Optional overrides for the generated object. These are applied even if a cached object is used.
* @param {boolean} [skipCache=false] - If true, the factory will create an object with totally different property values.
* @returns {T} The generated object with any optional overrides applied.
*/
export function factory<T>(
factoryName: string,
overrides?: FactoryOverride<T>,
skipCache = false,
): T {
const factoryFunction = getFactoryFunction<T>(factoryName, skipCache);
const factoryProduct = factoryFunction(faker);
if (overrides && typeof overrides === 'function') {
return overrides(factoryProduct);
}
return JSON.parse(
JSON.stringify({
...factoryProduct,
...overrides,
}),
);
}
/**
* Registers a new factory function with a given name and function definition.
*
* @param {string} factoryName - The name of the factory function to register.
* @param {FactoryFunctionDefinition<T>} factoryFunction - The definition of the factory function to register.
* @param {boolean} [cached=true] - Whether to cache the result of the factory function or not. By default it does and can be overridden at request time.
* @throws {Error} If a factory function with the same name has already been defined.
*/
export function registerFactory<T>(
factoryName: string,
factoryFunction: FactoryFunctionDefinition<T>,
cached = true,
) {
const previouslyDefined = factoryMap()[factoryName];
if (previouslyDefined) {
throw new Error(`factory ${factoryName} has already been defined`);
}
factoryMap()[factoryName] = { factory: factoryFunction, cached };
}
const factoryResults: Record<string, any> = {};
/**
* Resets the results cache, for using in beforeEach or afterEach
*
*/
export function resetFactoryGenerationCache() {
Object.keys(factoryResults).forEach((factoryName) => {
delete factoryResults[factoryName];
});
}
function getFactoryFunction<T>(
factoryName: string,
skipCache = false,
): FactoryFunctionDefinition<T> {
if (!factoryMap()[factoryName]) {
throw new Error(`factory ${factoryName} has not been registered`);
}
const factoryDefinition = factoryMap()[factoryName] as {
factory: FactoryFunctionDefinition<T>;
cached: boolean;
};
if (!factoryDefinition.cached || skipCache) {
return factoryDefinition.factory;
}
if (!factoryResults[factoryName]) {
factoryResults[factoryName] = factoryDefinition.factory(faker);
}
return (_: Faker) => factoryResults[factoryName];
}
function factoryMap(): Record<string, { factory: any; cached: boolean }> {
let map = (global as any)[globalPropertySymbol];
if (!map) {
map = {};
(global as any)[globalPropertySymbol] = map;
initialize(); // thow in case of using a jest global setup
}
return map;
}
function initialize() {
// this executes all the `registerFactory` calls.
// but is tied to having this `all-factories` file with all the correct
// exports.
// This could be replaced by a jest global setup file.
require('../all-factories');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment