Created
December 1, 2023 03:16
-
-
Save davidlaym/991e281cecdaaa8afb9c30c79be5d6da to your computer and use it in GitHub Desktop.
Micro factory for tests
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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