Writing Tests
In this section, we'll write tests to confirm the behavior of the Todo App.
Let us see where we are currently. Run the dev server and open the respective dev URL, as Vite shows in your browser.
Also, buckle up. This section is going to be a long ride.
Render Test
As frontend developers, we find it easier to work with visible things. We already know that the component is visible in the browser.
But why do we even need to test it? Writing tests for this seems futile. Right now, the component is straightforward. But, as the component grows, it will become more complex and we cannot be sure that any new changes will not cause regressions.
So, writing tests to confirm our current behavior is a good idea. Let us write a test to verify that the input and button are rendering in the browser.
Test Structure
Create a new App.test.tsx
file in the src
folder. This file will pretty much contain the tests for the entire App component.
We previously wrote a sample test for the sum
function. Every test should have a meaningful name. The test name should describe what the test is doing. In this case, we are testing if the App component renders the input field and the add button. (read more about test API here)
Describe is used to group tests. It is not necessary to use describe, but it is good practice to group tests. It helps organize the tests and debug them. (read more about describe API here)
Render Component for Testing
We need to render the component in the test. We can use the render
function from @testing-library/react
to render the component. (read more about render API here)
Query the DOM
We need to query the DOM to check if the input field and the button are present.
We can use various queries. In general, following the guidelines of RTL (read the priority here) for the query priority order is a good idea.
To summarise at a high level, we need to use the queries closest to the user. In our case:
- We'll use
getByRole
to query the input and the button.
Assert
We have the elements. Now, we need to assert that they are present in the DOM. We can use the expect
function from vitest
to assert. (read more about expect API here)
And also, use the toBeDefined
matcher to check if the element is defined. (read more about toBeDefined matcher here)
Run the Test
Now, run the test and see if it passes.
The test should pass with an output like this:
Improving the Test
Now, toBeDefined
is not the best matcher to use here. We can use toBeInTheDocument
matcher from @testing-library/jest-dom
to check if the element is present in the DOM. (read more about toBeInTheDocument matcher here)
- Read this StackOverflow answer here to understand the difference between
toBeDefined
andtoBeInTheDocument
.
So, let us install @testing-library/jest-dom
and use the toBeInTheDocument
matcher.
We will import the vitest
compatible version of @testing-library/jest-dom
from @testing-library/jest-dom/vitest
. (read more about vitest compatible version here)
And it would import the entire thing.
Rerun the test, and it should pass.
The jest-dom
import (line 3 in the above snippet) usually happens in a separate setup file for all the tests in our application. We will do that now as a good practice.
Create a new file, setupTests.ts
, at the project's root and add the import.
And remove the import from the test file.
We must also update our vite.config.ts
to use this setup file. (read more about setup files here)
Rerun the test, and it should pass. But you'll see an error related to TypeScript despite the test passing.
This error is because TypeScript does not include the setupTests.ts
file in our project. Let's update our tsconfig.json
to include this file.
Save the file, and the error should go away.
Great, we wrote our first test, which actually made sense to test. But we are not done yet. We need to test the functionality of the component further.
Before we move on, let us use an eslint plugin to help us write better tests. Install eslint-plugin-testing-library
and eslint-plugin-jest-dom
as dev dependencies. These plugins will help us write better tests.
And let us add them to our .eslintrc.cjs
file.
Testing the user interaction
Let us write a test to check if the user can add a task to the list.
So, how can we test user interaction? If we look at the docs of Testing Library
, you can see a section for User Actions
under the Core API
and a page for Firing Events
. Here's the exact link to the page.
Inputs are usually changed, and buttons are clicked. We pretty much end up with something like this:
Now, if you run your tests, they should be passing! But a couple of things are not good here.
- First, we still use the old rendered component from the previous test. This old component can cause issues if we are not careful. So, we need to render a new component again.
You might try doing something like the above! But that will fail the tests with this error:
The error might be intimidating at first, but it is constructive. It tells us that there are multiple elements with the exact text. This error happened because we did not clean up
our first test's render. So, the second render had the first test's elements as well. So, we must clean up the first render and subsequent renders.
Now, our ESLint rules would complain that:
cleanup is performed automatically by your test runner, you don't need manual cleanups.
You can read more about this here, figure out why, and find a solution. Here's the link
In short, ESLint assumes we have a global afterEach
declared. But we do not! There are multiple ways to fix this issue. We will solve it by moving our clean-up logic to the global setup file.
And clean up our ./src/App.test.tsx
file
This change is also good because we are not repeating the same code in every test, and even if someone forgets to clean up, it will be taken care of.
Now, run the tests, and they should pass. But there is still a problem:
- To amplify the problem significantly, let us make the input field
readonly
and see if our tests still pass.
Our tests will still pass! But, we know that the user cannot type in the input field. What we have right now is a false positive.
We could have avoided this problem by testing user interaction differently. Even the docs recommend another library called user-event
for testing user interaction. Here's the link to the docs
Let us install user-event
and use it in our tests.
Since user interactions are asynchronous, we need to use async
, await
, and waitFor
from @testing-library/react
for the changes to take effect. (read more about async methods)
Our tests should fail. Because we are using readOnly
on the input field, remove the readOnly
prop from the input field and rerun the tests.
Our tests should pass now. We also have a more robust test for user interaction.
Now, we have made a decent amount of progress on tests. Remember where we left our actual business logic? In the next section, let us return to that with TDD
(Test Driven Development).
At this point, your code should be a good match to the branch of the repository: 5-writing-tests