Skip to content

Instantly share code, notes, and snippets.

@soaxelbrooke
Last active August 25, 2019 23:30
Show Gist options
  • Save soaxelbrooke/ed8d3a143815bc193442f37ad83ca317 to your computer and use it in GitHub Desktop.
Save soaxelbrooke/ed8d3a143815bc193442f37ad83ca317 to your computer and use it in GitHub Desktop.
quickjest.js - A quickcheck-style property-based test wrapper for Jest
// A prototype-based test wrapper based on generators.
// See original Haskell quickcheck paper: http://www.cs.tufts.edu/~nr/cs257/archive/john-hughes/quick.pdf
// --------------------------- //
// Scroll to bottom for usage! //
// --------------------------- //
import R from 'ramda';
const RUNS_PER_TEST = 50;
function quickcheck(description, dataGenerators, testFn) {
let testCases = generate(dataGenerators);
test(description, () => {
return Promise.all(testCases.map(testCase => testFn.apply(this, testCase)));
});
}
function generate(generatorFactories) {
let generators = generatorFactories.map(gen => gen());
return R.range(0, RUNS_PER_TEST)
.map(idx => generators.map(gen => gen.next().value));
}
class Generate {
static floatWithin(min, max) {
return function*() {
let range = max - min;
yield min;
yield max;
while (true) {
yield range * Math.random() + min;
}
};
}
static intWithin(min, max) {
let floatGenerator = Generate.floatWithin(min, max)();
return function*() {
for (let number of floatGenerator) {
yield Math.floor(number);
}
};
}
static genRandomStringOfLen(length) {
let str = '';
R.range(0, length).forEach(_ => {str += String.fromCharCode(Math.random()*0xffffff)});
return str;
}
static stringOfLen(minLen, maxLen) {
let lengthGen = this.intWithin(minLen, maxLen);
return function*() {
for (let length of lengthGen()) {
yield Generate.genRandomStringOfLen(length);
}
};
}
static arrayOfLen(generatorFactory, minLen, maxLen) {
let lengthGen = this.intWithin(minLen, maxLen);
return function*() {
for (let length of lengthGen()) {
yield genTake(length, generatorFactory());
}
};
}
static oneOf(list) {
return function*() {
while (true) {
for (let item of shuffle(list)) {
yield item;
}
}
};
}
}
function shuffle(list) {
let a = R.clone(list);
for (let i = a.length; i; i--) {
let j = Math.floor(Math.random() * i);
[a[i - 1], a[j]] = [a[j], a[i - 1]];
}
return a;
}
function genTake(n, generator) {
return R.range(0, n).map(idx => generator.next().value);
}
export { quickcheck, Generate };
// EXAMPLE USAGE:
function ones(length) {
return R.range(0, length).map(_ => 1);
}
quickcheck(
'should be able to build list',
[Generate.intWithin(0, 1000)],
(length) => {
let result = ones(length);
expect(result.length).toBe(length);
expect(R.sum(result)).toBe(length);
result.forEach(num => expect(num).toBe(1));
}
);
// It even does promises properly! (looking at you, testcheck-js >_>)
quickcheck(
'should fail in a returned rejected promise',
[Generate.intWithin(-100, 1)],
(draw) => {
return draw > 0 ? Promise.resolve('yay') : Promise.reject('ruh-roh');
}
);
// Custom data generators are a breeze, just pass in a generator!
let personGenerator = function*() {
let nameGenerator = Generate.stringOfLen(1, 100)();
let ageGenerator = Generate.intWithin(18, 93)();
while (true) {
yield {
age: ageGenerator.next().value,
name: nameGenerator.next().value,
};
}
};
quickcheck(
'people should like have names and stuff',
[Generate.arrayOfLen(personGenerator, 1, 20)],
(people) => {
people.forEach((person) => {
expect(person.name).not.toBeUndefined();
expect(person.age).not.toBeUndefined();
});
}
);
@dubzzz
Copy link

dubzzz commented Dec 25, 2018

Why not relying on frameworks like:

They all come with generators and shrinking capabilities. IMO shrinker is more than helpful in property based testing :)

Anyway the snippet is a good start for a property based testing tool. It misses shrinker but it is not so hard to code a simple one ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment