Testing a normal React App
Introduction
Bootstrap a React app with Create React App by running the following command:
1 npx create-react-app GIVEN-PROJECT-NAME
Introduction
- What is Testing and why
- What to test and how to test
- Testing React Components
What is Testing and why
- When writing code, we test manually by running the code and checking the output. This is called manual testing.
- Manual testing is time consuming and error prone.
- Automated testing is the process of writing code to test the code.
- It is faster and more reliable than manual testing.
- Automated testing tests individual units of code and also the entire application.
Categories of Automated Testing
- Unit Testing
- Tests individual units of code in isolation
- For example, a function or a class
- It is the smallest type of testing and the most common
- Integration Testing
- Tests how different units of code work together
- For example, testing how a function works with a database
- It is more complex than unit testing
- End-to-End Testing(E2E Testing)
- Tests the entire application from start to finish
- For example, testing how a user interacts with the application like login, sign up, etc.
- It is the most complex type of testing
What to test and how to test
What to test
- Test the diferent building blocks of the application
- small units of code focused on a single task
How to test
- Test success and failure cases
- Rare edge cases
Testing React Components
For testing,
- we need a tool for running our tests and asserting the results like Jest.
- And a tool for simulating the browser environment React like testing-library/react.
Jest
- Jest is a JavaScript testing framework maintained by Facebook.
- It is used to test JavaScript code including React applications.
- It is fast, easy to use and has a lot of features.
Testing a component
- We will test a simple component called
Button
which takes alabel
prop and renders a button with the label.
1
2
3
4
5
6
7
8
// Button.js
import React from "react";
const Button = ({ label }) => {
return <button>{label}</button>;
};
export default Button;
- We will write a test for the
Button
component using Jest and testing-library/react.
1
2
3
4
5
6
7
8
9
10
// Button.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import Button from "./Button";
test("renders a button with the label", () => {
render(<Button label="Click me" />);
const button = screen.getByText("Click me");
expect(button).toBeInTheDocument();
});
Letโs break down the test.
- We need to name the test file starting with the name of the component followed by
.test.js
likeButton.test.js
. - We import
render
andscreen
from@testing-library/react
to render the component and query the DOM. - We import the
Button
component we want to test. - We write a test using the
test
function provided by Jest.- The first argument is the description of the test.
- The second argument is the test function.
- We need to name the test file starting with the name of the component followed by
In the test function,
- We render the
Button
component with thelabel
prop set to"Click me"
. - We query the DOM for the button with the label
"Click me"
usingscreen.getByText
. - We assert that the button is in the document using
expect(button).toBeInTheDocument()
. - If the button is not in the document, the test will fail.
- If the button is in the document, the test will pass.
- We render the
- To run the test, we use the
test
script inpackage.json
which runsjest
. - In our example, we include the
test
script inpackage.json
like this.
1
2
3
4
5
{
"scripts": {
"test": "jest"
}
}
We run the test using the command
npm test
in the terminal.- Jest will run the test and show the result in the terminal.
- If the test passes, Jest will show a green checkmark.
- If the test fails, Jest will show a red cross.
Jest will also show the number of tests passed and the number of tests failed.
- To simulate a failure, we can change the label in the
Button
component to"Click us!"
. - When we run the test, Jest will show a red cross and the error message.
Additionally,
- Jest will show the difference between the expected and received values.
- The line and position of the error in the test file.
- We can quit test mode by pressing
ctrl + c
in the terminal.
Testing Suites
- We can group related tests into a
suite
using thedescribe
function provided by Jest.- the first argument is the description of the suite.
- the second argument is a function containing the tests.
- A suite can contain multiple tests and other suites.
1
2
3
4
5
6
7
8
9
10
11
12
// Button.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import Button from "./Button";
describe("Button", () => {
test("renders a button with the label", () => {
render(<Button label="Click me" />);
const button = screen.getByText("Click me");
expect(button).toBeInTheDocument();
});
});
- In the above example, we group the test for the
Button
component into a suite calledButton
. - Now when we run the test, Jest will show the suite name and the test name in the terminal.
Testing User Interaction
- We are now going to test a component called
Counter
which has a button to increment the count.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Counter.js
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
const handleChangeCounter = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleChangeCounter}>Increment</button>
</div>
);
};
export default Counter;
- We will write a test for all possible user interactions with the
Counter
component:- Renders the count and the button
- Increments the count when the button is clicked
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Counter.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Counter from "./Counter";
describe("Counter", () => {
test("renders a button with the label", () => {
render(<Counter />);
const count = screen.getByText("Count: 0");
const button = screen.getByText("Increment");
expect(count).toBeInTheDocument();
expect(button).toBeInTheDocument();
});
test("increments the count when the button is clicked", () => {
render(<Counter />);
const count = screen.getByText("Count: 0");
const button = screen.getByRole("button", { name: "Increment" });
userEvent.click(button);
expect(count).toHaveTextContent("Count: 1");
});
});
Testing connected components
- Letโs say our app.jsx file looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useState } from "react";
const Greeting = () => {
const [changedText, setChangedText] = useState(false);
const handleTextChange = () => {
setChangedText(true);
};
return (
<div>
<h1>Hello, World!</h1>
{changedText ? <p>Changed!</p> : <p>Welcome to my first Test app.</p>}
<button onClick={handleTextChange}>Change Text</button>
</div>
);
};
export default Greeting;
- A greeting component that has a button to change the text.
- Our test file for the Greeting component looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Greeting from "./Greeting";
describe("Greeting component", () => {
test("renders hello world as a text", () => {
// Arrange
render(<Greeting />);
// Act
// ... nothing
// Assert
const helloWorldElement = screen.getByText("Hello, World!");
expect(helloWorldElement).toBeInTheDocument();
});
test("renders Welcome to my first Test app if the button was NOT clicked", () => {
render(<Greeting />);
const paragraphElement = screen.getByText("Welcome to my first Test app.", {
exact: false,
});
expect(paragraphElement).toBeInTheDocument();
});
test("renders Changed! if the button was clicked", () => {
render(<Greeting />);
// Act
const buttonElement = screen.getByRole("button");
userEvent.click(buttonElement);
// Assert
const outputElement = screen.getByText("Changed!");
expect(outputElement).toBeInTheDocument();
});
test("does not render Welcome to my first Test app if the button was clicked", () => {
render(<Greeting />);
// Act
const buttonElement = screen.getByRole("button");
userEvent.click(buttonElement);
// Assert
const outputElement = screen.queryByText("Welcome to my first Test app.", {
exact: false,
});
expect(outputElement).toBeNull();
});
});
- Now letโs create another component that:
- accepts a prop
- renders the children of the prop
1
2
3
4
5
6
7
import React from "react";
const Text = (props) => {
return <p>{props.children}</p>;
};
export default Text;
- We will import the Text component into the Greeting component and pass the text as a prop.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useState } from "react";
import Text from "./Text";
const Greeting = () => {
const [changedText, setChangedText] = useState(false);
const handleTextChange = () => {
setChangedText(true);
};
return (
<div>
<h1>Hello, World!</h1>
{changedText ? (
<Text>Changed!</Text>
) : (
<Text>Welcome to my first Test app.</Text>
)}
<button onClick={handleTextChange}>Change Text</button>
</div>
);
};
- With such a simple component, we MUST not write a test for the Text component.
- Howerver, if the Text component was a complex component with its own state for example, then we would have to write a test for it.
Testing Asynchronous Code
- Now letโs say we have a component that fetches data from an API and renders it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { useEffect, useState } from "react";
const Async = () => {
const [posts, setPosts] = useState([]);
const fetchPosts = async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const data = await response.json();
setPosts(data);
};
useEffect(() => {
fetchPosts();
}, []);
return (
<div>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
};
export default Async;
- We will write a test for the
Async
component using Jest and testing-library/react.
1
2
3
4
5
6
7
8
9
10
import { render, screen } from "@testing-library/react";
import Async from "./Async";
describe("Async component", () => {
test("renders a list of posts", async () => {
render(<Async />);
const listElement = await screen.findAllByRole("listitem");
expect(listElement).not.toHaveLength(0);
});
});
In the test function,
- We render the
Async
component. - We query the DOM for the list items using
screen.findAllByRole
. - We assert that the list items are not empty using
expect(listElement).not.toHaveLength(0)
. - We use
await
to wait for the list items to be rendered.
- We render the
- The above implementation is not ideal because it makes a real API call.
- It is expensive for our resources.
- It is also unreliable because the API might be down or slow.
- It is therefore better to mock the API call.
Mocking
- Mocking is the process of replacing a function with a fake function.
- We use mocking to isolate the code we are testing from the rest of the application.
- We can mock functions, modules, and objects.
We can also mock the return value of a function.
- Letโs mock the test for the
Async
component to isolate the component from the API.- We will use the
window.fetch
function to fetch the data from the API. - The
window.fetch
function will use thejest.fn
function to create a mock function. - We will use
mockResolvedValueOnce
to mock the return value of thewindow.fetch
function. - then we will use
mockImplementation
to mock theresponse.json
function.
- We will use the
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { render, screen } from "@testing-library/react";
import Async from "./Async";
describe("Async component", () => {
test("renders a list of posts", async () => {
window.fetch = jest.fn();
window.fetch.mockResolvedValueOnce({
json: async () => [
{ id: 1, title: "Post 1" },
{ id: 2, title: "Post 2" },
],
});
render(<Async />);
const listElement = await screen.findAllByRole("listitem");
expect(listElement).not.toHaveLength(0);
});
});
This post is licensed under CC BY 4.0 by the author.