I tested a React app with Jest, Enzyme, Testing Library and Cypress. Here are the differences.
Finally, a side-by-side code comparison between the most popular testing tools for React. Watch as we attempt to write the exact same tests with Jest, Enzyme, Testing Library, and Cypress.
You’ve probably know — or at least have heard — that knowing how to test your code is important.
A lot of jobs in tech will require knowledge of testing with at least one testing tool/framework/library.
Here we will look to show you how to write tests with three different tools so that you can not only see the differences between them, but also begin to understand how to write tests with each and form your own opinions over which may be preferable over the other under certain circumstances.
We will take the React To Do app from one of my previous articles — I created the exact same app in React and Vue. Here are the differences — and will test all of its core functionality with Jest, which is the testing tool created by Facebook (who also created React), Testing Library (formerly React Testing Library), which is a tool that is actually recommended for use in the React docs, and Cypress, which is an e2e testing tool (more on what that means later).
Note: You’ll commonly hear these called testing tools referred to as testing libraries, but I wanted to try and make a clear distinction between a testing library, and Testing Library. Therefore, when we’re referring to a testing library in lowercase, we’ll call it a testing tool.
What’s Testing Library?
According to their website, “the Testing Library family of libraries is a very light-weight solution for testing without all the implementation details. The main utilities it provides involve querying for nodes similarly to how users would find them. In this way, testing-library helps ensure your tests give you confidence in your UI code.” Their philosophy is that “the more your tests resemble the way your software is used, the more confidence they can give you”.
According to their website, Cypress is an end-to-end (e2e) “test runner built for humans”, with tests that “are easy to read and understand” and can be run in your browser and “run as fast as your browser can render”. Finally, Cypress provides “readable error messages [that] help you to debug quickly”. As you may have noticed, there wasn’t a nice chunk of text that I could pick up from the website, but rather I had to pick out sentences from their Features page.
What’s the difference between them?
A key difference I should mention here is that Jest and Testing Library are typically what you might refer to as Unit Testing and Integration Testing libraries, whereas Cypress is typically used for End-To-End (e2e) testing. This means that you would test your application from start to finish, as if you were a user using your application (with all the clicks and page navigations included and so on).
This is not to say that you couldn’t use Jest and Testing Library for e2e, or that you couldn’t use Cypress for Unit and/or Integration testing. You may even find that you decide to take a hybrid approach and utilise more than one tool for your testing. It’s really up to you when it comes to what you consider to be the best solution.
Before we look at the tests, let’s take a quick review of our React To Do app and make a list of all of the functionality that we will be testing.
We have three files: App.js, ToDo.js, and ToDoItem.js.
App.js is simply our root component which renders our ToDo.js. ToDo.js is our main file and handles all of our application logic — think of it as the brains of our To Do app. By default, our To Do app has two items. These are mapped to ToDoItem.js which means that we end up with two ToDoItem’s rendered inside of our ToDo component.
If you’re interested in checking out the code, here is a link to the repo without the tests: https://github.com/sunil-sandhu/react-todo-2019.
What will we be testing?
We will be looking to test that our app is able to do the following:
- Render without crashing
- Render two default ToDo items
- Render an input field for typing up new ToDo items
- Render an ‘Add’ button for adding ToDo items
- If the ‘Add’ button is pressed but the input field is empty, prevent a new ToDo item from being created
- If the ‘Add’ button is pressed but the input field is empty, show an alert to the user
- If the ‘Add’ button is pressed and the input field has content, add a new ToDo item
- When the ‘Delete’ button is pressed for a single ToDo item, remove that ToDo item from the App
- From the two default items in ToDo, if the first ToDo item has been removed from the app, the second item should now become the first (and only) item.
- For the data being passed down from ToDo to ToDoItem as props, each ToDoItem should render the text that was passed down to it.
- Each ToDoItem should render a ‘Delete’ button.
For what is conceivably a fairly straightforward application, we have no less than 11 tests to make. This should give us a nice introduction into each testing tool to see how they handle these. Let’s quickly take a look at the file structure for our three applications.
One thing I’d like to point out is that we have used Create-React-App (CRA) to bootstrap our React app. This means that it comes with Jest installed by default. We won’t be removing this from our set up, as Jest actually comes in handy in more places than just in the Jest set up. You can of course look to strip it out completely if you prefer.
Let’s take another look at our file structure:
We can see here that there are three test files in our Jest setup: App.test.js, ToDo.test.js, and ToDoItem.test.js. Now the first thing I’d like to note is that putting these files alongside their .js counterparts is not necessary. We could have put these all in one folder called Tests if we wanted to. This is because create-react-app has a set up that automatically searches for any files that have .test.js in the filename, so they can be put anywhere. There are other alternatives for how they could be named and placed, but we won’t go into that any further here. The same also applies for the Testing Library and Cypress setups.
Anyway, let’s take a look at one of our Jest tests so that you can preview what the syntax looks like:
We will talk more about what this all means later.
Now if you cast your eye back to the screenshot we attached earlier that showed the folder/file structure for our Jest app, you may recall seeing a file in the src folder called setupTests.js. Let’s take a moment to explain the purpose of this file.
This file basically signals to our testing environment to include this code in any test file. This set isn’t necessary, as we could just add this to every test file. However, in the spirit of keeping our code DRY, we’ve opted to create this file. One thing to note is that we don’t have to tell our app to pick this up as create-react-app already has an internal set up to look out for this file in the src directory. So in our case, we just create the file and add the following lines of code:
This basically imports Enzyme and an Adapter that allows Enzyme to be used with the latest version of React. We then set it up by adding that
Enzyme.configure function. This isn’t something I created myself, but rather took this from the docs on the Enzyme website.
Let’s take another look at our file structure, then we’ll get into each file and review what is going on:
For Testing Library, you can see that we have the exact same file structure as that of Jest. This means that we have three test files, App.test.js, ToDo.test.js, and ToDoItem.test.js.
As with Jest, let’s take a look at one of our Testing Library test files so that you can preview what the syntax looks like:
As with Jest, I also couldn’t fit all of the tests inside of these screenshots as some of the files had quite a lot of lines. But what I wanted to demonstrate here is the language (or rather syntax) used in these texts.
Now one thing you may notice is that the setup for Testing Library is quite similar to that of Jest. In the case of Testing Library, it actually uses and extends the capabilities of the
expect() assertion from Jest.
I would like to note though that while we have allowed Testing Library to incorporate parts of Jest in our setup, you don’t have to use Testing Library with Jest. We opted for it because our create-react-app already comes with Jest bundled in by default.
We also make use of a setupTests.js file. The purpose for this file is the same as with Jest, but let’s just take a moment to explain what we have included in this file.
As with our Jest setup, we have opted to keep our code as DRY as possible by creating this file.
As you can see, it only has one line of code, which is importing a module from the Testing Library package that extends the capabilities of the
expect assertion. This allows us to be able to use things such as
toBeInTheDocument() as part of our assertions.
This stays in line with the Testing Library approach to testing, which is to mimic how a user would use your app/website. A user doesn’t care about how an app/website has been implemented, but rather whether it does the thing it is meant to. So if we use that last assertion we mentioned, a user cares whether certain elements show up on the page. If the element is meant “to be in the document”, we test with
You will see from our test files in Testing Library that we are importing a
render function from
”@testing-library/react”. This is basically the equivalent of us using the
mount function in our Jest + Enzyme set up.
A key difference between Jest and Testing Library is that Testing Library bases its querying criteria on
data tags that must be added to the elements in your components. So for example, if I wanted to grab a specific input element, I wouldn’t be looking to get it by finding an
input element, or even a specific class that our
input element is using. Instead, we would add a
data-testid tag to our
input element. It then ends up looking like this:
Then in our ToDo.test.js file, we would query it by writing:
The reason why Testing Library follows a philosophy of querying by
data-testid is because these IDs typically wouldn’t change during the lifetime of an application and, therefore, your tests will be less fragile and subject to breaking because a class name has changed somewhere down the line. Now we could actually follow this same type of pattern in Jest by adding
data-testids and querying by them. If we did this, our test would likely look something like this:
In fairness, this is likely to be an encouraged pattern to take if using any testing tool, as the
data-testid attribute is less likely to change than the css
className. So bear this in mind if you are writing tests with Jest, or any other tool. For now we will continue to test in Jest (and also in Cypress) by classing either html elements or classes but note that grabbing by data attributes is a better practice all round.
Let’s take another look at our file structure, then we’ll get into each file and review what is going on:
What differs greatly here between Cypress and the other two testing tools is that Cypress has its own folder that handles the files for testing. Therefore, we do not have our separate test.js files for App, ToDo and ToDoItem, but rather we have one file App.test.js which sits inside of the integration folder.
Because Cypress is used for end-to-end (e2e) testing, we only required one file in our setup. This is because Cypress effectively opens up a browser and runs all of the tests required. We could have done this with three separate files, but it would mean that we would have to run three separate e2e tests. This would still work, but would be a much less optimal approach to take.
Note: Cypress offers other capabilities, such as a fixtures folder which would be our place for storing mock data. We can also opt to extend the capabilities of Cypress in the support folder. Our tests do not require any of these, so we won’t be covering them here.
As with Jest and Testing Library, let’s take a look at our Cypress test file so that you can preview what the syntax looks like:
Note: We’ve ended up showing a bit more of a preview here than with Jest and Cypress. This wasn’t deliberate. It was simply because we have all of our tests in one file here, so the screenshot ended up showing a bit more as a result of this.
All our tests use
describe() blocks and it() functions
A similarity you will notice from the tests across all of our examples is that they all follow the same pattern of having a
describe() block with some
it() statements inside for each test. There are other ways to structure these, such as using
test() instead of
it(), but we’ve opted for this approach as it is typically the most common and means that we can better structure our tests to look as similar to one another as possible.
Anyway, we’ve covered a lot of stuff here so far and we haven’t really reviewed the tests side-by-side.
So let’s step through each of the criteria we wanted to test and see how each testing tools handles it.
1. Render without crashing.
This is typically known as a “smoke test” where we just make sure that our app doesn’t throw an error and actually loads up as expected. In this case, you might see people check to see whether our component returns a length of 1. I really don’t like those tests as they aren’t expressive enough. Therefore, I’ve opted to check whether the title tag from the header of our To Do app has rendered.
Here we create a variable called
app and set it to the
mount() function that takes in our
App component that is imported in the same file. Notice that we pass in the component as a React component, by passing it in as
We then use the
find() function in Jest which basically works in the same way that
document.querySelector, we put
app.find instead as
app has been assigned to our
<App/> component. You will notice that we then chain
text() onto it, which basically returns the text from that element. Finally, we check to see whether it is equal to
"React To Do”. This is case-sensitive, so in our case, the use of capital letters for each word is vital, as that is how it is in our To Do app.
Here we use object destructuring to capture the value of
getByText from our
render() function, which has been imported into the file. This is similar to the
mount() function used in Jest. There are many other values that appear from the
render() function, but for this test, we just need the
getByText value. This basically grabs all of the text from whatever we use to search it on, which in this case, is our App component.
As with Jest, we have imported our
App component and then pass it into our
render() function with our angle brackets, meaning that
App is passed in as
<App/>. On line 4 of the code snippet above, you will see that we pass
"React To Do" in as the param of our
Finally, we chain onto it,
toBeInTheDocument(). As you may have guessed, this checks to see if the value passed in exists in the document.
Here we begin by typing
cy, which is kind of similar to how you might use the
$ dollar sign in jQuery. In Cypress, we begin all of our tests with
cy. We then use
get() which is similar to using
contains(), which takes in a parameter. It then checks to see if
h1 includes the parameter —
"React To Do" — we have passed in, which in this case is
React To Do.
You’ll likely have also noticed that we have a
before() function at the top that basically instructs Cypress to do this first before running any tests. All other tests inside of App.test.js sit inside of
describe() blocks that are inside of this main
describe() block. It wasn’t a requirement to have multiple
describe() blocks in a file - it’s really down to how expressive you choose your tests to be.
Anyway, we effectively have a root
describe() block and then further
describe() blocks sat inside of it. Because of this set up, we only need one
before() function inside of our root
Another thing worth noting about Cypress is that because we are testing an app that we are working on locally that hasn’t been compiled. we have to start up the server for our create-react-app by running
npm run start. This has to be done first before running any tests, otherwise Cypress will try to visit localhost:3000 and nothing will be there.
We could have added another step to our code and had Cypress handle this for us, but opted against it for this article.
2–4. Render two default ToDo items; Render an input field for typing up new ToDo items; Render an ‘Add’ button for adding ToDo items.
Here we have similar functions to those used earlier, but have used
toBe for the first test, but
toEqual for the second and third.
Typically for the examples we have used above, we would be fine with just using
toBe which checks with strict equality
===. As we are dealing with primitives, this would have been fine. We would usually use
toEqual when we want to check deep equality with things such as objects. Here I’ve opted to use both so that you can know that both exist — and that both would still work with primitives such as numbers and strings.
You see here that our first set of tests are pretty much the same as those used in the earlier Testing Library example. However, in the second and third tests you will see that we are using a
getByTestId variable instead. This is where we begin to use our Testing Library best practice, which is to attach
data-testid attributes to all of the elements in our app that we plan to test. Therefore, in these examples, we have elements in our app that look like this:
<input data-testid=”todo-input”/> and
getByTestId works in the same way, so it’s basically a
querySelector. As you can see, we then use
toBeInTheDocument() in the same way as our previous examples.
You will see here that we begin each test with
cy.get(), which is the same as we did before. In these tests we chain each with
should(). This allows us to make assertions in our code. You will see that inside each, there are two parameters being passed. The first param for each is
"have.length". We use
have. when we require a second parameter, so in our examples, we use things such as
cy.get('.ToDo-Item').should('have.length', 2). This is because our app has two To Do Items rendered by default, so there should be two elements with the
ToDo-Item class on the page.
As an aside, there is also another option for our
should() functions that only takes one parameter. This is when we don’t need to check for a specific value, such as when were checking whether our elements were of a particular length. If we don’t need to pass in a value to compare against, but rather just want to check if something is true or false, we can look to use
be. An example might be something like
cy .get(‘h1’).should(‘be.visible’), where we just want to check whether the h1 tag is visible on the page and is not being hidden by css.
5–7. If the ‘Add’ button is pressed but the input field is empty, prevent a new ToDo item from being created; If the ‘Add’ button is pressed but the input field is empty, show an alert to the user; If the ‘Add’ button is pressed and the input field has content, add a new ToDo item.
Here’s three new things to review —
simulate(). Oddly enough, we will discuss these in reverse order. On line 9 of the code snippet above, you will see that we query our app to find the add button that has a class of
.ToDo-Add. We then chain on a
simulate() function. This is something that we have access to from Enzyme. This allows us to simulate events such as clicking and typing input and a bunch of other things. It accepts parameters, which in our case we pass in
"click". This, as you might expect, simulates clicking on our button that adds new To Do Items to our app.
Because our app has two default items already to begin with, we are saying to our app that we still expect this to be 2 even after clicking the add button. The reason for this is because our app does not allow for new items to be added if the input field is empty. Instead of adding a new item, our app displays an alert to the user via the native alert feature in browsers. This takes us to the second new thing we want to look at —
Jest allows us to mock functions. This is generally when we want to override the typical behaviour of our app, which could be for a variety of reasons, ranging from avoiding expensive and/or slow operations such as fetching data from an API or a database, to replacing functions that otherwise wouldn’t be available in our testing environment, such as the
alert() function that lives in the browser window. Here we replace the native function by writing
window.alert = jest.fn(). This in effect stops our tests from breaking when they hit this bit, because it would break due to the lack of existence of a window — and
alert() function as a result.
You will then see that the next test checks to see whether the function that now sits as
window.alert has been called. Here we use
toHaveBeenCalled(). There are variations to this if we needed to check whether the function had been called more than once.
Here we make use of the
simulate() function again, but in this case we use
"change" as the first parameter. This works in the same way as checking for
onChange events in React, so for things such as input field changes. You will notice that we create a variable called event and pass in an object with a key of
target which has a value of another object with a key of
value and a value of
"Create more tests". This is so as to mimic how the
event object typically looks when grabbing the
event object of an input field.
Anyway, we pass that
event variable in as the second parameter to our
simulate() function. We then simulate a click event after. Finally, we look to grab the third element with a class of
".ToDoItem-Text" — which is at zero index of 2 — and check whether it equals the value of the event text we passed in earlier, which was
"Create more tests".
This third new feature we wanted to explain was the
afterAll() function. This basically runs at the end of all of the tests inside of a
describe() block. We run this simply to revert the number of To Do Items inside of our testing environment to 2, which is the default number. We do this by finding the first element with a class of
".ToDoItem-Delete", and clicking it. This wasn’t entirely necessary, but I opted to do this. If we wanted to be more precise, we would really want to look for the third delete button in our app as that would correspond with the new item we added. To do this, we would have written something such as:
A lot of the tests here run similar to those we saw in our Jest equivalent. We actually use Jest to mock the
window.alert function in the exact same way. You can also see that we even check to see if the function was called in the same way through the use of
toHaveBeenCalled(). The only variation here is that we use
fireEvent.click() instead of
simulate("click"). You also notice that we pass in the element we want to click on as the parameter inside of
The test for adding a new item is also quite similar to Jest’s equivalent. We created an
event variable, use
fireEvent to simulate an input change on the input field, passing in our
event as the second parameter.
We then checked whether the element with the
todo-input had a value that matched the value from our
event variable. We didn’t do this in our Jest example and I was going to take this out but though I’d leave it in.
Finally, we click the button that adds new To Do items in our app and then check to see if we have an element with a text value of
"Create more tests" is in our document via
toBeInTheDocument(). This check does differ from our earlier test of checking the input value, as
getByText looks for DOM nodes that contain text, whereas our input DOM node doesn’t contain text, but rather it contains an
event object. We can test the validity of this claim by simply commenting out the line from our test that clicks the ‘add’ button as follows:
And checking to see whether our test still passes — which it doesn’t.
We handle things quite differently here. Again, this is because our tests run inside of the browser. Firstly we
get() our add button which has a class of
".ToDo-Add" and simulate a click event by chaining
click(), to it. We then
get() our input field, chain a
type() function and pass in the value of
"Create more tests".
We continue here by chaining a
should() function to see whether our input field now has the value —
have.value — of
"Create more tests". Again, this last bit probably wasn’t necessary, but I kept it in for Testing Library, so I kept it in for Cypress.
Moving on, we then
".ToDo-Add" button again and chain a
click() onto it. Finally, we check how many To Do Items we have by running a
get() function on
.ToDo-Item, and check to see whether it has a length of 3 — which would be our two default items, plus the one we just created. This covers the check we need to see whether To Do Items are prevented from being added if the input field is empty because if it didn’t prevent it, our
cy.get(“.ToDoItem”).should(“have.length”, 3) test would fail as it would have had a length of 4.
8–9. When the ‘Delete’ button is pressed for a single ToDo item, remove that ToDo item from the App; From the two default items in ToDo, if the first ToDo item has been removed from the app, the second item should now become the first (and only) item.
The tests here are the same types as those used before. One difference you will see here is that both tests have a
first() chained into the middle of them. Because there are multiple elements with matching classNames, we can use
first() to literally grab the first instance.
querySelectorAll. In Testing Library we have
queryAllByTestId. So we created a variable called
deleteButtons and assigned
queryAllByTestId, to it.
Here we checked that the To Do Items length was 2. We then clicked the first element in the index by writing
fireEvent.click(deleteButtons). Finally we checked to see if the To Do Items length was now 1.
The test after is pretty similar. However, in this case, we click the first ‘delete’ button and then check if the first To Do item now has the value of
"buy milk". The significance here is that this was initially our second To Do Item. So in effect, we’re checking to see if this has now moved from position 2, to position 1 (or position 1 to position 0 if you are referring to zero index).
With Cypress, we query items by using
nth-child selectors from CSS. So here we look to grab the
nth-child at position 1 of our
.ToDoItem-Delete buttons and
click() it. We then check if the
nth-child at position 1 of our
"buy milk", to ensure that the item has moved from position 2 to position 1.
10–11. For the data being passed down from ToDo to ToDoItem as props, each ToDoItem should render the text that was passed down to it; Each ToDoItem should render a ‘Delete’ button.
Here we are isolating our
ToDoItem component and have created a variable called
item which we pass in to our
mount() function as a prop on the
ToDoItem, as so:
You may here some murmurs in the Enzyme community over whether to use
shallow() when rendering components. Put simply,
mount() will render everything in that component, including subcomponents and so on, while
shallow() will just attempt to render that component alone, so subcomponents won’t be included.
I’ve opted to just use
mount() all the time as I feel that is closer in resembling how your users would see your app ie. they will see your app in its entirety, not your components in isolation.
Anyway, the tests here to
ToDoItem.test.js are pretty straightforward at this point. We check to see if the text from our
item prop is rendered by writing:
expect(toDoItem.find(“p”).text()).toEqual(item.text). Finally, we check to see whether our ToDoItem component has a ‘delete’ button. Here’s what both of those tests look like:
At this point you can probably deduce how these tests work. They’re also very similar to those written for Jest.
We don’t have tests for this. The reason being that we run these tests in a real browser, and therefore, cannot simulate passing dummy data in this way. This is not really a problem though as being able to visually see our two previous To Do Items in the browser sufficiently demonstrates that our data is being passed in properly and in the correct format.
And there we have it 🎉
We have shown you how to write over ten equivalent tests written with three different testing tools!
There is more functionality when it comes to testing, such as handling asynchronous code, mocking, and snapshot testing, but I hope that reading this has given you at least a base level understanding of the differences between Jest, Testing Library and Cypress. Now pick up at least one of these, write a bunch of tests and leave a comment letting me how you got on with them!
Interested in checking out the code?
Here are the repos:
Testing Library: https://github.com/sunil-sandhu/react-todo-2019-with-testing-library
We have also launched three new publications! Show some love for our new publications by following them: AI in Plain English, UX in Plain English, Python in Plain English — thank you and keep learning!