Stack Builders logo
Arrow icon Insights

Diving into Property-Based Testing with JavaScript - Part 2

In this part of the blog, discover the differences and add-ons of property-based testing over traditional methods. 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.

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.

Published on: Mar. 20, 2024
Last updated: Dec. 21, 2024

Written by:

NatalyRocha
Nataly Rocha

Subscribe to our blog

Join our community and get the latest articles, tips, and insights delivered straight to your inbox. Don’t miss it – subscribe now and be part of the conversation!
We care about your data. Check out our Privacy Policy.