Write a property-based test with TypeScript and fast-check

Learn how to identify and fix a bug in a function using property-based testing in TypeScript with the fast-check library.

We start with a function isPrime that checks whether a number is prime. This function was initially tested with a list of known prime and non-prime numbers, which it passes successfully.

However, using fast-check, we implement property-based testing that tests the function across a broad range of integers to catch more errors. This helps us discover a bug where the function incorrectly identified perfectly square numbers as prime due to an off-by-one error.

Share with a coworker

Transcript

[00:00] In the top left we have a function called isPrime. This function checks whether a given number is a prime number or not, so it divides evenly only by one and itself. In the bottom left we have some tests for this function to make sure it works correctly. We have a list of prime numbers, and we make sure that each of them returns true from isPrime. We have a list of non-prime numbers, and we make sure that each of them returns false from isPrime.

[00:21] We can run these tests and see that they all pass. The problem with this, unfortunately, is that the code does have a bug in it, and we're going to find that bug by using property-based testing. Start by importing the FastCheck library, which is a popular property-based testing library for TypeScript. FastCheck can be used alongside existing testing libraries like Jest. Here, we create a FastCheck assertion.

[00:46] This is the thing that will make sure the property that we have defined is true. We create a fast check property which represents a property of our code and the property that we are going to check takes an integer. This is a constructing a fast check that will generate integers for us so we make sure our property works across a wide range of integers. The property that we're checking is that if a number returns true from isPrime, we make sure that it does not divide evenly by any number between 2 and 1 less than n. We can run these tests, and unfortunately they appear to be taking a long time.

[01:27] The reason for this is that the fc.integer number generator can return integers in a very wide range. It's very likely that we're trying to loop over many millions, potentially billions of iterations here. So what we're going to do to fix that is create a constraint. I think 10, 000 should do it, and then rerun our tests. So this test failed, and because we know that there's a bug in our isPrime function that's to be expected, The output is quite verbose, but the thing that we're looking for here is this counter example line.

[02:06] This is fast check telling us which of these randomly generated integers the property did not hold for. And in this case, it's 9. The bug in the code is that for perfectly square numbers we are incorrectly reporting that they are prime and the reason for that is that we have an off by one error here. Rerun our test and we can see that it passes. In summary we've moved away from testing hard-coded values to asserting a given property over our code for a wide variety of potential values.