Learn How To Test React Applications (Testing React Applications with Jest and React Testing Library)
What are tests. Why test. What to test. What to use to test. How to test.
Table Of Content
- TL;DR:
- Why Write Tests?
- Types of Testing
- Which Tests Should I Write?
- Which Testing Library/Framework Should I Use?
- React Testing Library vs Enzyme
- What Should We Test?
- Testing Terminology
- Test Anatomy
- Setup React With Jest and React Testing Library
- Writing React Tests With react-testing-library and Jest
- Write a testable App
- What Do We Want to Test (Test Cases)
- Write and run actual Tests
- Appendix
TL;DR:
- You should use React Testing Library and Jest to test React Applications
- The GitHub repo for this article can be found here: https://github.com/machariamuguku/react_jest_react_testing_library
Why Write Tests?
- Increase confidence that your project is bug free
- Avoid regression (avoid potentially breaking future changes).
- Ensure your application does what you want it to (enforce business logic).
- Your team lead forced you to 😃
Types of Testing
- Static Testing/Type Checking: Catch typos and type errors as you write code. This helps identify certain types of problems before you even run your code. Tools for this include static type checkers such as Flow and Typescript and linters such as eslint.
- Unit Testing: Verify that individual, isolated parts (Components) work as expected. That given certain inputs a component returns/renders/does expected results. Tools for this include Jest, enzyme and react testing library.
- Integration Testing: Verify that several units work together in harmony. For example, that an admin user is taken to the admin dashboard after login. Tools for this include Jest, enzyme and react testing library. Notice something?
- End to End Testing: A helper robot that behaves like a user to click around the app and verify that it functions correctly. Runs the tests like how a Q.A person would, on a real browser with a real back-end(or a stubbed one but hey!). Sometimes called functional testing or e2e. Tools for this include cypress.
- Scream Test: Read This article. And don’t @ me when you are fired 😃
N/b: You may have noticed this but, the difference between unit and integration testing can be quite fuzzy. Especially when the react testing library is involved. Also read this article by Kent C. Dodds (he wrote the react testing library)
Which Tests Should I Write?
Basically as you scale the testing ladder (Static ---> End-To-End) two things happen:
- Confidence that your application is bug free increases.
- Costs (Time and Money) increases.
This means that it is ultimately better to have end-to-end tests alone than it is to have static tests alone. There is, however, a sweet spot. Integration testing (higher confidence and lower costs). Read this article for more.
Which Testing Library/Framework Should I Use?
Now that we know we should prioritize Integration testing (and that the line between integration and unit testing is fuzzy), which testing libraries/utilities should we use? Let’s break down the alternatives
- Jest - Jest is a complete JavaScript testing framework created and maintained by Facebook. Jest can be used as is for complete integration testing, however, it is generally used as a test runner, assertion and mocking framework. More on testing terminology later.
- Enzyme - Enzyme is a JavaScript Testing library for React created by AirBnB. Unlike Jest, Enzyme does not ship with test running, mocking, or assertion functionality (normally used with Jest for that). Enzyme is normally used for app/component rendering, DOM node(s) query, and event simulation. I promise testing terminology are covered below.
- React Testing Library - Like Enzyme. Used for app/component rendering, DOM node(s) query, and event simulation.
So Jest and either Enzyme or React Testing Library.
React Testing Library vs Enzyme
For me, it boils down to each library’s primary guiding principle:
- React Testing Library - The more your tests resemble the way your software is used, the more confidence they can give you.
- Enzyme - To be intuitive and flexible by mimicking jQuery’s API for DOM manipulation and traversal.
Enzyme API leans more towards testing implementation (stand alone components, state, props, and functions) whilst React-Testing-Library leans more towards testing user behavior. The main difference between the two libraries is summed up by this William Shakespeare quote: “To shallow render or not to shallow render, that’s the question”
I’ll spare you the semantics and tell you why I chose react testing library
- The guiding principle of testing your software the way it’s being used (by real users) increases confidence in your app being bug free in production. Not only is this principle supported, it’s enforced. This enforces a different mindset as you test your application. The best thing about this mindset change is that you are thinking about your users within your tests, thinking about how they interact with it rather than thinking about how the props and state objects look (implementation).
- By not testing implementation details (stand alone components, state, props, and functions), your tests will survive implementation overhaul and will not break if implementation change but assertions hold. Focusing on testing users behavior rather than the implementation allows you to easily refactor code going forward. You want your user experience to be the same regardless of your implementation details.
- Due to the strict use case/philosophy, there’s only a couple of queries and helpers baked in, this makes the library extremely lightweight and minimal.
- The library makers provide a custom jest matchers library (jest-dom) that is aligned to the libraries guiding principle.
So Jest and React Testing Library.
What Should We Test?
Two things:
- Use Case Coverage > Code Coverage
- The more your tests resemble the way your software is used, the more confidence they can give you.
You should always endeavor to test things that a user can test manually (use cases as opposed to implementation). An example is to test whether the DOM renders the heading “Welcome To The Admin Dashboard” when an admin user is logged in as opposed to whether the <Admin /> component is mounted and the <Landing /> component unmounted or more absurdly whether the global logged in context changes to “admin” from null. This is a case against shallow rendering and snapshot testing.
Read this post for more.
Testing Terminology
- Software Testing: A process, to evaluate the functionality of a software application with an intent to find whether the developed software meet the specified requirements or not and to identify the defects to ensure that the product is defect-free in order to produce a quality product.
- Test runner: A library or tool that picks up software tests in a source code directory and then executes them and writes the test results to the console or log files. Like Jest
- Test Suite: A collection of test cases that are intended to be used to test a software program to show that it has some specified set of behaviors. Like Jest’s Describe Block
- Test Setup and Teardown: Setup work that needs to happen before and after tests run. Like Jest’s beforeEach and afterEach
- Test Case: A specification of the inputs, execution conditions, testing procedure, and expected results that define a single test to be executed to achieve a particular software testing objective. Multiple test cases make up a single Test Suite. Like Jest’s it/test block
- Mocking: Creating objects that simulate the behavior of real objects(including functions, props, react hooks e.t.c). Like Jest Mocks
- Render/Execution: execute the software being tested in an artificial environment. Like react-testing-library’s render function and JSDOM
- DOM node queries: Utility to query for DOM nodes/elements. Like react-testing-library’s getByText, queryByText and getByRole.
- Assertion: An expression which encapsulates some testable logic specified about a target under test. Has the anatomy: expect(selector/query).matcher function (i.e expect (a certain element).to have a certain property).
- Expect block: Represents a test assertion. Like Jest’s expect block
- Matcher Function: Represents a test expectation. Like Jest-dom’s toBeInTheDocument()
- Actions: such as firing events or calling functions. like react-testing-library’s fireEvent
Test Anatomy
See the gist below. Citations and annotations available inline.
Setup React With Jest and React Testing Library
Disclaimer/Prerequisite:
- We are going to use the create-react-app cli for bootstrapping (which ships with Jest pre-installed). For manual React, Webpack, Babel setup there are tutorials for setup from scratch out there. Like this one
- It is assumed that you have Node.js and Yarn package manager installed.
- I prefer to name my unit tests with “it” instead of “test” but you could use either.
Steps:
cd
into your projects folder and create a new React project.
npx create-react-app react_jest_react_testing_library #npx is an npm package runner that ships with npm
2. cd into the project
cd react_jest_react_testing_library
3. Open the project in your favorite IDE/editor
code . #for vscode
3.1. If you are you are as lucky as me, when you open package.json you’ll notice something strange: create-react-app now ships with jest and react-testing-library pre-installed! If so, you can safely skip steps 4 and 5 below.
4. Install react-testing-library and custom jest matchers (jest-dom)
yarn add --dev @testing-library/react @testing-library/jest-dom
5. Add the following file to the /src directory. The contents of this file are run before every test automatically by jest.
5. You’ll also notice that there’s an App.test.js file in the /src directory. This is a test file pre-written to pass for the default create-react-app template.
6. Run tests with the command below. Select ‘a’ to run all tests if prompted.
yarn test
7. You should see all tests passing with semblance to the following screenshot.
Writing React Tests With React-Testing-Library and Jest
0. Write a Testable App
Let’s write a simple app to be used as our test prototype.
Basically, the app shows a “You are Logged out!” text, a button with “Log In” text and a button with “Generate A Random Quote” text when launched by default. It shows randomly generated quotes when you click the “Generate A Random Quote” button. The “You are Logged out!” text changes to “You are …” and then to “You are Logged In!” when you click the “Log In” text. The “Log In” text in the button changes to “Loading…” and then to “Log Out” when you click the “Log In” text. Vice versa happens to the “You are Logged In!” text and “Log Out” button when you click the “Log Out” button.
i. Change App.js to:
ii. Add a /components folder inside /src with the following two files
a) LoggedIn.js - Shows whether logged in/loading and has functionality to log out.
b) LoggedOut.js - Shows whether logged out/loading and has functionality to log in.
iii. Add a /utils folder inside /src with the following two files
a) useLogin.js - Custom react hook to fake network calls for logging in and out.
b) getRandomQuote.js - Returns one of eleven quotes randomly.
1. What Do We Want to Test (Test Cases)?
- Default State: By default, it shows “You are Logged out!”, a button with “Log In” text, a button with “Generate Random Quote” text, and no quote.
- Generating Quote: When you click the “Generate Random Quote” button, a new quote is shown and when you click again a different one is shown.
- Log In Process: When you click on the “Log In” button, it changes text to “Loading…” and shows the login state as “You are …”. The button then finally changes to “Log Out” and the text to “You are Logged In!”
- Log Out Process: When you click on the “Log Out” button, it changes text to “Loading…” and shows the login state as “You are …”. The button then finally changes to “Log In” and the text to “You are Logged out!”
2. Write and Run Actual Tests
N/B: Since we are testing the user behavior (Cases coverage) and not the implementation (Code Coverage), you can safely ignore the utils and components folder and instead concentrate on what the DOM tree renders (what the app renders when you yarn start). You can write test cases for the app without ever looking at the implementation!.
- I had to bump the pre-installed react testing library and jest-dom to the latest versions and install jest-environment-jsdom-sixteen to make waitFor and waitForElementToBeRemoved which are new to the API work. You Probably don’t need to because this is in the future and the create react app developers have bumped up the internals. If so, skip steps 1 and 2 and head to step 3.
yarn add jest-environment-jsdom-sixteen --dev && yarn add --dev @testing-library/react @testing-library/jest-dom
2. Then change the test command in package.json to run with the installed jest-environment-jsdom-sixteen
“test”: “react-scripts test — env=jest-environment-jsdom-sixteen”,
3. Open app.test.js file inside /src and edit it as below. Citations and annotations available inline.
N/B: It’s imperative that you name your test files ending with .test.js or .spec.js. This is how jest finds them.
2. Re-run the tests and watch them pass 🎉
yarn test
Appendix (for those not cited inline)
- https://hackernoon.com/what-should-we-test-reactjs-components-647ded674928
- https://reactjs.org/docs/testing.html
- https://medium.com/javascript-scene/unit-testing-react-components-aeda9a44aae2
- https://blog.usejournal.com/getting-started-with-jest-react-testing-library-a1fae4efce21
- https://kentcdodds.com/blog/write-fewer-longer-tests