What if tests write themselves? An introduction to property-based testing
\n\nHave you ever written a unit test, run it, gotten a green checkmark, and then a month later found a bug in the same function? Sound familiar? Classic unit tests are like checking that your wallet isn't empty by looking at just one bill. You check one specific scenario, one set of input data. But what if I told you there is a way to make the computer come up with thousands of different scenarios on its own and check if your code breaks?
\n\nThis isn't science fiction. This is property-based testing in JavaScript (or property-based testing). Instead of writing "if you pass 2 and 3, you get 5," you describe a property of your function: "for any two numbers, the result of addition will be a number that is always greater than or equal to each of the addends." And the test environment generates hundreds of random pairs of numbers, checking this assertion.
\n\nIn this article, we will break down how property-based testing works, how it differs from regular tests, and how to implement it in your project using the cool library fast-check. You will see how easy it is to find "edge case" bugs that you wouldn't even think to look for.
Why aren't regular unit tests always enough?
\n\nLet's get straight to the point. Suppose you have a function that sorts an array of numbers in ascending order:
\n\nfunction sortNumbers(arr) {\n return arr.slice().sort((a, b) => a - b);\n}\n\nYou write a unit test:
\n\ntest('sorts the array [3, 1, 2]', () => {\n expect(sortNumbers([3, 1, 2])).toEqual([1, 2, 3]);\n});\n\nThe test is green. You are happy. But what if the array contains null? Or undefined? Or very large numbers? Or an array with 10,000 elements? You won't write a test for each of these cases. But property-based testing in JavaScript will.
Instead of listing examples, you describe invariants (properties) that should always be true:
\n- \n
- The length of the sorted array equals the length of the original array. \n
- Every element of the original array is present in the sorted array. \n
- For any index
i, it is true thatarr[i] <= arr[i+1]. \n
These are precisely the properties that property testing will check.
\n\nThe fast-check library: your helper in random testing
\n\nIn the JavaScript world, the main tool for property-based testing is fast-check. It can generate random data of any type: numbers, strings, arrays, objects, dates — and even complex structures.
\n\nInstallation is simple:
\nnpm install fast-check --save-dev\n\nNow let's write our first property-based test for the sortNumbers function. We'll use the Jest framework, but fast-check works with any test runner.
const fc = require('fast-check');\n\ntest('sorting preserves the array length', () => {\n fc.assert(\n fc.property(fc.array(fc.integer()), (arr) => {\n const sorted = sortNumbers(arr);\n return sorted.length === arr.length;\n })\n );\n});\n\nWhat's happening here? fc.array(fc.integer()) is a generator that creates random arrays of random integers. fc.property takes this generator and checks the assertion (our arrow function) on 100 (by default) different sets of data. If even one test fails — fc.assert will throw an error and show the counterexample.
Adding more properties
\n\nOne property is good, but three is better. Let's check that after sorting, the array is indeed ordered:
\n\ntest('sorting returns an ordered array', () =>