Test-driven Frontend — Fun with functional testing

Kathrin Holzmann
5 min readFeb 14, 2020

In this article, you will learn how to test React Components for functions and user behaviour in a test-driven way

This story is Part 4 of test-driven frontend series. Find the other Parts of the series here:
Part1 — Testing strategies
Part 2 — How to set up a unit testing Environment
Part 3 — Accessibility testing
Part 5— Design testing

If you want to start the code along straight away without implementing the 3 & 2 yourself, you can clone this branch here

To run a functional test that covers user interaction, we need to implement three steps.

  • Render the component in a virtual environment
  • Interact with the component
  • Check our assertion

In the basis of the create-react-app, there is already the test engine Jest included. Which means we can use it to write assertions. But before we can interact with the component to simulate user events or call functions we have to make it accessible to the testing framework. Rendering can be done with normal ES6, by creating the component in the global document element, but then we can only check assertions that happen on the first render. For full access to all functions and state of the component, we need to use an additional library. One of the most popular ones is enzyme, which we are going to use.

yarn add jest-environment-enzyme enzyme enzyme-adapter-react-16 --dev

In /src/components/Counter/test/Counter.test.js we can now try out what enzyme has to offer.

1) Rendering

The first step might be the trickiest one to wrap your head around. How to make the component available to the virtual DOM and what are the differences of the rendering methods? The most important ones for us from enzyme are:

  • shallow
  • mount

Both make your component available to the virtual browser for interactions and assertions. But there are small differences between them. Best to understand is if we try and compare the outcome.

Shallow render the counterComponent

To execute this test we can now run in the terminalyarn run test the output will look something like this:

console.log src/components/Counter/test/Counter.test.js:9
<div className="counter">
<button>
-
</button>
<input type="text" readOnly={true} value={0} aria-label="current amount" />
<button>
+
</button>
</div>

Shallow renders the exact component as you hand it over and also calls the lifecycle methods like componentDidMount or componentDidUpdate. There will be no component instance or child components rendered. We can inspect further what this means if we create a wrapper component for the counter.

New Component for wrapping the counter
Counter wrapper test file
console.log src/components/Counter/test/CounterWrapper.test.js:9
<div className="counterWrapper">
<Counter amount={0} />
</div>

As we can see, only counterWrapper get’s now rendered as html, and the Child component Counter stays as react component. This behaviour is excellent for unit testing purposes. While for an integration test, you might want to use mount for a full dom render. Full render means that, for your described test case, all child components will be rendered and be able to manipulate the same DOM. Let’s try it and change the render method from shallow to mount.

console.log src/components/Counter/test/CounterWrapper.test.js:13
<CounterWrapper>
<div className="counterWrapper">
<Counter amount={0}>
<div className="counter">
<button>
-
</button>
<input type="text" readOnly={true} value={0} aria-label="current amount" />
<button>
+
</button>
</div>
</Counter>
</div>
</CounterWrapper>

Now we have the full render tree, but also a hint that we are rendering multiple react components.

There is also a third option which is just called render, which is static rendering. Enzyme did not implement the functionality itself but provides it via the Cheerio library. This variation of rendering is useful if you want to test class names or the existence of HTML elements inside your react component.

For moving on with the test, we will keep the shallow rendering of the counter component without the wrapper.

2) Interactions

Now that we have successfully rendered we should try to interact with it. Our first test says: it('increases the amount on plus interaction') . First, we have to find the increase button in order to interact with it. There are multiple ways to do this. The one I like, because it also adds more accessibility to our component, is to add aria-labels to both buttons, as we did already to the input field. Now we can find the increase button by counterComponent.find("[aria-label='increase']")

Once we selected them, we can use the simulate() function of enzyme. Simulate does not create an actual event object but calls the onEvent handler of the component. In our case, we use the onClick event. It also means that, if we would use the event Object e.g. event.target orevent.preventDefault()inside the function, we need to hand it over, too.

component.simulate('click', target: { target:{ name: 'width', value: 50},preventDefault(){}});

In our case, it is straight forward as we attach the function to the onClick handler of the Button, and we do not need the event’s target.

3) Assertions

Now we come to our assertions. For the plus interaction, we expect two things: The function increaseAmount should be called, and the value of the input should be two.

For observing the function, we will use jest.spyOn. spyOn is returning a mock function, but also tracks the calls of the original function. So it’s not overwriting the function itself — which is precisely what we need in this case.

For the second assertion of the input value, we create another selector and then look for its value prop.

Congratulations, we have a failing test!

Implementing the functionality

To now fix the failing test we have to add the functionality we want it to have. In Counter we create a new function called increaseAmount, which sets the state variable amount to x+1

And the same for decreasing functionality

Note that the value inside the test has to be 0 and not -1, this is because we render the component outside of the describe block. Which means for every test, we manipulate the same DOM and state as before. And as we have first tested the increase function the value, to begin with, is 1 and not 0.

Mocking of props

We now have a counter that can increase and decrease a value, but in a real-life scenario, we would also need a callback function that knows what to do with these changes. To test if this callback function get’s called with the correct parameters, we have to hand it over to the component as a so-called mock. To mock a function, we can use jest.fn() This function we can automatically follow along when it is called.

We can now hand this function over when we render the component, and as we have the tests for increasing and decreasing already in place, we can directly write what we expect for this function. First, it should have been called twice, and the last call should have had the parameter 0.

And then to make the test pass we call the function in the lifecycle method componentDidUpdate

Finished counter component

Summary

We now dipped into the uncertain waters of functional testing. I hope you feel ready now to dive deeper into the topic of writing frontend tests. In the next and last article in this series, we are going to add styling to our component, where all our current setup will help us.

--

--