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.

setuptests.js

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 toBeInTheDocument .

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:

<input

data-testid=”todo-input”

type=”text”

value={toDo}

onChange={handleInput}

onKeyPress={handleKeyPress}

/>

Then in our ToDo.test.js file, we would query it by writing: getByTestId(“todo-input”) .

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-testid s and querying by them. If we did this, our test would likely look something like this:

app.find(“[data-testid=’ToDoInput’]”)

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.

Cypress

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.

cypress/integration/App.test.js

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:

App.test.js, tested with Cypress

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.

Jest

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 <App/> .

We then use the find() function in Jest which basically works in the same way that querySelector would in JavaScript. But instead of writing something such as 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.

Testing Library

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 getByText() function.

Finally, we chain onto it, toBeInTheDocument() . As you may have guessed, this checks to see if the value passed in exists in the document.

Cypress

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 querySelector in JavaScript. We then chain onto it 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 describe() block.

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 yarn start / 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.

Jest

Jest

Here we have similar functions to those used earlier, but have used length to check the lengths here. This works in the same way as array lengths might be checked in JavaScript. You will notice that we have chained on 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.

Testing Library

Testing Library

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 <button data-testid=”add”/> .

Our 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.

Cypress

Cypress

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.

Jest

Here’s three new things to review — afterAll() , jest.fn() and 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.fn() .

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: app.find(".ToDoItem-Delete").at(2).simulate("click") .

Testing Library

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 fireEvent.click .

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 data-testid of 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.

Cypress

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 get() our ".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.

Jest

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.

Testing Library

One difference here is that Testing Library as separate values for querying items, similar to how JavaScript has querySelector and querySelectorAll . In Testing Library we have getByTestId and 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[0]) . 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).

Cypress

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 .ToDoItem s contains "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.

Jest

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: mount(<ToDoItem item={item}/> .

You may here some murmurs in the Enzyme community over whether to use mount() or 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:

Remember that our item variable is an object with a key of text and a value of “Clean the pot”.

Testing Library

At this point you can probably deduce how these tests work. They’re also very similar to those written for Jest.

Cypress

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:

Jest: https://github.com/sunil-sandhu/react-todo-2019-with-jest

Testing Library: https://github.com/sunil-sandhu/react-todo-2019-with-testing-library

Cypress: https://github.com/sunil-sandhu/react-todo-2019-with-cypress

A note from JavaScript In Plain English

We are always interested in helping to promote quality content. If you have an article that you would like to submit to JavaScript In Plain English, send us an email at submissions@javascriptinplainenglish.com with your Medium username and we will get you added as a writer.

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!