Register to get access to free programming courses with interactive exercises

Property-based testing JS: Advanced Testing

Property-based testing is an approach to functional testing that helps to verify whether the function under test corresponds to a given property. You don't need to set all the tests cases for this approach. Its task is to match the characteristics at output with a given property.

A property is a statement that can be represented in pseudocode as follows:

for all (x, y, ...)
such as precondition(x, y, ...) holds
property(x, y, ...) is true

We describe an invariant in the style of “for any data such that ... condition is satisfied” and, unlike usual tests, we don't explicitly specify all test cases, we only describe the conditions that they must satisfy.

Suppose we have a divide() function that finds the quotient of two numbers.

const divide = (a, b) => a / b;

Let's write an ordinary test for this function:

const {equal} = require('assert');

equal(divide(4, 2), 2);
equal(divide(18, 3), 6);

Here it's simple, we pass the numbers 18 and 3 to the input, and we expect to get 6. The tests pass. But here we only tested the function on two pairs of input data. The test shows that the function works correctly only in these two cases. But it may turn out that with a different pair of numbers, the function works in a different way unexpectedly. To solve this problem, we need to write a test that focuses not on the input and output parameters, but on the properties as a whole. These properties must be true for any correct implementation.

The division operation has a property: the distributive property on the right. It means that dividing the sum of a and b by c is equal to a / c + b / c.

We use this in tests. Let's not get hung up on specific values and use a random number generator to obtain test data.

const a = Math.random() * 1000;
const b = Math.random() * 1000;
const c = Math.random() * 1000;

const left = divide(a + b, c);
const right = divide(a, c) + divide(b, c);

We'll run this test several times in a loop, and at a certain point, we'll get a set of test data where all three numbers equal zero. The test will fall because dividing zero by zero gives NaN, and NaN is not equal to NaN. So we understand that we need to add a check for zeros to the function.

AssertionError [ERR_ASSERTION]: NaN == NaN

We wrote an ordinary test, but we used arbitrary values instead of those taken from our head and were able to run the test many times on different input data. In this way, we've checked the specification itself, i.e., what the function should do, not its behavior in individual cases. This is property-based testing.

In real life, no one runs tests in a loop by manually putting in values. There are ready-made frameworks for this. The data is generated automatically by the property-based testing framework based on the described properties. If after a certain number of runs with random data that meet the description, the condition is met, the test is considered passed. Otherwise, the framework completes the test with an error.

Let's look at what the advantages of property-based testing are:

  • It covers all possible data. The frameworks automatically generate data based on the described properties. In theory, this feature allows you to cover all possible types of input data: for example, the entire range of strings or integers.

  • Whenever a failure occurs, the framework tries to shorten the test case. For example, if the failure condition is the presence of a given character in the string, the framework should return a single-character string that contains only that character. This is a serious advantage of property-testing - if failure occurs, the test stops working on a minimal example, not on a set of input data.

  • Reproducibility: Before each test run, initial values are created so that if the test fails, you can reproduce the test on the same data set.

It's important to note that property testing does not replace unit testing. It should be treated as an additional level of tests, which will help reduce the time to check your code is written properly compared with other approaches.

Frameworks

The idea of property testing was first implemented in the QuickCheck framework in Haskell. There are several libraries for JavaScript too, one of them is fast-check.

To install it, run this command:

npm install fast-check --save-dev

Let's use it to test the contains(), which checks if a substring is contained in a string. Strings have two properties that we can use:

  • A string always contains itself as a substring
  • The string a + b + c always contains its substring b, regardless of the contents of b, regardless of content a, b and c
import fc from 'fast-check';

// Code being tested
const contains = (text, pattern) => text.indexOf(pattern) >= 0;

// Defining the properties

test('string should always contain itself', () => {
  fc.assert(
    fc.property(
      fc.string(),
      text => contains(text, text)
    )
  );
});

test('string should always contain its substring', () => {
  fc.assert(
    fc.property(
      fc.string(), fc.string(), fc.string(),
      (a, b, c) => contains(a + b + c, b)
    )
  );
});

Let's examine the structure of the test in more detail

fc.assert(<property>(, parameters)) — performs a test and checks that the property remains true for all a, b and c strings created by the library. When a failure occurs, this line is responsible for reducing the test case to a minimum size to make it easier for the user. By default, it performs a property check on 100 pieces of generated input data.

fc.property(<...arbitraries>, <predicate>) — defines a property. arbitraries — are values that are responsible for building input data, and predicate — is a function that tests input data. The predicate predicate should either return a logical value or return nothing and terminate the test in case of failure.

fc.string() is a string generator that's responsible for creating and shortening test values.

If desired, you can extract the generated values to check the properties by replacing fc.assert with fc.sample:

fc.sample(
  fc.property(
    fc.string(), fc.string(), fc.string(),
    (a, b, c) => contains(a + b + c, b)
  )
);

The generated data will look something like this:

{a: ") | 2", b: "", c: "$ & RJh %%"}
{a: "\\\" ", b:" Y \\\ "\\\" ", c:" $ S # K3 "}
{a:" $ ", b:" \\\\ cx% wf ", c:" 't4qRA "}
{a:" ", b:" ", c:" n? H. 0% "}
{a:" 6_ # 7 ", b:" b ", c:" 4% E "}
...

Now let's try testing a knowingly wrong implementation of contains(). We use it as an example to show what the framework generates in case of failure and how it shortens the input:

const contains = (pattern, text) => {
  return text.substr(1).indexOf(pattern) !== -1;
};

The framework generates a certain set of data. As soon as the test sees a failure, it triggers the shortening process. When testing the example above, a failure occurs:

Error: Property failed after 20 tests
{ seed: 1783957873, path: "19:1:0:1:1", endOnFailure: true }
Counterexample: [""," ",""]
Shrunk 4 time(s)
Got error: Property failed by returning false

Conclusion

Property-based testing is a useful and powerful tool. We shouldn't abandon classic testing methods, but we can combine them with property-based testing. For example, you can cover the basic functionality with classic example-based tests and also cover the critical functions with property-based tests.


Recommended materials

  1. Property-based testing framework

Are there any more questions? Ask them in the Discussion section.

The Hexlet support team or other students will answer you.

For full access to the course you need a professional subscription.

A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.

Get access
130
courses
1000
exercises
2000+
hours of theory
3200
tests

Sign up

Programming courses for beginners and experienced developers. Start training for free

  • 130 courses, 2000+ hours of theory
  • 1000 practical tasks in a browser
  • 360 000 students
By sending this form, you agree to our Personal Policy and Service Conditions

Our graduates work in companies:

Bookmate
Health Samurai
Dualboot
ABBYY
Suggested learning programs
profession
Development of front-end components for web applications
10 months
from scratch
Start at any time

Use Hexlet to the fullest extent!

  • Ask questions about the lesson
  • Test your knowledge in quizzes
  • Practice in your browser
  • Track your progress

Sign up or sign in

By sending this form, you agree to our Personal Policy and Service Conditions
Toto Image

Ask questions if you want to discuss a theory or an exercise. Hexlet Support Team and experienced community members can help find answers and solve a problem.