Skip to content

Commit

Permalink
fix(test): wrap test loops in assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
ssube committed Oct 18, 2022
1 parent 796572a commit 2370fb2
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 186 deletions.
10 changes: 8 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,19 @@ export interface ErrorParameters<T> extends Parameters<T> {
* @param strategy a fast-check `Arbitrary` used to produce semi-random values
* @param suite the test suite, as you would pass to `describe(name, suite)`
* @param parameters additional parameters to the fast-check `Runner`
* @param tester the test execution callback, typically Mocha's `it`
* @throws Error when the property check fails
* @public
*/
export function over<T>(suiteName: string, strategy: Arbitrary<T>, suite: Suite<T>, parameters: ErrorParameters<T> = {}): void {
export function over<T>(
suiteName: string,
strategy: Arbitrary<T>,
suite: Suite<T>,
parameters: ErrorParameters<T> = {},
tester: Mocha.TestFunction = it): void {
describe(suiteName, () => {
suite((name, test) => {
it(name, function (this: Mocha.Context): Promise<void> {
tester(name, function (this: Mocha.Context): Promise<void> {
const ctx = this;
// something about check's type signature requires examples to be tuples,
// which leads to triple-wrapping examples for tuple properties. help remove one layer
Expand Down
150 changes: 95 additions & 55 deletions test/TestOver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,80 +3,120 @@ import { array, defaultReportMessage, integer, lorem, tuple, uuid } from 'fast-c
import { describe } from 'mocha';

import { over } from '../src/index.js';
import { itFails, itPasses } from './helpers.js';

const LARGE_VALUE = Math.floor(Math.random() * 1_000_000_000);

describe('example properties', () => {
over('some numbers', integer(), (it) => {
it('should be a small number', (n: number) => {
return n < LARGE_VALUE;
});
it('number arbitrary', (done) => {
over('some numbers', integer(), (it) => {
it('should be a small number', (n: number) => {
return n < LARGE_VALUE;
});
}, {}, itFails('by returning false', done));
});

it('another number arbitrary', (done) => {
over('some numbers', integer(), (it) => {
it('should be even', (n: number) => {
return n % 2 === 0;
});
}, {}, itFails('by returning false', done));
});

it('should be even', (n: number) => {
return n % 2 === 0;
});
it('yet another number arbitrary', (done) => {
over('some numbers', integer(), (it) => {
it('should not throw', (n: number) => {
throw new Error('bad number!');
});
}, {}, itFails('by throwing an error', done));
});

it('should not throw', (n: number) => {
throw new Error('bad number!');
});

it('should resolve async checks', async (n: number) => {
expect(n).to.be.lessThanOrEqual(90);
});
it('many number arbitrary', (done) => {
over('some numbers', integer(), (it) => {
it('should resolve async checks', async (n: number) => {
expect(n).to.be.lessThanOrEqual(90);
});
}, {}, itFails('by throwing an error', done));
});

over('some IDs', uuid(), (it) => {
// beforeEach hooks work normally, since the wrapped it calls through to real it
beforeEach(() => {
console.log('before each ID test');
});
it('UUID arbitrary', (done) => {
over('some IDs', uuid(), (it) => {
// beforeEach hooks work normally, since the wrapped it calls through to real it
beforeEach(() => {
console.log('before each ID test');
});

it('should be a good one', (id: string) => {
return id[9] !== 'a';
});
it('should be long enough', (id: string) => {
expect(id).to.have.length.greaterThan(2);
});
}, {
// fast-check parameters are supported, like examples
// examples: ['a', 'b'],
numRuns: 1_000,
}, itPasses(done));
});

it('should be long enough', (id: string) => {
expect(id).to.have.length.greaterThan(2);
});
}, {
// fast-check parameters are supported, like examples
examples: ['a', 'b'],
numRuns: 1_000,
it('UUID arbitrary failure', (done) => {
over('some IDs', uuid(), (it) => {
it('should be a good one', (id: string) => {
return id[9] !== 'a';
});
}, {
// fast-check parameters are supported, like examples
examples: ['a', 'b'],
numRuns: 1_000,
}, itFails('by returning false', done));
});

over('even numbers', integer().filter(n => n % 2 === 0), (it) => {
it('should be even', (n: number) => {
return n % 2 === 0;
});
it('even numbers', (done) => {
over('even numbers', integer().filter(n => n % 2 === 0), (it) => {
it('should be even', (n: number) => {
return n % 2 === 0;
});
}, {}, itPasses(done));
});

over('mapped properties', tuple(lorem(), integer()).map(([a, b]) => a.substr(b)), (it) => {
it('should have content', (text: string) => {
return text.length > 0;
});
}, {
// error formatting can be overridden with a custom handler, or fast-check's default
errorReporter: defaultReportMessage,
it('mapped properties', (done) => {
over('mapped properties', tuple(lorem(), integer()).map(([a, b]) => a.substr(b)), (it) => {
it('should have content', (text: string) => {
return text.length > 0;
});
}, {
// error formatting can be overridden with a custom handler, or fast-check's default
errorReporter: defaultReportMessage,
}, itFails('after 1 tests', done));
});

over('tuples', tuple(integer(), integer()), (it) => {
// tuple properties are passed as a single parameter
it('should not be equal', ([a, b]) => {
return a === b;
});
it('equal tuples', (done) => {
over('tuples', tuple(integer(), integer()), (it) => {
// tuple properties are passed as a single parameter
it('should not be equal', ([a, b]) => {
return a === b;
});
}, {
examples: [[1, 2]]
}, itFails('by returning false', done));
});

it('should be uneven', ([a, b]) => {
return a < b;
});
}, {
examples: [[1, 2]]
it('uneven tuples', (done) => {
over('tuples', tuple(integer(), integer()), (it) => {
it('should be uneven', ([a, b]) => {
return a < b;
});
}, {
examples: [[1, 2]]
}, itFails('by returning false', done));
});

over('arrays', array(integer()), (it) => {
it('should have items', (t: Array<number>) => {
expect(t).to.have.length.lessThan(5_000);
});
}, {
numRuns: 1_000,
it('arrays', (done) => {
over('arrays', array(integer()), (it) => {
it('should have items', (t: Array<number>) => {
expect(t).to.have.length.lessThan(5_000);
});
}, {
numRuns: 1_000,
}, itPasses(done));
});
});
37 changes: 37 additions & 0 deletions test/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect } from 'chai';

export function itFails(msg: string, done: Mocha.Done, count = 1): Mocha.TestFunction {
const expectations: Array<Chai.PromisedAssertion> = [];

const mockIt = function (this: Mocha.Context, name: string, test: Mocha.AsyncFunc) {
console.log('mock it invoked with test', name, test.name, expectations.length);

expectations.push(expect(test.call(this), name).to.eventually.be.rejectedWith(Error, `Property failed ${msg}`));

if (expectations.length >= count) {
console.log('all expectations queued for', name, expectations.length);

Promise.all(expectations).then((_val) => done(), (err) => done(err));
}
};

return (mockIt as unknown as Mocha.TestFunction);
}

export function itPasses(done: Mocha.Done, count = 1): Mocha.TestFunction {
const expectations: Array<Chai.PromisedAssertion> = [];

const mockIt = function (this: Mocha.Context, name: string, test: Mocha.AsyncFunc) {
console.log('mock it invoked with test', name, test.name, expectations.length);

expectations.push(expect(test.call(this), name).to.eventually.be.oneOf([true, undefined]));

if (expectations.length >= count) {
console.log('all expectations queued for', name, expectations.length);

Promise.all(expectations).then((_val) => done(), (err) => done(err));
}
};

return (mockIt as unknown as Mocha.TestFunction);
}

0 comments on commit 2370fb2

Please sign in to comment.