Implementing "Complete Task" Feature
You will practice more with unit tests, and learn a few techniques that are useful when testing React apps.
We'll cover the following
Where we left off
In the last lesson, we decided that our TODO app needs to be able to mark tasks as “completed” by clicking on them. The integration test that tests for this behaviour is currently failing, and we will begin fixing it now.
Task
component
Stop and think about the behaviour we are going to implement. It assigns some of the responsibility (reacting to user input, e.g., mouse click) to individual tasks. Right now, we only have the TaskList
component, which is responsible for displaying all of the tasks. To adhere to the single responsibility principle, we will create another component: Task
. It is responsible for displaying a single task and reacting to user interaction for this task.
As usual, we will first write failing unit tests, and fix them with implementation. We will need to test (at least) three aspects of this component:
- It must display the task.
- It must communicate its completion (
completed
CSS class). - It must react to user interaction.
Create a file Task.test.js
in src/components
with these placeholders:
import React from 'react'
import Task from './Task';
import {render, screen, fireEvent} from '@testing-library/react'
describe('<Task />', () => {
it('renders the task', () => {
});
});
To check if Task
renders the task we ask it to, we need to think about the way we inject the task into the component. Right now, we store the tasks as an array of strings, where each element of the array represents a task. Since we will be adding the completion property to tasks, a new data structure is called for. Without any unnecessary complication, I suggest using a JS object like this:
{
label: 'Buy milk',
completed: false
}
This should be pretty self-explanatory. Every task is now a JS object, with the label
key being the task content, and the completed
boolean representing the task completion. To make our life easier when testing, create these two placeholder tasks in the describe
block of the test:
const completedTask = {
label: 'Do this',
completed: true
};
const uncompletedTask = {
label: 'Do that',
completed: false
};
Test task display
Now, testing for the task content becomes a piece of cake. We will assume that the Task
component will accept a prop task
, which is the JS object. Then we will look for this text in the rendered result:
it('renders the task', () => {
render(<Task task={completedTask}/>);
expect(screen.getByText(completedTask.label)).toBeInTheDocument();
});
Fix task display
This test will fail due to a number of reasons. The first being that the Task
component is not yet created. Let’s fix that by creating a Task.js
file under src/components
with this content:
import React from 'react';
const Task = ({task: {label}}) => (
<li>{label}</li>
);
export default Task;
This simple component does two things for now: take in the task
prop, and display the label
wrapped by ul
. Though the code is simple, it fixes the failing unit test, thus marking another iteration of Test-Driven-Development. Now, it is time to write another test.
Test task completion
As you recall, we want to show whether the task is completed or not by using the CSS class completed
. We already created the completedTask
and uncompletedTask
objects, so all that’s left to do is render both, and assert on the CSS class:
it('assigns completed class', () => {
render(<Task task={completedTask} />);
expect(screen.getByText(completedTask.label)).toHaveClass('completed');
render(<Task task={uncompletedTask} />);
expect(screen.getByText(uncompletedTask.label)).not.toHaveClass('completed');
});
The
toHaveClass
matcher comes from thejest-dom
library, which is included by CRA. You can see other useful functions fromjest-dom
here
Fix task completion
To fix this test, you will need to:
- Extract the
completed
key from the task. - Assign a CSS class based on the
completed
key.
This code will do exactly that:
const Task = ({task: {label, completed}}) => (
<li className={completed ? 'completed' : null}>{label}</li>
);
Exercise
Now it is time for your second hands-on exercise. We are almost done developing the Task
component, but it still misses one important feature. We must be notified every time a user clicks on it, so the controlling component will mark tasks as completed. To be more precise, we want the Task
component to accept a callback function via prop onToggle
, and call this function every time the user clicks on a task. Use this integrated environment to write the test, and then fix the test by writing the implementation. You will see one of the correct solutions in the next lesson.
Get hands-on with 1400+ tech skills courses.