Property-based testing with React and fast-check
Tags: react, javascript, testing, typescript
Published: 2020-03-25 18:12:14

Property-based testing is quite a popular testing method in the functional world. Mainly introduced by QuickCheck in Haskell, it targets all the scope covered by example-based testing: from unit tests to integration tests.

If you have never heard anything about `property-based` testing or `QuickCheck`, don't worry, I've got you covered 😉.

Like the name is intending, this testing philosophy is all about properties. 

> It checks that a system under test abides by a property. Property can be seen as a trait you expect to see in your output, given the inputs. It does not have to be the expected result itself, and most of the time, it will not be. 
> [fast-check documentation](https://github.com/dubzzz/fast-check/blob/master/documentation/1-Guides/HandsOnPropertyBased.md)

## Our example application

To demonstrate what the benefits are and why you should also consider this testing method, let's assume that we have the following `react` application written in `TypeScript`.

In this example, we will use `fast-check`, a framework for this testing method.

Our application is a pixel to rem converter. The purpose is to enter a `pixel` value, which is converted to the corresponding `rem` value, assuming that the base font size is `16px`.

**`RemConverter.tsx`**
```tsx
import React, { FC, useState, FormEvent } from 'react'

interface Props {}

const RemConverter: FC<Props> = () => {
  const [baseFontSize] = useState(16)
  const [px, setPx] = useState(baseFontSize)
  const [rem, setRem] = useState(px2Rem(px, baseFontSize))

  const convert = (e: FormEvent) => {
    e.preventDefault()
    setRem(px2Rem(px, baseFontSize))
  }

  return (
    <div>
      <form onSubmit={convert}>
        <h6>Base font-size: {baseFontSize}</h6>

        <div>
          <label>PX</label>
          <input
            data-testId="px"
            value={px}
            onChange={e => setPx(parseInt(e.target.value, 10))}
          />
        </div>

        <div>
          <label>REM</label>
          <input data-testId="rem" value={rem} disabled />
        </div>

        <button type="submit">Convert</button>
      </form>
    </div>
  )
}

export function px2Rem(px: number, baseFontSize: number) {
  return px / baseFontSize
}

export default RemConverter
```

Our `<RemConverter />` is a functional component that expects an input for the `pixel` value and outputs the corresponding `rem` in another input. Nothing to fancy yet.

## Getting into testing

To begin our testing adventure, we will write a regular integration test with [`@testing-library/react`](https://github.com/testing-library/react-testing-library).

So what do we want to test here?

*Scenario: We want to enter a pixel value of `32` and press on the `Convert` button. The correct `rem` value of `2` is displayed.*

**`RemConverter.test.tsx`**
```tsx
import React from 'react'
import { cleanup, render, fireEvent } from '@testing-library/react'
import RemConverter from '../RemConverter'

afterEach(cleanup)

describe('<RemConverter />', () => {
  it('renders', () => {
    expect(render(<RemConverter />)).toBeDefined()
  })

  it('should convert px to the right rem value', async () => {
    const { getByTestId, getByText } = render(<RemConverter />)
    fireEvent.change(getByTestId('px'), {
      target: { value: '32' },
    })
    fireEvent.click(getByText('Convert'))
    expect((getByTestId('rem') as HTMLInputElement).value).toBe('2')
  })

})
```

Above is an easy and simple test to validate our scenario and prove that it is working.

Now you should start thinking 🤔
* Did I cover all the possible values?
* What happens if I press the button multiple times?
* ...

If you go the TDD way, you should have thought about things like that beforehand, but I don't want to get into that direction with the article.

We could start creating a list of possible values with `it.each`, but this is where property-based testing can help us.

`QuickCheck` in `Haskell`, for example, creates n-amount of property-values to prove that your function is working.

`fast-check`, like said before, is a library for that written in `TypeScript`.

So let's rewrite our test with `fast-check`.

## Testing with `fast-check`

To start writing tests with `fast-check` and `jest`, all you need to do is import it.

```tsx
import fc from 'fast-check'
```

Afterward, we can use specific features to generate arguments.

Our test would look like this:

```tsx
import React from 'react'
import { cleanup, render, fireEvent } from '@testing-library/react'
import fc from 'fast-check'
import RemConverter from '../RemConverter'

afterEach(cleanup)

describe('<RemConverter />', () => {
  it('renders', () => {
    expect(render(<RemConverter />)).toBeDefined()
  })

  it('should convert px to the right value with fc', async () => {
    const { getByTestId, getByText } = render(<RemConverter />)
    fc.assert(
      fc.property(fc.nat(), fc.constant(16), (px, baseFontSize) => {
        fireEvent.change(getByTestId('px'), {
          target: { value: `${px}` },
        })
        fireEvent.click(getByText('Convert'))
        expect((getByTestId('rem') as HTMLInputElement).value).toBe(
          `${px / baseFontSize}`,
        )
      }),
    )
  })
})
```

Quite different, doesn't it?

The most important part is 
```tsx
 fc.assert(
      fc.property(fc.nat(), fc.constant(16), (px, baseFontSize) => {
        fireEvent.change(getByTestId('px'), {
          target: { value: `${px}` },
        })
        fireEvent.click(getByText('Convert'))
        expect((getByTestId('rem') as HTMLInputElement).value).toBe(
          `${px / baseFontSize}`,
        )
      }),
    )
```
We will go through it step by step.

First of all, we tell `fast-check` with `fc.assert` to run something with automated inputs.

`fc.property` defines that property. The first argument is `fc.nat()` that represents a natural number. The second argument is our base font size served with the constant `16`.

Last but not least, the callback function is containing the automatically created inputs.

Within this callback function, we include our previous test using the given parameters. 

That's it 🎉.

If we run our test with `jest` now, `fast-check` generates number inputs for us.

***How can I reproduce my test, if something goes wrong?***
Whenever `fast-check` detects a problem, it will print an error message containing the settings required to replay the very same test.

```
Property failed after 1 tests
{ seed: -862097471, path: "0:0", endOnFailure: true }
Counterexample: [0,16]
Shrunk 1 time(s)
Got error: Error: Found multiple elements by: [data-testid="px"]
```

Adding the `seed` and `path` parameter will replay the test, starting with the latest failing case.

```tsx
   fc.assert(
      fc.property(fc.nat(), fc.constant(16), (px, baseFontSize) => {
        fireEvent.change(getByTestId("px"), {
          target: { value: `${px}` }
        });
        fireEvent.click(getByText("Convert"));
        expect((getByTestId("rem") as HTMLInputElement).value).toBe(
          `${px / baseFontSize}`
        );
      }),
      {
        // seed and path taken from the error message
        seed: -862097471,
        path: "0:0"
      }
    );
  });
```

## Conclusion

This is only a simple example of what you can do with the power of `property-based` testing and `fast-check`. 

You can generate objects, strings, numbers, complex data structures, and much more awesome stuff.

I would recommend everybody to look into `fast-check` because it can automate and enhance many of your tests with generated arguments.

For further reading and many more examples, please visit the [`fast-check` website](https://dubzzz.github.io/fast-check.github.com/#header).

The example application can be found on [CodeSandbox](https://codesandbox.io/s/fast-check-react-example-wl9nl) and [GitHub](https://github.com/tobiastimm/fast-check-react-example)