Last active
August 25, 2019 23:30
-
-
Save soaxelbrooke/ed8d3a143815bc193442f37ad83ca317 to your computer and use it in GitHub Desktop.
quickjest.js - A quickcheck-style property-based test wrapper for Jest
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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(); | |
}); | |
} | |
); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 ;)