aboutsummaryrefslogtreecommitdiff
path: root/test/support
diff options
context:
space:
mode:
authorST-DDT <[email protected]>2022-07-22 15:40:07 +0200
committerGitHub <[email protected]>2022-07-22 21:40:07 +0800
commit895799b23e8224eb1b623ef1d0ffd508f101389f (patch)
tree4618f67b04f51a60efabf5e61b6fa1eb6e6cf731 /test/support
parent9ac77b8acf2bfd008883589673e1c8106241046f (diff)
downloadfaker-895799b23e8224eb1b623ef1d0ffd508f101389f.tar.xz
faker-895799b23e8224eb1b623ef1d0ffd508f101389f.zip
test: introduce seededTests factory (#838)
Diffstat (limited to 'test/support')
-rw-r--r--test/support/seededRuns.ts318
1 files changed, 318 insertions, 0 deletions
diff --git a/test/support/seededRuns.ts b/test/support/seededRuns.ts
index 6e6539b7..be61cfad 100644
--- a/test/support/seededRuns.ts
+++ b/test/support/seededRuns.ts
@@ -1 +1,319 @@
+import { describe, describe as vi_describe, expect, it as vi_it } from 'vitest';
+import type { Faker } from '../../src/faker';
+import type { Callable, MethodOf } from '../../src/utils/types';
+
export const seededRuns = [42, 1337, 1211];
+
+/**
+ * A type allowing only the names of faker modules.
+ */
+type FakerModules = {
+ [Key in keyof Faker]: Faker[Key] extends Callable | string | number | number[]
+ ? never
+ : Key extends 'definitions' | 'locales'
+ ? never
+ : Key;
+}[keyof Faker];
+
+/**
+ * Picks only the methods from the given type.
+ */
+type OnlyMethods<T> = Pick<T, MethodOf<T>>;
+
+/**
+ * A Faker type with modules trimmed to only methods.
+ */
+type OnlyMethodsFaker = {
+ [Key in FakerModules]: OnlyMethods<Faker[Key]>;
+};
+
+/**
+ * The type allowing only the names of methods that have exactly zero arguments.
+ */
+type NoArgsMethodOf<ObjectType> = MethodOf<ObjectType> &
+ {
+ [Key in MethodOf<ObjectType, () => unknown>]: ObjectType[Key] extends (
+ arg0: string | number | boolean | Record<string, undefined>,
+ ...args
+ ) => unknown
+ ? Key
+ : never;
+ }[MethodOf<ObjectType, () => unknown>];
+
+/**
+ * Method that prepares seeded tests.
+ *
+ * It ensures that all methods in that module have exactly one test case or block associated to them.
+ * Duplicate calls to `t.it(methodName)` or `t.describe(methodName)` will directly throw an error.
+ * Before the method returns it will check that there are tests for all methods of the module.
+ *
+ * You may add custom vitest's `it` and `describe` blocks both on a module and method level,
+ * however these will be ignored by the completeness checks and you have to call the `setup()` callback yourself.
+ *
+ * @param faker The faker instance to use for the tests.
+ * @param module The name of the faker module to test.
+ * @param factory The factory used to create the seeded tests. Supports both fluent and individual calls.
+ *
+ * @example
+ * seededTests(faker, 'random', (t) => {
+ * t.it('methodWithoutArgs')
+ * .itRepeated('methodWithoutArgs2', 5)
+ * .describe('methodWithArgs3', (t) => {
+ * t.it('noArgs')
+ * .it('withParam1', 1337)
+ * .it('variant1', { min: 0})
+ * .it('variant2', { max: 1337})
+ * .it('variant1And2', { min: 0, max: 1337})
+ * });
+ * })
+ */
+export function seededTests<
+ K extends FakerModules,
+ M extends Record<string, Callable> = OnlyMethodsFaker[K]
+>(
+ faker: Faker,
+ module: K,
+ factory: (tg: TestGenerator<K, M>, setup: () => void) => void
+): void {
+ describe.each(seededRuns)('%s', (seed) => {
+ const testGenerator: TestGenerator<K, M> = new TestGenerator(
+ faker,
+ seed,
+ module
+ );
+ factory(testGenerator, () => testGenerator.setup());
+
+ testGenerator.expectAllMethodsToBeTested();
+ });
+}
+
+/**
+ * Generator for seed based tests.
+ *
+ * The individual methods generate default test blocks, that use test snapshots to verify consistent return values.
+ */
+class TestGenerator<
+ ModuleName extends FakerModules,
+ Module extends Record<string, Callable> = OnlyMethodsFaker[ModuleName]
+> {
+ private readonly tested: Set<MethodOf<Module>> = new Set();
+ private readonly module: Module;
+
+ constructor(
+ private readonly faker: Faker,
+ private readonly seed: number,
+ private readonly moduleName: ModuleName
+ ) {
+ this.module = this.faker[moduleName] as unknown as Module;
+ }
+
+ /**
+ * Ensures that there is only one test block for each method.
+ *
+ * @param method The method name to check.
+ */
+ private expectNotTested(method: MethodOf<Module>): void {
+ expect(
+ this.tested.has(method),
+ `${method} not to be tested yet`
+ ).toBeFalsy();
+ this.tested.add(method);
+ }
+
+ /**
+ * Should never be called from tests.
+ *
+ * Configures the faker instance for the test by resetting the seed.
+ *
+ * This method will automatically be called by the default methods
+ * and should be called at the beginning of custom vitest's `it` blocks.
+ */
+ setup(): void {
+ this.faker.seed(this.seed);
+ this.faker.locale = 'en';
+ }
+
+ /**
+ * Runs the instructions for a vitest's `it` block.
+ *
+ * @param method The method name to call.
+ * @param args The arguments to call it with.
+ * @param repetitions The number of times to call it.
+ */
+ private callAndVerify<MethodName extends MethodOf<Module>>(
+ method: MethodName,
+ args: Parameters<Module[MethodName]>,
+ repetitions: number = 1
+ ): void {
+ this.setup();
+ for (let i = 0; i < repetitions; i++) {
+ const callable = this.module[method];
+ const value = callable(...args);
+ expect(value).toMatchSnapshot();
+ }
+ }
+
+ /**
+ * Permanently ignores this method.
+ *
+ * @param method The name of the method.
+ */
+ skip(method: MethodOf<Module>): this {
+ this.expectNotTested(method);
+ vi_it.skip(method);
+ return this;
+ }
+
+ /**
+ * Temporarily ignores this method. Useful for testing purposes.
+ *
+ * @param method The name of the method.
+ *
+ * @deprecated Implement a proper test.
+ */
+ todo(method: MethodOf<Module>): this {
+ this.expectNotTested(method);
+ vi_it.todo(method);
+ return this;
+ }
+
+ /**
+ * Generates a test for a method without arguments.
+ *
+ * @param method The name of the method.
+ */
+ it<MethodName extends NoArgsMethodOf<Module>>(method: MethodName): this {
+ return this.itRepeated(method, 1);
+ }
+
+ /**
+ * Generates a repeated test for a method without arguments.
+ * The seed is not reset between repetitions.
+ *
+ * @param method The name of the method.
+ * @param repetitions The number of repetitions to run.
+ */
+ itRepeated<MethodName extends NoArgsMethodOf<Module>>(
+ method: MethodName,
+ repetitions: number
+ ): this {
+ this.expectNotTested(method);
+ vi_it(method, () =>
+ this.callAndVerify(
+ method,
+ [] as Parameters<Module[MethodName]>,
+ repetitions
+ )
+ );
+ return this;
+ }
+
+ /**
+ * Generates no argument tests for the given methods.
+ *
+ * @param methods The names of the methods.
+ */
+ itEach<MethodName extends NoArgsMethodOf<Module>>(
+ ...methods: MethodName[]
+ ): this {
+ for (const method of methods) {
+ this.it(method);
+ }
+ return this;
+ }
+
+ /**
+ * Generates a test section for a method.
+ * Useful to cover multiple argument variations.
+ *
+ * @param method The name of the method.
+ * @param factory The factory used to generate the individual tests.
+ */
+ describe<MethodName extends MethodOf<Module>>(
+ method: MethodName,
+ factory: (tester: MethodTester<Module[MethodName]>) => void
+ ): this {
+ this.expectNotTested(method);
+ const callAndVerify: TestGenerator<ModuleName, Module>['callAndVerify'] =
+ this.callAndVerify.bind(this);
+ const tester: MethodTester<Module[MethodName]> = {
+ it(name: string, ...args: Parameters<Module[MethodName]>) {
+ vi_it(name, () => callAndVerify(method, args));
+ return tester;
+ },
+ itRepeated(
+ name: string,
+ repetitions: number,
+ ...args: Parameters<Module[MethodName]>
+ ) {
+ vi_it(name, () => callAndVerify(method, args, repetitions));
+ return tester;
+ },
+ };
+ vi_describe(method, () => {
+ factory(tester);
+ });
+ return this;
+ }
+
+ /**
+ * Generates a test section for multiple methods with a similar signature.
+ * Useful to cover multiple argument variations.
+ *
+ * @param methods The names of the methods to generate the tests for.
+ */
+ describeEach<MethodName extends MethodOf<Module>>(
+ ...methods: MethodName[]
+ ): (factory: (tester: MethodTester<Module[MethodName]>) => void) => this {
+ return (factory) => {
+ for (const method of methods) {
+ this.describe(method, factory);
+ }
+ return this;
+ };
+ }
+
+ /**
+ * Should never be called from tests.
+ *
+ * Checks that all methods in the module have associated tests.
+ * This method will be called automatically at the end of each run.
+ */
+ expectAllMethodsToBeTested(): void {
+ const actual = Array.from(this.tested).sort();
+ const expected = Object.entries(this.module)
+ .filter(([, value]) => typeof value === 'function')
+ .map(([key]) => key)
+ .sort();
+ vi_it('should test all methods', () => {
+ expect(actual).toEqual(expected);
+ });
+ }
+}
+
+/**
+ * Simple interface for a test generator for a given method.
+ */
+interface MethodTester<Method extends Callable> {
+ /**
+ * Generates a test for the method.
+ *
+ * @param name The name of the test case.
+ * @param args The arguments to use in the test.
+ */
+ it(name: string, ...args: Parameters<Method>): this;
+
+ /**
+ * Generates a repeated test for the method.
+ * The seed is not reset between repetitions.
+ *
+ * @param name The name of the test case.
+ * @param repetitions The number of repetitions to run.
+ * @param args The arguments to use in the test.
+ */
+ itRepeated(
+ name: string,
+ repetitions: number,
+ ...args: Parameters<Method>
+ ): this;
+}