Skip to content

Commit

Permalink
✨(jest) Add support for record-based it.prop (#3366)
Browse files Browse the repository at this point in the history
Related to #3303
  • Loading branch information
dubzzz authored Oct 28, 2022
1 parent 28e38b0 commit bce49c3
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 9 deletions.
2 changes: 2 additions & 0 deletions .yarn/versions/d4bbed25.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
releases:
"@fast-check/jest": minor
109 changes: 100 additions & 9 deletions packages/jest/src/jest-fast-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ type It = typeof itJest;
type ArbitraryTuple<Ts extends [any] | any[]> = {
[P in keyof Ts]: fc.Arbitrary<Ts[P]>;
};
type ArbitraryRecord<Ts> = {
[P in keyof Ts]: fc.Arbitrary<Ts[P]>;
};

type Prop<Ts extends [any] | any[]> = (...args: Ts) => boolean | void | PromiseLike<boolean | void>;
type PropRecord<Ts> = (arg: Ts) => boolean | void | PromiseLike<boolean | void>;

type PromiseProp<Ts extends [any] | any[]> = (...args: Ts) => Promise<boolean | void>;

function wrapProp<Ts extends [any] | any[]>(prop: Prop<Ts>): PromiseProp<Ts> {
Expand Down Expand Up @@ -112,32 +117,118 @@ function internalTestProp(testFn: It) {
}

/**
* Type used for any `{it,test}.*.prop`
* Type used for any `{it,test}.*.prop` taking tuples
*/
type TestProp<Ts extends [any] | any[], TsParameters extends Ts = Ts> = (
type TestPropTuple<Ts extends [any] | any[], TsParameters extends Ts = Ts> = (
arbitraries: ArbitraryTuple<Ts>,
params?: fc.Parameters<TsParameters>
) => (testName: string, prop: Prop<Ts>, timeout?: number | undefined) => void;

/**
* Type used for any `{it,test}.*.prop` taking records
*/
type TestPropRecord<Ts, TsParameters extends Ts = Ts> = (
arbitraries: ArbitraryRecord<Ts>,
params?: fc.Parameters<TsParameters>
) => (testName: string, prop: PropRecord<Ts>, timeout?: number | undefined) => void;

/**
* prop has just been declared for typing reasons, ideally TestProp should be enough
* and should be used to replace `{ prop: typeof prop }` by `{ prop: TestProp<???> }`
*/
declare const prop: <Ts extends [any] | any[], TsParameters extends Ts = Ts>(
arbitraries: ArbitraryTuple<Ts>,
declare const prop: <Ts, TsParameters extends Ts = Ts>(
arbitraries: Ts extends [any] | any[] ? ArbitraryTuple<Ts> : ArbitraryRecord<Ts>,
params?: fc.Parameters<TsParameters>
) => (testName: string, prop: Prop<Ts>, timeout?: number | undefined) => void;
) => (
testName: string,
prop: Ts extends [any] | any[] ? Prop<Ts> : PropRecord<Ts>,
timeout?: number | undefined
) => void;

function adaptParametersForRecord<Ts>(
parameters: fc.Parameters<[Ts]>,
originalParamaters: fc.Parameters<Ts>
): fc.Parameters<Ts> {
return {
...(parameters as Required<fc.Parameters<[Ts]>>),
examples: parameters.examples !== undefined ? parameters.examples.map((example) => example[0]) : undefined,
reporter: originalParamaters.reporter,
asyncReporter: originalParamaters.asyncReporter,
};
}

function adaptExecutionTreeForRecord<Ts>(executionSummary: fc.ExecutionTree<[Ts]>[]): fc.ExecutionTree<Ts>[] {
return executionSummary.map((summary) => ({
...summary,
value: summary.value[0],
children: adaptExecutionTreeForRecord(summary.children),
}));
}

function adaptRunDetailsForRecord<Ts>(
runDetails: fc.RunDetails<[Ts]>,
originalParamaters: fc.Parameters<Ts>
): fc.RunDetails<Ts> {
const adaptedRunDetailsCommon: fc.RunDetailsCommon<Ts> = {
...(runDetails as Required<fc.RunDetailsCommon<[Ts]>>),
counterexample: runDetails.counterexample !== null ? runDetails.counterexample[0] : null,
failures: runDetails.failures.map((failure) => failure[0]),
executionSummary: adaptExecutionTreeForRecord(runDetails.executionSummary),
runConfiguration: adaptParametersForRecord(runDetails.runConfiguration, originalParamaters),
};
return adaptedRunDetailsCommon as fc.RunDetails<Ts>;
}

/**
* Build `{it,test}.*.prop` out of `{it,test}.*`
* @param testFn - The source `{it,test}.*`
*/
function buildTestProp<Ts extends [any] | any[], TsParameters extends Ts = Ts>(
testFn: It | It['only' | 'skip' | 'failing' | 'concurrent'] | It['concurrent']['only' | 'skip' | 'failing']
): TestProp<Ts, TsParameters> {
return (arbitraries: ArbitraryTuple<Ts>, params?: fc.Parameters<TsParameters>) =>
(testName: string, prop: Prop<Ts>, timeout?: number | undefined) =>
internalTestPropExecute(testFn, testName, arbitraries, prop, params, timeout);
): TestPropTuple<Ts, TsParameters>;
function buildTestProp<Ts, TsParameters extends Ts = Ts>(
testFn: It | It['only' | 'skip' | 'failing' | 'concurrent'] | It['concurrent']['only' | 'skip' | 'failing']
): TestPropRecord<Ts, TsParameters>;
function buildTestProp<Ts extends [any] | any[], TsParameters extends Ts = Ts>(
testFn: It | It['only' | 'skip' | 'failing' | 'concurrent'] | It['concurrent']['only' | 'skip' | 'failing']
): TestPropTuple<Ts, TsParameters> | TestPropRecord<Ts, TsParameters> {
return (arbitraries, params?: fc.Parameters<TsParameters>) => {
if (Array.isArray(arbitraries)) {
return (testName: string, prop: Prop<Ts>, timeout?: number | undefined) =>
internalTestPropExecute(testFn, testName, arbitraries, prop, params, timeout);
}
return (testName: string, prop: Prop<Ts>, timeout?: number | undefined) => {
const recordArb = fc.record(arbitraries as ArbitraryRecord<Ts>);
const recordParams: fc.Parameters<[TsParameters]> | undefined =
params !== undefined
? {
// Spreading a "Required" makes us sure that we don't miss any parameters
...(params as Required<fc.Parameters<TsParameters>>),
// Following options needs to be converted to fit with the requirements
examples:
params.examples !== undefined ? params.examples.map((example): [TsParameters] => [example]) : undefined,
reporter:
params.reporter !== undefined
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(runDetails) => params.reporter!(adaptRunDetailsForRecord(runDetails, params))
: undefined,
asyncReporter:
params.asyncReporter !== undefined
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(runDetails) => params.asyncReporter!(adaptRunDetailsForRecord(runDetails, params))
: undefined,
}
: undefined;
internalTestPropExecute(
testFn,
testName,
[recordArb],
(value) => (prop as PropRecord<Ts>)(value),
recordParams,
timeout
);
};
};
}

/**
Expand Down
56 changes: 56 additions & 0 deletions packages/jest/test/jest-fast-check.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,62 @@ describe.each<{ specName: string; runnerName: RunnerType; useLegacySignatures: b
expect(out).toMatch(/[×✕] property fail async \(with seed=-?\d+\)/);
});

if (!useLegacySignatures) {
it.concurrent('should pass on truthy record-based property', async () => {
// Arrange
const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, useLegacySignatures, () => {
runner.prop({ a: fc.string(), b: fc.string(), c: fc.string() })('property pass record', ({ a, b, c }) => {
expect(typeof a).toBe('string');
expect(typeof b).toBe('string');
expect(typeof c).toBe('string');
return `${a}${b}${c}`.includes(b);
});
});

// Act
const out = await runSpec(jestConfigRelativePath);

// Assert
expectPass(out, specFileName);
expect(out).toMatch(/[√✓] property pass record \(with seed=-?\d+\)/);
});

it.concurrent('should fail on falsy record-based property', async () => {
// Arrange
const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, useLegacySignatures, () => {
runner.prop({ a: fc.string(), b: fc.string(), c: fc.string() })('property fail record', ({ a, b, c }) => {
return `${a}${b}${c}`.includes(`${b}!`);
});
});

// Act
const out = await runSpec(jestConfigRelativePath);

// Assert
expectFail(out, specFileName);
expectAlignedSeeds(out);
expect(out).toMatch(/[×✕] property fail record \(with seed=-?\d+\)/);
});

it.concurrent('should fail on falsy record-based property with seed', async () => {
// Arrange
const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, useLegacySignatures, () => {
runner.prop({ a: fc.string(), b: fc.string(), c: fc.string() }, { seed: 4869 })(
'property fail record seeded',
(_unused) => false
);
});

// Act
const out = await runSpec(jestConfigRelativePath);

// Assert
expectFail(out, specFileName);
expectAlignedSeeds(out, { noAlignWithJest: true });
expect(out).toMatch(/[×✕] property fail record seeded \(with seed=4869\)/);
});
}

it.concurrent('should fail with locally requested seed', async () => {
// Arrange
const { specFileName, jestConfigRelativePath } = await writeToFile(runnerName, useLegacySignatures, () => {
Expand Down

0 comments on commit bce49c3

Please sign in to comment.