Now that we have grasped the fundamentals of property-based testing in Part 1, we will explore the differences and add-ons that property-based testing offers over traditional example-based testing. Furthermore, a practical template will be shared to guide you in incorporating property-based testing into your toolkit, providing you with additional resources to strengthen your testing practices.
Why should we use property-based tests?
- We incorporate a wider variety of inputs into our testing.
- We test more inputs and avoid infinite copy-pasting
- Some counterexamples and edge cases could be discovered during the testing
Let’s see the difference between example-based testing and property-based testing for this case:
Example-based tests
describe('capitalize', () => {
test('capitalizes the first letter of a word', () => {
const result = capitalize('hello');
expect(result).toBe('Hello');
});
test('handles an empty string', () => {
const result = capitalize('');
expect(result).toBe('');
});
test('does not change an already capitalized word', () => {
const result = capitalize('World');
expect(result).toBe('World');
});
test('capitalizes the first letter of a sentence', () => {
const result = capitalize('this is a test.');
expect(result).toBe('This is a test.');
});
test('handles special characters at the beginning', () => {
const result = capitalize('!test');
expect(result).toBe('!test');
});
});
Property-based Test
test('should capitalize any string', () => {
fc.assert(
fc.property(fc.string()), (data) => {
const result = capitalize(data);
expect(
data.length === 0 || result[0] === result[0].toUpperCase()
).toBeTruthy()
})
);
});
This approach is particularly useful when dealing with complex or hard-to-reach code paths that may not be exercised with traditional testing methods.
For example, in this case, fc.string will start generating all kinds of strings you can imagine, which helps to test cases like empty text, special characters, upper case, lower case, and others. However, you will have to consider that the text generated can also be bizarre or useless. The test above works fine for this function, but if you would like something more real, you need to consider this when defining the arbitraries. Fast check provides several options that you can use for this, like stringOf, stringMatching, examples, and others.
Also, if we dig a little deeper, we can see that the property-based testing is just checking if the first letter is capitalized with all the advantages of looking for edge cases. But what happens when we send all letters in uppercase, or we want to control for empty strings? The test will pass because the property we added is just testing for the first letter to be uppercase and not for the rest. It all depends on how our function works and what we want to test, so we can either combine it with a unit test that will check for it or we can add another property, for example:
fc.assert(
fc.property(fc.string(), data => {
return data.length === 0 || [...capitalize(data).slice(1)].every(c => c === c.toLowerCase());
})
);
A template for approaching property-based testing
Usually, when we are writing an example test, we are guided by a template like: when a full lowercase word is received it returns the word with the first letter in uppercase
The property test can’t be different; we need a mind template or a guide to describe our test and make it easier to reason. As we are receiving many inputs at the same time and we don’t know the exact value but the shape of it, we can think of this template as follows:
given any valid arbitrary inputs of a certain type when we call or perform certain action it should always (return certain result / hold certain condition / behave in a certain way)
For the capitalize method example, we can say:
given any valid string when the capitalize function is run it should always return the first letter in uppercase
Finally, it looks like:
describe('given any valid string when we run capitalize', () => {
it('should always return the first letter in uppercase', () => {
fc.assert(
...
);
});
});
Next Steps:
In this part of the blog, we have defined why we should use property-based tests and showcase their capabilities with a basic example. We also have a template that can help us define our tests and make it easier to reason about them. The next part of this blog will cover a real example of where property-based tests were helpful and how to debug the errors found when running the tests.
References:
- https://fast-check.dev/
- https://fast-check.dev/docs/introduction/why-property-based/
- https://fast-check.dev/docs/tutorials/quick-start/
- https://jrsinclair.com/articles/2021/how-to-get-started-with-property-based-testing-in-javascript-with-fast-check/
- https://jsverify.github.io/
- https://medium.com/criteo-engineering/introduction-to-property-based-testing-f5236229d237