You’ll probably want to include some unit tests in your build pipeline to avoid regressions. There are some popular options to help do this, jest being probably the most popular with mocha also high up there on the leaderboard. In this tutorial, we’ll cover some basics of mocha and how it integrates with chai, sinon, and enzyme.
- enzyme lets us pretend like we’re rendering react components without opening a browser, AKA “headlessly”.
- mocha is a testing framework that helps us keep our unit tests organized.
- chai is an assertion library that we use to make assertions about things.
- sinon is a mocking library that lets us spy on, stub, and mock functions.
Let’s say we’ve got two components: one parent — Marlin, and one child— Nemo.
To start with, we need to see what our component’s code looks like before we can test it. So here’s the code for the Marlin component:
and here’s Nemo:
Our First Unit Test
We’ll start with a simple unit test, modified from the one in the enzyme mocha integration example here. The point of this unit test is to make sure that our componentDidMount method gets called when we mount our component. Not a very useful test, but it’s a good example to start with:
Let’s break this unit test down, line-by-line.
We start by importing all the relevant libraries: react, enzyme, chai, and sinon, as well as the react component(s) we’ll be testing.
Before we use enzyme to mount our component, we set up a spy on the componentDidMount method using sinon.
Now, we organize our test suite and the unit test(s) within it with mocha’s describe and it callbacks. You can think of each describe as a group in which we’ll place individual unit tests, and each it as an individual unit test.
Within the it callback, we start writing our unit test. The first step is to mount the component with enzyme. Note that this won’t actually open a browser and show us our component on a web page; rather, this is done headlessly.
Finally, we use chai’s expect to actually test something. Here, we’re simply checking that when the component gets mounted, our componentDidMount method gets called once.
Cool. That makes sense. There are some optimizations we can make, though. Notice that we used enzyme’s mount function. This will mount our Marlin component as well as any child components that Marlin calls within its render function.
Since Marlin also renders Nemo, this means that Nemo will get mounted as well. In this unit test, we’re only testing Marlin’s componentDidMount function, so we don’t really care whether Nemo gets mounted or not.
We can make our test run a little bit faster if we use enzyme’s shallow function instead. When we use shallow, enzyme only renders one level deep — that is, it won’t actually render any child components. Since our test no longer needs to render an instance of the Nemo component, our test runs a little bit faster. Also notice that instead of importing mount, we import shallow:
As a general rule, we want to use shallow rendering wherever possible. There are some caveats where we’ll need to use mount (or render) instead, such as when we’re testing React refs.
Our Second Unit Test
Let’s add another unit test to make sure that our state gets updated properly. In this case, componentDidMount just sets the value of the state’s findingNemo variable to the value of the nemoIsLost property.
Since we’re still testing our Marlin component, we’ll include this second unit test within the test suite we already created:
We have a little bit of redundancy. Both tests call enzyme’s shallow function. We can make this less redundant with mocha’s beforeEach function:
There’s also an afterEach function that’ll come in handy, particularly for cleaning up after our tests. You’ll often see calls to sinon.restore() within afterEach callbacks.
Arrow functions aren’t included within a class’ prototype, which can make mocking them with sinon quite difficult.
We’ve seen how sinon can spy on instance methods of react components such as componentDidMount. Let’s look now at how that works with our arrow function, foundNemo. You’d think it’d be something like this (notice that we stub the function prior to calling enzyme):
This doesn’t actually work. You’ll see an error like
Marlin.prototype.foundNemo is not a function.
This is because, although foundNemo is a function, it’s not included with Marlin’s prototype since it’s an arrow function. Note that if we tried to stub our foundDory function instead (which isn’t an arrow function), this error wouldn’t pop up.
There’s a workaround, though. We can check that the contents of the arrow function were executed and that our state was updated accordingly. We know that calling foundNemo updates the state’s findingNemo variable to false. Let’s update our test to remove our stub of what our compiler thinks is a non-existent function, and check that the state gets updated as we’d expect:
If you have any recommendations on how I can improve this or if anything is wrong, let me know.