From 475040962cbb3f42caea76df7a2fd40e159f94d4 Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Thu, 20 Oct 2022 23:09:30 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(jest)=20Add=20new=20`it.prop`=20and?= =?UTF-8?q?=20related?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Related to #3303 --- packages/jest/src/jest-fast-check.ts | 47 +++- packages/jest/test/jest-fast-check.spec.ts | 306 +++++++++++---------- 2 files changed, 201 insertions(+), 152 deletions(-) diff --git a/packages/jest/src/jest-fast-check.ts b/packages/jest/src/jest-fast-check.ts index 0f1d5f80807..94bfb15ba5e 100644 --- a/packages/jest/src/jest-fast-check.ts +++ b/packages/jest/src/jest-fast-check.ts @@ -1,7 +1,8 @@ -import { it, test, jest } from '@jest/globals'; +import { it as itJest, test as testJest, jest } from '@jest/globals'; +import { type Global } from '@jest/types'; import * as fc from 'fast-check'; -type It = typeof it; +type It = Global.ItConcurrent; // Pre-requisite: https://github.com/Microsoft/TypeScript/pull/26063 // Require TypeScript 3.1 @@ -106,6 +107,44 @@ function internalTestProp(testFn: It) { return Object.assign(base, extras); } -export const testProp = internalTestProp(test); -export const itProp = internalTestProp(it); +type TestProp = ( + arbitraries: ArbitraryTuple, + params?: fc.Parameters +) => (testName: string, prop: Prop, timeout?: number | undefined) => void; + +function buildTestProp( + testFn: It | It['only' | 'skip' | 'failing' | 'concurrent'] | It['concurrent']['only' | 'skip' | 'failing'] +): TestProp { + return (arbitraries: ArbitraryTuple, params?: fc.Parameters) => + (testName: string, prop: Prop, _timeout?: number | undefined) => + internalTestPropExecute(testFn, testName, arbitraries, prop, params); +} + +type FastCheckItBuilder = T & + ('each' extends keyof T ? T & { prop: TestProp } : T) & { + [K in keyof Omit]: FastCheckItBuilder; + }; + +function enrichWithTestProp any>(testFn: T): FastCheckItBuilder { + if (typeof testFn !== 'function') { + throw new Error(`Unexpected entry encountered while build {it/test} for @fast-check/jest`); + } + if (Object.keys(testFn).length === 0) { + return testFn as FastCheckItBuilder; + } + const enrichedTestFn = (...args: Parameters): ReturnType => testFn(...args); + const extraKeys: Partial> = {}; + for (const key in testFn) { + extraKeys[key] = key === 'each' ? enrichWithTestProp(testFn[key] as any) : testFn[key]; + } + if ('each' in testFn) { + extraKeys['prop' as keyof typeof extraKeys] = buildTestProp(testFn as any) as any; + } + return Object.assign(enrichedTestFn, extraKeys) as FastCheckItBuilder; +} + +export const test: FastCheckItBuilder = enrichWithTestProp(testJest); +export const it: FastCheckItBuilder = enrichWithTestProp(itJest); +export const testProp = internalTestProp(testJest); +export const itProp = internalTestProp(itJest); export { fc }; diff --git a/packages/jest/test/jest-fast-check.spec.ts b/packages/jest/test/jest-fast-check.spec.ts index a838062c11f..3c5bad3ac07 100644 --- a/packages/jest/test/jest-fast-check.spec.ts +++ b/packages/jest/test/jest-fast-check.spec.ts @@ -5,14 +5,14 @@ import { execFile as _execFile } from 'child_process'; const execFile = promisify(_execFile); import _fc from 'fast-check'; -import { testProp as _testProp, itProp as _itProp } from '@fast-check/jest'; +import { test as _test, it as _it } from '@fast-check/jest'; declare const fc: typeof _fc; -declare const runnerProp: typeof _testProp | typeof _itProp; +declare const runner: typeof _test | typeof _it; const generatedTestsDirectoryName = 'generated-tests'; const generatedTestsDirectory = path.join(__dirname, generatedTestsDirectoryName); -type RunnerType = 'testProp' | 'itProp'; +type RunnerType = 'test' | 'it'; jest.setTimeout(60_000); @@ -23,158 +23,84 @@ afterAll(async () => { await fs.rm(generatedTestsDirectory, { recursive: true }); }); -describe.each<{ runner: RunnerType }>([{ runner: 'testProp' }, { runner: 'itProp' }])('$runner', ({ runner }) => { - it.concurrent('should pass on truthy synchronous property', async () => { - // Arrange - const { specFileName, jestConfigRelativePath } = await writeToFile(runner, () => { - runnerProp('property pass sync', [fc.string(), fc.string(), fc.string()], (a, b, c) => { - return `${a}${b}${c}`.includes(b); - }); - }); - - // Act - const out = await runSpec(jestConfigRelativePath); - - // Assert - expectPass(out, specFileName); - expect(out).toMatch(/[√✓] property pass sync \(with seed=-?\d+\)/); - }); - - it.concurrent('should pass on truthy asynchronous property', async () => { - // Arrange - const { specFileName, jestConfigRelativePath } = await writeToFile(runner, () => { - runnerProp('property pass async', [fc.string(), fc.string(), fc.string()], async (a, b, c) => { - await new Promise((resolve) => setTimeout(resolve, 0)); - return `${a}${b}${c}`.includes(b); +describe.each<{ runnerName: RunnerType }>([{ runnerName: 'test' }, { runnerName: 'it' }])( + '$runner', + ({ runnerName }) => { + it.concurrent('should pass on truthy synchronous property', async () => { + // Arrange + const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, () => { + runner.prop([fc.string(), fc.string(), fc.string()])('property pass sync', (a, b, c) => { + return `${a}${b}${c}`.includes(b as string); + }); }); - }); - - // Act - const out = await runSpec(jestConfigRelativePath); - // Assert - expectPass(out, specFileName); - expect(out).toMatch(/[√✓] property pass async \(with seed=-?\d+\)/); - }); + // Act + const out = await runSpec(jestConfigRelativePath); - it.concurrent('should fail on falsy synchronous property', async () => { - // Arrange - const { specFileName, jestConfigRelativePath } = await writeToFile(runner, () => { - runnerProp('property fail sync', [fc.nat()], (a) => { - return a === 0; - }); + // Assert + expectPass(out, specFileName); + expect(out).toMatch(/[√✓] property pass sync \(with seed=-?\d+\)/); }); - // Act - const out = await runSpec(jestConfigRelativePath); - - // Assert - expectFail(out, specFileName); - expectAlignedSeeds(out); - expect(out).toMatch(/[×✕] property fail sync \(with seed=-?\d+\)/); - }); - - it.concurrent('should fail on falsy asynchronous property', async () => { - // Arrange - const { specFileName, jestConfigRelativePath } = await writeToFile(runner, () => { - runnerProp('property fail async', [fc.nat()], async (a) => { - await new Promise((resolve) => setTimeout(resolve, 0)); - return a === 0; + it.concurrent('should pass on truthy asynchronous property', async () => { + // Arrange + const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, () => { + runner.prop([fc.string(), fc.string(), fc.string()])('property pass async', async (a, b, c) => { + await new Promise((resolve) => setTimeout(resolve, 0)); + return `${a}${b}${c}`.includes(b as string); + }); }); - }); - - // Act - const out = await runSpec(jestConfigRelativePath); - - // Assert - expectFail(out, specFileName); - expectAlignedSeeds(out); - expect(out).toMatch(/[×✕] property fail async \(with seed=-?\d+\)/); - }); - it.concurrent('should fail with locally requested seed', async () => { - // Arrange - const { specFileName, jestConfigRelativePath } = await writeToFile(runner, () => { - runnerProp('property fail with locally requested seed', [fc.constant(null)], (_unused) => false, { - seed: 4242, - }); - }); - - // Act - const out = await runSpec(jestConfigRelativePath); - - // Assert - expectFail(out, specFileName); - expectAlignedSeeds(out, { noAlignWithJest: true }); - expect(out).toMatch(/[×✕] property fail with locally requested seed \(with seed=4242\)/); - }); - - it.concurrent('should fail with globally requested seed', async () => { - // Arrange - const { specFileName, jestConfigRelativePath } = await writeToFile(runner, () => { - fc.configureGlobal({ seed: 4848 }); - runnerProp('property fail with globally requested seed', [fc.constant(null)], (_unused) => false); - }); - - // Act - const out = await runSpec(jestConfigRelativePath); - - // Assert - expectFail(out, specFileName); - expectAlignedSeeds(out, { noAlignWithJest: true }); - expect(out).toMatch(/[×✕] property fail with globally requested seed \(with seed=4848\)/); - }); + // Act + const out = await runSpec(jestConfigRelativePath); - it.concurrent('should fail with seed requested at jest level', async () => { - // Arrange - const { specFileName, jestConfigRelativePath } = await writeToFile(runner, () => { - runnerProp('property fail with globally requested seed', [fc.constant(null)], (_unused) => false); + // Assert + expectPass(out, specFileName); + expect(out).toMatch(/[√✓] property pass async \(with seed=-?\d+\)/); }); - // Act - const out = await runSpec(jestConfigRelativePath, { jestSeed: 6969 }); - - // Assert - expectFail(out, specFileName); - expectAlignedSeeds(out); - expect(out).toMatch(/[×✕] property fail with globally requested seed \(with seed=6969\)/); - }); - - describe('.skip', () => { - it.concurrent('should never be executed', async () => { + it.concurrent('should fail on falsy synchronous property', async () => { // Arrange - const { jestConfigRelativePath } = await writeToFile(runner, () => { - runnerProp.skip('property never executed', [fc.constant(null)], (_unused) => false); + const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, () => { + runner.prop([fc.nat()])('property fail sync', (a) => { + return a === 0; + }); }); // Act const out = await runSpec(jestConfigRelativePath); // Assert - expect(out).toMatch(/Test Suites:\s+1 skipped, 0 of 1 total/); - expect(out).toMatch(/Tests:\s+1 skipped, 1 total/); + expectFail(out, specFileName); + expectAlignedSeeds(out); + expect(out).toMatch(/[×✕] property fail sync \(with seed=-?\d+\)/); }); - }); - describe('.failing', () => { - it.concurrent('should pass because failing', async () => { + it.concurrent('should fail on falsy asynchronous property', async () => { // Arrange - const { specFileName, jestConfigRelativePath } = await writeToFile(runner, () => { - runnerProp.failing('property pass because failing', [fc.constant(null)], async (_unused) => false); + const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, () => { + runner.prop([fc.nat()])('property fail async', async (a) => { + await new Promise((resolve) => setTimeout(resolve, 0)); + return a === 0; + }); }); // Act const out = await runSpec(jestConfigRelativePath); // Assert - expectPass(out, specFileName); - expect(out).toMatch(/[√✓] property pass because failing \(with seed=-?\d+\)/); + expectFail(out, specFileName); + expectAlignedSeeds(out); + expect(out).toMatch(/[×✕] property fail async \(with seed=-?\d+\)/); }); - it.concurrent('should fail because passing', async () => { + it.concurrent('should fail with locally requested seed', async () => { // Arrange - const { specFileName, jestConfigRelativePath } = await writeToFile(runner, () => { - runnerProp.failing('property fail because passing', [fc.constant(null)], async (_unused) => true); + const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, () => { + runner.prop([fc.constant(null)], { seed: 4242 })( + 'property fail with locally requested seed', + (_unused) => false + ); }); // Act @@ -182,45 +108,62 @@ describe.each<{ runner: RunnerType }>([{ runner: 'testProp' }, { runner: 'itProp // Assert expectFail(out, specFileName); - expect(out).toMatch(/[×✕] property fail because passing \(with seed=-?\d+\)/); + expectAlignedSeeds(out, { noAlignWithJest: true }); + expect(out).toMatch(/[×✕] property fail with locally requested seed \(with seed=4242\)/); }); - }); - describe('.concurrent', () => { - it.concurrent('should pass on truthy property', async () => { + it.concurrent('should fail with globally requested seed', async () => { // Arrange - const { specFileName, jestConfigRelativePath } = await writeToFile(runner, () => { - runnerProp.concurrent('property pass on truthy property', [fc.constant(null)], (_unused) => true); + const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, () => { + fc.configureGlobal({ seed: 4848 }); + runner.prop([fc.constant(null)])('property fail with globally requested seed', (_unused) => false); }); // Act const out = await runSpec(jestConfigRelativePath); // Assert - expectPass(out, specFileName); - expect(out).toMatch(/[√✓] property pass on truthy property \(with seed=-?\d+\)/); + expectFail(out, specFileName); + expectAlignedSeeds(out, { noAlignWithJest: true }); + expect(out).toMatch(/[×✕] property fail with globally requested seed \(with seed=4848\)/); }); - it.concurrent('should fail on falsy property', async () => { + it.concurrent('should fail with seed requested at jest level', async () => { // Arrange - const { specFileName, jestConfigRelativePath } = await writeToFile(runner, () => { - runnerProp.concurrent('property fail on falsy property', [fc.constant(null)], (_unused) => false); + const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, () => { + runner.prop([fc.constant(null)])('property fail with globally requested seed', (_unused) => false); }); // Act - const out = await runSpec(jestConfigRelativePath); + const out = await runSpec(jestConfigRelativePath, { jestSeed: 6969 }); // Assert expectFail(out, specFileName); expectAlignedSeeds(out); - expect(out).toMatch(/[×✕] property fail on falsy property \(with seed=-?\d+\)/); + expect(out).toMatch(/[×✕] property fail with globally requested seed \(with seed=6969\)/); + }); + + describe('.skip', () => { + it.concurrent('should never be executed', async () => { + // Arrange + const { jestConfigRelativePath } = await writeToFile(runnerName, () => { + runner.skip.prop([fc.constant(null)])('property never executed', (_unused) => false); + }); + + // Act + const out = await runSpec(jestConfigRelativePath); + + // Assert + expect(out).toMatch(/Test Suites:\s+1 skipped, 0 of 1 total/); + expect(out).toMatch(/Tests:\s+1 skipped, 1 total/); + }); }); describe('.failing', () => { it.concurrent('should pass because failing', async () => { // Arrange - const { specFileName, jestConfigRelativePath } = await writeToFile(runner, () => { - runnerProp.concurrent.failing('property pass because failing', [fc.constant(null)], async (_unused) => false); + const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, () => { + runner.failing.prop([fc.constant(null)])('property pass because failing', async (_unused) => false); }); // Act @@ -233,8 +176,8 @@ describe.each<{ runner: RunnerType }>([{ runner: 'testProp' }, { runner: 'itProp it.concurrent('should fail because passing', async () => { // Arrange - const { specFileName, jestConfigRelativePath } = await writeToFile(runner, () => { - runnerProp.concurrent.failing('property fail because passing', [fc.constant(null)], async (_unused) => true); + const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, () => { + runner.failing.prop([fc.constant(null)])('property fail because passing', async (_unused) => true); }); // Act @@ -245,14 +188,81 @@ describe.each<{ runner: RunnerType }>([{ runner: 'testProp' }, { runner: 'itProp expect(out).toMatch(/[×✕] property fail because passing \(with seed=-?\d+\)/); }); }); - }); -}); + + describe('.concurrent', () => { + it.concurrent('should pass on truthy property', async () => { + // Arrange + const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, () => { + runner.concurrent.prop([fc.constant(null)])('property pass on truthy property', (_unused) => true); + }); + + // Act + const out = await runSpec(jestConfigRelativePath); + + // Assert + expectPass(out, specFileName); + expect(out).toMatch(/[√✓] property pass on truthy property \(with seed=-?\d+\)/); + }); + + it.concurrent('should fail on falsy property', async () => { + // Arrange + const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, () => { + runner.concurrent.prop([fc.constant(null)])('property fail on falsy property', (_unused) => false); + }); + + // Act + const out = await runSpec(jestConfigRelativePath); + + // Assert + expectFail(out, specFileName); + expectAlignedSeeds(out); + expect(out).toMatch(/[×✕] property fail on falsy property \(with seed=-?\d+\)/); + }); + + describe('.failing', () => { + it.concurrent('should pass because failing', async () => { + // Arrange + const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, () => { + runner.concurrent.failing.prop([fc.constant(null)])( + 'property pass because failing', + async (_unused) => false + ); + }); + + // Act + const out = await runSpec(jestConfigRelativePath); + + // Assert + expectPass(out, specFileName); + expect(out).toMatch(/[√✓] property pass because failing \(with seed=-?\d+\)/); + }); + + it.concurrent('should fail because passing', async () => { + // Arrange + const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, () => { + runner.concurrent.failing.prop([fc.constant(null)])( + 'property fail because passing', + async (_unused) => true + ); + }); + + // Act + const out = await runSpec(jestConfigRelativePath); + + // Assert + expectFail(out, specFileName); + expect(out).toMatch(/[×✕] property fail because passing \(with seed=-?\d+\)/); + }); + }); + }); + } +); // Helper let num = -1; async function writeToFile( - runner: 'testProp' | 'itProp', + runner: 'test' | 'it', fileContent: () => void ): Promise<{ specFileName: string; jestConfigRelativePath: string }> { const specFileSeed = Math.random().toString(16).substring(2); @@ -262,12 +272,12 @@ async function writeToFile( const specFilePath = path.join(generatedTestsDirectory, specFileName); const fileContentString = String(fileContent); const wrapInDescribeIfNeeded = - runner === 'itProp' + runner === 'it' ? (testCode: string) => `describe('test suite', () => {\n${testCode}\n});` : (testCode: string) => testCode; const specContent = "const fc = require('fast-check');\n" + - `const {${runner}: runnerProp} = require('@fast-check/jest');\n` + + `const {${runner}: runner} = require('@fast-check/jest');\n` + wrapInDescribeIfNeeded( fileContentString.substring(fileContentString.indexOf('{') + 1, fileContentString.lastIndexOf('}')) );