What is fast-check?
fast-check is a property-based testing library for JavaScript and TypeScript. It allows developers to define properties that should hold true for a wide range of inputs, and then automatically generates test cases to verify those properties. This helps in identifying edge cases and ensuring the robustness of the code.
What are fast-check's main functionalities?
Property-based Testing
This feature allows you to define properties that should hold true for a wide range of inputs. In this example, the property being tested is the commutativity of addition.
const fc = require('fast-check');
fc.assert(
fc.property(fc.integer(), fc.integer(), (a, b) => {
return a + b === b + a;
})
);
Custom Arbitraries
Custom arbitraries allow you to define complex data structures for your tests. In this example, a custom arbitrary is created that generates objects with a number and a string.
const fc = require('fast-check');
const myArbitrary = fc.tuple(fc.integer(), fc.string()).map(([num, str]) => ({ num, str }));
fc.assert(
fc.property(myArbitrary, ({ num, str }) => {
return typeof num === 'number' && typeof str === 'string';
})
);
Shrinkable Values
Shrinkable values help in minimizing the size of failing test cases to make debugging easier. In this example, if the property fails, fast-check will try to find the smallest array that causes the failure.
const fc = require('fast-check');
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
return arr.length < 100;
}),
{ verbose: true }
);
Other packages similar to fast-check
jsverify
jsverify is another property-based testing library for JavaScript. It offers similar functionality to fast-check, such as defining properties and generating test cases. However, fast-check is generally considered to have a more modern API and better TypeScript support.
testcheck
testcheck is a property-based testing library inspired by QuickCheck. It provides similar capabilities for generating test cases and defining properties. Compared to fast-check, testcheck is less actively maintained and has fewer features.
hypothesis
Hypothesis is a property-based testing library for Python, but it has inspired several JavaScript libraries, including fast-check. While not a direct competitor, it offers similar concepts and is often used as a reference for property-based testing.
Property based testing framework for JavaScript/TypeScript
Getting started
Property based testing frameworks check the truthfulness of properties. A property is a statement like: for all (x, y, ...) such as precondition(x, y, ...) holds property(x, y, ...) is true.
Install the module with: npm install fast-check --save-dev
Example of integration in mocha:
const fc = require('fast-check');
const contains = (text, pattern) => text.indexOf(pattern) >= 0;
describe('properties', () => {
it('should always contain itself', () => {
fc.assert(fc.property(fc.string(), text => contains(text, text)));
});
it('should always contain its substrings', () => {
fc.assert(fc.property(fc.string(), fc.string(), fc.string(), (a,b,c) => contains(a+b+c, b)));
});
});
In case of failure, the test raises a red flag. Its output should help you to diagnose what went wrong in your implementation. Example with a failing implementation of contain:
1) should always contain its substrings
Error: Property failed after 1 tests (seed: 1527422598337, path: 0:0): ["","",""]
Shrunk 1 time(s)
Got error: Property failed by returning false
Hint: Enable verbose mode in order to have the list of all failing values encountered during the run
Integration with other test frameworks:
ava,
jasmine,
jest,
mocha
and
tape.
More examples:
simple examples,
fuzzing
and
against various algorithms.
Useful documentations:
In a web-page
In order to use fast-check from a web-page (for instance with QUnit or other testing tools), you have to reference the web-aware script as follow:
<script src="https://cdn.jsdelivr.net/npm/fast-check/lib/bundle.js"></script>
You can also reference a precise version by setting the version you want in the url:
<script src="https://cdn.jsdelivr.net/npm/fast-check@0.0.11/lib/bundle.js"></script>
Once it has been included, fast-check becomes accessible directly by calling fastcheck
(in window.fastcheck
). I highly recommend you to alias it by fc
whenever possible by running const fc = fastcheck
at the beginning of the scripts using it.
Why should I migrate to fast-check?
fast-check has initially be designed in an attempt to cope with limitations I encountered while using other property based testing frameworks designed for JavaScript:
- strong and up-to-date types - thanks to TypeScript
- ability to shrink on
fc.oneof
- surprisingly some frameworks don't - easy
map
method to derive existing arbitraries while keeping shrink - some frameworks ask the user to provide both a->b and b->a mappings in order to keep a shrinker - kind of flatMap-operation called
chain
- able to bind the output of an arbitrary as input of another one while keeping the shrink working - biased by default - by default it generates both small and large values, making it easier to dig into counterexamples without having to tweak a size parameter manually
- verbose mode - easier troubleshooting with verbose mode enabled
- replay directly on the minimal counterexample - no need to replay the whole sequence, you get directly the counterexample
For more details, refer to the documentation in the links above.
Issues found by fast-check in famous packages
fast-check has been able to find some unexpected behaviour among famous npm packages. Here are some of the errors detected using fast-check:
Issue detected: enabling !!int: binary
style when dumping negative integers produces invalid content [more]
Code example: yaml.dump({toto: -10}, {styles:{'!!int':'binary'}})
produces toto: 0b-1010
not toto: -0b1010
Issue detected: unicode characters outside of the BMP plan are not handled consistently [more]
Code example:
leftPad('a\u{1f431}b', 4, 'x')
leftPad('abc', 4, '\u{1f431}')
Issue detected: enabling the bracket
setting when exporting arrays containing null values produces an invalid output for the parser [more]
Code example:
m.stringify({bar: ['a', null, 'b']}, {arrayFormat: 'bracket'})
m.parse('bar[]=a&bar&bar[]=b', {arrayFormat: 'bracket'})