The introduction
In 2020, building a Web application is easy. There are plenty of good front-end frameworks out there (React, Vue, Angular); And some easy-to-use and powerful UI libraries, such as Ant Design, that greatly shorten the application build cycle.
But the Internet age has also dramatically changed the way much software is designed, developed and distributed. The problem for developers is that with more and more demands, more and more complex applications, every now and then there’s a feeling of losing control and Shouting, “I’m too south!” . In serious cases, I can change a line of code without knowing the extent of its impact. In this case, testing is needed to ensure the quality and stability of our application.
Let’s learn how to write unit tests for the React application 🎁
What kind of testing is required
There are levels of software testing. The following is an excerpt of the definition of testing certification levels in the Book “The Google Way of Software Testing” :
- Level 1
- Use test coverage tools.
- Use continuous integration.
- The test was classified into small, medium, and large.
- Create a smoke test set (main process test case).
- Flag which tests are nondeterministic (test results are not unique).
- Level 2
- If any test runs are red (failed ❌), they will not be published.
- A smoke test is required before each code submission. (Self-test, simply walk down the main process)
- Overall code coverage for each type is greater than 50%.
- Coverage for small tests is greater than 10%.
- Level 3
- All significant code changes are tested.
- Coverage for small tests is greater than 50%.
- New important functions must be verified by integration tests.
- Level 4
- Smoke tests are automatically run before committing any new code.
- The smoke test must be completed within 30 minutes.
- There is no test of uncertainty.
- Overall test coverage should not be less than 40%.
- Code coverage for small tests should be no less than 25%.
- All important functions should be verified by integration tests.
- Level 5
- Add a test case for each critical defect fix.
- Actively use available code analysis tools.
- The overall test coverage should not be less than 60%.
- Code coverage for small tests should not be less than 40%.
Small tests, also known as unit tests, are generally automated. Used to verify that a single function, component, or function module behaves as expected.
What matters to the developer is the action of testing. This article focuses on unit testing the React component, which is intended to put developers in the user’s shoes. Make sure every function of your component works by testing it, and focus on quality rather than letting users test it for you.
When writing unit tests, you have to make repeated adjustments to the previous code, although the process is painful, and the quality of the components is improving bit by bit.
Technology stack selection
When writing unit tests for React applications, the official recommendation is to use React Testing Library + Jest. Enzyme is also an excellent unit test library. Which test tool should we choose?
Let’s look at a simple counter example and two corresponding tests: the first written using Enzyme, and the second written using the React Testing Library.
counter.js
// counter.js
import React from "react";
class Counter extends React.Component {
state = { count: 0 };
increment = () => this.setState(({ count }) => ({ count: count + 1 }));
decrement = () => this.setState(({ count }) => ({ count: count - 1 }));
render() {
return( <div> <button onClick={this.decrement}>-</button> <p>{this.state.count}</p> <button onClick={this.increment}>+</button> </div> ); }}export default Counter;Copy the code
counter-enzyme.test.js
// counter-enzyme.test.js
import React from "react";
import { shallow } from "enzyme";
import Counter from "./counter";
describe("<Counter />", () => {
it("properly increments and decrements the counter", () => {
const wrapper = shallow(<Counter />);
expect(wrapper.state("count")).toBe(0);
wrapper.instance().increment();
expect(wrapper.state("count")).toBe(1);
wrapper.instance().decrement();
expect(wrapper.state("count")).toBe(0);
});
});Copy the code
counter-rtl.test.js
// counter-rtl.test.js
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import Counter from "./counter";
describe("<Counter />", () => {
it("properly increments and decrements the counter", () => {
const { getByText } = render(<Counter />);
const counter = getByText("0");
const incrementButton = getByText("+");
const decrementButton = getByText("-");
fireEvent.click(incrementButton);
expect(counter.textContent).toEqual("1");
fireEvent.click(decrementButton);
expect(counter.textContent).toEqual("0");
});
});Copy the code
Comparing the two examples, can you see which test file is the best? If you’re not familiar with unit testing, you might be able to do both. But actually the Enzyme implementation has two risks of false positives:
- Even if the code is broken, the test will pass.
- Even if the code is correct, the test will fail.
Let’s illustrate these two points. Suppose you want to refactor the component because you want to be able to set any count value. Therefore, you can remove the increment and decrement methods, and then add a new setCount method. Suppose you forgot to connect this new method to a different button:
counter.js
// counter.js
export default class Counter extends React.Component {
state = { count: 0 };
setCount = count => this.setState({ count });
render() {
return( <div> <button onClick={this.decrement}>-</button> <p>{this.state.count}</p> <button onClick={this.increment}>+</button> </div> ); }}Copy the code
The first test (Enzyme) will pass, but the second test (RTL) will fail. In fact, the first one doesn’t care if the button is properly connected to the method. It only looks at the implementation itself, that is, whether the state of the application is correct after your increment and decrement methods are executed. This is code corruption and the test will pass.
It’s 2020, and you’ve probably heard of React Hooks and are planning to rewrite our counter code using React Hooks:
counter.js
// counter.js
import React, { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count => count + 1);
const decrement = () => setCount(count => count - 1);
return (
<div>
<button onClick={decrement}>-</button>
<p>{count}</p>
<button onClick={increment}>+</button>
</div>
);
}Copy the code
This time, the first test will be broken even if your counter is still working. State cannot be used in a function component:
ShallowWrapper::state() can only be called on class componentsCopy the code
Next, we need to rewrite the unit test file:
counter-enzyme.test.js
import React from "react";
import { shallow } from "enzyme";
import Counter from "./counter";
describe("<Counter />", () => {
it("properly increments and decrements the counter", () => {
const setValue = jest.fn();
const useStateSpy = jest.spyOn(React, "useState");
useStateSpy.mockImplementation(initialValue => [initialValue, setValue]);
const wrapper = shallow(<Counter />);
wrapper
.find("button")
.last()
.props()
.onClick();
expect(setValue).toHaveBeenCalledWith(1);
// We can't make any assumptions here on the real count displayed // In fact, the setCount setter is mocked! wrapper .find("button") .first() .props() .onClick(); expect(setValue).toHaveBeenCalledWith(-1); }); });Copy the code
Unit tests written using the React Testing Library will still work because they focus more on application event handling and presentation. Not the implementation details of the application, and state changes. More in line with our original aspirations for unit testing and best practices.
Simple rules to follow
Perhaps the unit test example above, written with the React Testing Library, still feels confused. Let’s take this piece of code apart step by step using AAA mode.
AAA mode: Arrange, Act, Assert.
Almost all tests are written this way. First, you choreograph (initialize) your code so that everything is ready for the next steps. Then, you perform the steps that a user should perform (such as clicking). Finally, you assert what should happen.
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import Counter from "./app";
describe("<Counter />", () => {
it("properly increments the counter", () => {
// Arrange
const { getByText } = render(<Counter />);
const counter = getByText("0");
const incrementButton = getByText("+");
const decrementButton = getByText("-");
// Act
fireEvent.click(incrementButton);
// Assert
expect(counter.textContent).toEqual("1");
// Act
fireEvent.click(decrementButton);
// Assert
expect(counter.textContent).toEqual("0");
});
});Copy the code
Arrange
In this choreography step, we need to accomplish two tasks:
- Rendering component
- Get the different elements of the DOM as required.
The render component can be done using RTL’s API’s Render method. Signed as follows:
functionrender( ui: React.ReactElement, options? : Omit<RenderOptions,'queries'>
): RenderResultCopy the code
The UI is the component that you load. Options Usually you do not need to specify options. The official document is here, and if specified, the following values are a simple extract of the official document:
- The Container: React Testing library creates a div and appends it to the document. With this parameter, you can customize the container.
- BaseElement: Defaults to this value if a container is specified, or document.documentElement otherwise. This will be used as the base element of the query, as well as what is printed when using DEBUG ().
- Hydrate: For server-side rendering, use
ReactDOM.hydrate
Load your component. - Wrapper: Pass a component as a wrapper and render the component we want to test in it. This is typically used to create custom render functions that can be reused to provide commonly used data.
- Queries: queries are bound. The default Settings in the DOM test library are overridden unless merged.
Basically, all this function does is render the component using the ReactDOM. Render in a newly created div directly attached to document.body (or hydrate for server-side rendering). As a result, a large number of queries can be obtained from DOM test libraries and other useful methods such as Debug, rerender, or unmount.
Documents: testing-library.com/docs/dom-te…
But you might be wondering, what are these problems? Some utilities allow you to query the DOM as a user would: find elements by tag text, placeholders, and titles. Here are some examples of queries from documentation:
- GetByLabelText: Searches for a label that matches the given text passed as a parameter, and then finds the element associated with that label.
- GetByText: Searches for all elements with text nodes where textContent matches the given text passed as a parameter.
- GetByTitle: Returns an element with a title attribute that matches the given text passed as an argument.
- GetByPlaceholderText: Searches for all elements that have placeholder attributes and finds elements that match the given text passed as a parameter.
There are many variations on a particular query:
- GetBy: Returns the first matching node of the query and throws an error if there are no matching elements or more than one match is found.
- GetAllBy: Returns an array of all matched nodes in a query, throwing an error if there are no matched elements.
- QueryBy: Returns the first matching node of the query, or NULL if there is no matching element. This is useful for asserting elements that don’t exist.
- QueryAllBy: Returns an array of all matched nodes of a query, or an empty array ([]) if there are no matched elements.
- FindBy: Returns a promise that will be resolved when an element matching a given query is found. If no elements are found, or if more than one element is found after the default timeout is 4500 milliseconds, the promise will be rejected.
- FindAllBy: Returns a promise that will be resolved into an array of elements when any elements that match a given query are found.
Enforcement (Act)
Now everything is ready and we can move. For this, we spent most of our time using fireEvent from the DOM test library, signed as follows:
fireEvent(node: HTMLElement, event: Event)Copy the code
Simply put, this function takes a DOM node (you can query it using the query you saw above!). And trigger DOM events such as click, focus, change, and so on. You can find many other events that can be scheduled here.
Our example is fairly simple because we just want to click a button, so we just need:
fireEvent.click(incrementButton);
// OR
fireEvent.click(decrementButton);Copy the code
Assert
Here’s the last part. Triggering events usually trigger some changes in the application, so we must perform some assertions to ensure that these changes occur. In our tests, a good way to do this was to ensure that the count presented to the user had changed. Therefore, we simply assert that the counter of the textContent property is increasing or decreasing:
expect(counter.textContent).toEqual("1");
expect(counter.textContent).toEqual("0");Copy the code
Congratulations, you have dismantled our example at this point. 🥳
Note: This AAA pattern is not specific to the test library. In fact, it’s even the general structure of any test case. I show you this here because I find it interesting how easy the test library is to write tests in each section.
Eight typical examples
Rts-guide-demo rTS-Guide-Demo rTS-guide-Demo
Take a quick look at our project while you install the dependencies. The SRC /test directory holds all the unit test related files. Let’s clear this folder and walk through the following examples again. 🙏 (CV is also available 👌)
1. How do I create a test snapshot
Snapshots, as the name suggests, allow us to save a snapshot of a given component. It provides a lot of help when you are updating or refactoring and want to capture or compare changes.
Now, let’s take a snapshot of the app.js file.
App.test.js
import React from 'react'
import {render, cleanup} from '@testing-library/react'
import App from '.. /App'
afterEach(cleanup)
it('should take a snapshot', () => {
const { asFragment } = render(<App />)
expect(asFragment()).toMatchSnapshot()
})
Copy the code
To get a snapshot, we first have to import Render and cleanup. Both methods will be used extensively in this article.
Render, as the name suggests, helps render the React component. Cleanup is passed to afterEach as a parameter to clean everything up afterEach test to avoid memory leaks.
Next, we can render the App component with Render and get the asFragment from the method as the return value. Finally, make sure the App component fragment matches the snapshot.
Now, to run the tests, open your terminal and navigate to the root of your project, and run the following command:
npm testCopy the code
So it will create a new folder __snapshots__ and a file app.test.js:
App.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should take a snapshot 1`] = `
<DocumentFragment>
<div
class="App"
>
<h1>
Testing Updated
</h1>
</div>
</DocumentFragment>
`;Copy the code
If you make changes in app.js, the test will fail because the snapshots will no longer match. To update a snapshot, press U or delete the snapshot file.
2. Test the DOM element
To test the DOM element, you must first look at the TestElements.js file.
TestElements.js
import React from 'react'
const TestElements = () => {
const [counter, setCounter] = React.useState(0)
return (
<>
<h1 data-testid="counter">{ counter }</h1>
<button data-testid="button-up" onClick={() => setCounter(counter + 1)}> Up</button>
<button disabled data-testid="button-down" onClick={() => setCounter(counter - 1)}>Down</button>
</>
)
}
export default TestElements
Copy the code
The only thing you need to keep here is the data-testid. It will be used to select these elements from the test file. Now, let’s complete the unit tests:
Test whether the counter is 0 and the disabled state of the button:
TestElements.test.js
import React from 'react';
import "@testing-library/jest-dom/extend-expect";
import { render, cleanup } from '@testing-library/react';
import TestElements from '.. /components/TestElements'
afterEach(cleanup);
it('should equal to 0', () => {
const { getByTestId } = render(<TestElements />);
expect(getByTestId('counter')).toHaveTextContent(0)
});
it('should be enabled', () => {
const { getByTestId } = render(<TestElements />);
expect(getByTestId('button-up')).not.toHaveAttribute('disabled')}); it('should be disabled', () => {
const { getByTestId } = render(<TestElements />);
expect(getByTestId('button-down')).toBeDisabled()
});Copy the code
As you can see, the syntax is very similar to the previous tests. The only difference is that we use the getByTestId to select the necessary element (according to the data-testid) and check whether the test has passed. In other words, we check to see if the text content in
{counter}
is equal to 0.
Here, as usual, we use getByTestId to select the element and check the first test if the button disables the property. For the second, if the button is disabled.
If you save the file or run it again in the terminal yarn test, the test will pass.
3. Test events
Before writing unit tests, let’s take a look at what testEvents.js looks like.
import React from 'react'
const TestEvents = () => {
const [counter, setCounter] = React.useState(0)
return (
<>
<h1 data-testid="counter">{ counter }</h1>
<button data-testid="button-up" onClick={() => setCounter(counter + 1)}> Up</button>
<button data-testid="button-down" onClick={() => setCounter(counter - 1)}>Down</button>
</>
)
}
export default TestEvents
Copy the code
Now, let’s write the test.
When we click the button, we test whether the counter increases or decreases correctly:
import React from 'react';
import "@testing-library/jest-dom/extend-expect";
import { render, cleanup, fireEvent } from '@testing-library/react';
import TestEvents from '.. /components/TestEvents'
afterEach(cleanup);
it('increments counter', () => {
const { getByTestId } = render(<TestEvents />);
fireEvent.click(getByTestId('button-up'))
expect(getByTestId('counter')).toHaveTextContent('1')}); it('decrements counter', () => {
const { getByTestId } = render(<TestEvents />);
fireEvent.click(getByTestId('button-down'))
expect(getByTestId('counter')).toHaveTextContent('1')});Copy the code
As you can see, the two tests are very similar except for the expected text content.
The first test fires a click event using fireEvent.click() to check if the counter increases to 1 when the button is clicked.
The second checks whether the counter is reduced to -1 when the button is clicked.
FireEvent has several methods you can use to test events, so you are free to drill down into the documentation to learn more.
Now that we know how to test events, we’ll learn how to handle asynchronous operations in the next section.
4. Test asynchronous operations
Asynchronous operations are operations that take time to complete. It can be HTTP requests, timers, and so on.
Now, let’s examine the testAsync.js file.
import React from 'react'
const TestAsync = () => {
const [counter, setCounter] = React.useState(0)
const delayCount = () => (
setTimeout(() => {
setCounter(counter + 1)
}, 500)
)
return (
<>
<h1 data-testid="counter">{ counter }</h1>
<button data-testid="button-up" onClick={delayCount}> Up</button>
<button data-testid="button-down" onClick={() => setCounter(counter - 1)}>Down</button>
</>
)
}
export default TestAsync
Copy the code
Here, we use setTimeout() to delay the incrementing event by 0.5 seconds.
Test counter to determine whether to increase after 0.5 seconds:
TestAsync.test.js
import React from 'react';
import "@testing-library/jest-dom/extend-expect";
import { render, cleanup, fireEvent, waitForElement } from '@testing-library/react';
import TestAsync from '.. /components/TestAsync'
afterEach(cleanup);
it('increments the counter after 0.5 s', async () => {
const { getByTestId, getByText } = render(<TestAsync />);
fireEvent.click(getByTestId('button-up'))
const counter = await waitForElement(() => getByText('1'))
expect(counter).toHaveTextContent('1')});Copy the code
To test incremental events, we must first use async/await to process the operation because, as mentioned earlier, it takes time to complete.
Next, we use a new helper method, getByText(). This is similar to getByTestId(). GetByText () selects the text content instead of the ID.
Now, after clicking the button, we wait for waitForElement(() => getByText(‘1’) to increment the counter. Once the counter has increased to 1, we can now move to the condition and check if the counter is equal to 1.
That said, let’s now move on to more complex test cases.
Are you ready?
5. Test React Redux
Let’s check what testredux.js looks like.
TestRedux.js
import React from 'react'
import { connect } from 'react-redux'
const TestRedux = ({counter, dispatch}) => {
const increment = () => dispatch({ type: 'INCREMENT' })
const decrement = () => dispatch({ type: 'DECREMENT' })
return (
<>
<h1 data-testid="counter">{ counter }</h1>
<button data-testid="button-up" onClick={increment}>Up</button>
<button data-testid="button-down" onClick={decrement}>Down</button>
</>
)
}
export default connect(state => ({ counter: state.count }))(TestRedux)
Copy the code
store/reducer.js
export const initialState = {
count: 0,
}
export function reducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + 1,
}
case 'DECREMENT':
return {
count: state.count - 1,
}
default:
return state
}
}Copy the code
As you can see, nothing special. It’s just a basic counter component handled by React Redux.
Now, let’s write the unit tests.
Test whether the initial state is 0:
import React from 'react'
import "@testing-library/jest-dom/extend-expect";
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { render, cleanup, fireEvent } from '@testing-library/react';
import { initialState, reducer } from '.. /store/reducer'
import TestRedux from '.. /components/TestRedux'
const renderWithRedux = (
component,
{ initialState, store = createStore(reducer, initialState) } = {}
) => {
return {
...render(<Provider store={store}>{component}</Provider>),
store,
}
}
afterEach(cleanup);
it('checks initial state is equal to 0', () => {
const { getByTestId } = renderWithRedux(<TestRedux />)
expect(getByTestId('counter')).toHaveTextContent('0')
})
it('increments the counter through redux', () => {
const { getByTestId } = renderWithRedux(<TestRedux />,
{initialState: {count: 5}
})
fireEvent.click(getByTestId('button-up'))
expect(getByTestId('counter')).toHaveTextContent('6')
})
it('decrements the counter through redux', () => {
const { getByTestId} = renderWithRedux(<TestRedux />, {
initialState: { count: 100 },
})
fireEvent.click(getByTestId('button-down'))
expect(getByTestId('counter')).toHaveTextContent('99')})Copy the code
We need to import something to test React Redux. Here, we created our own helper function, renderWithRedux(), to render the component, because it will be used many times.
RenderWithRedux () receives the component to render, initial state, and storage as an argument. If there is no store, it creates a new store, and if it does not receive the initial state or store, it returns an empty object.
Next, we use Render () to render the component and pass the storage to the provider.
That is, we can now pass the component TestRedux to renderWithRedux() to test if the counter is equal to 0.
Test whether the increase or decrease of the counter is correct:
To test the increment and decrement events, we pass the initial state as the second argument to renderWithRedux(). Now we can click the button and test whether the expected results meet the criteria.
Now, let’s move on to the next section and introduce the React Context.
6. Test React Context
Let’s examine what textContext.js looks like.
import React from "react"
export const CounterContext = React.createContext()
const CounterProvider = () => {
const [counter, setCounter] = React.useState(0)
const increment = () => setCounter(counter + 1)
const decrement = () => setCounter(counter - 1)
return (
<CounterContext.Provider value={{ counter, increment, decrement }}>
<Counter />
</CounterContext.Provider>
)
}
export const Counter = () => {
const { counter, increment, decrement } = React.useContext(CounterContext)
return (
<>
<h1 data-testid="counter">{ counter }</h1>
<button data-testid="button-up" onClick={increment}> Up</button>
<button data-testid="button-down" onClick={decrement}>Down</button>
</>
)
}
export default CounterProvider
Copy the code
Now, the React Context manages the counter state. Let’s write unit tests to check that it works as expected.
Test whether the initial state is 0:
TextContext.test.js
import React from 'react'
import "@testing-library/jest-dom/extend-expect";
import { render, cleanup, fireEvent } from '@testing-library/react'
import CounterProvider, { CounterContext, Counter } from '.. /components/TestContext'
const renderWithContext = (
component) => {
return {
...render(
<CounterProvider value={CounterContext}>
{component}
</CounterProvider>)
}
}
afterEach(cleanup);
it('checks if initial state is equal to 0', () => {
const { getByTestId } = renderWithContext(<Counter />)
expect(getByTestId('counter')).toHaveTextContent('0')
})
it('increments the counter', () => {
const { getByTestId } = renderWithContext(<Counter />)
fireEvent.click(getByTestId('button-up'))
expect(getByTestId('counter')).toHaveTextContent('1')
})
it('decrements the counter', () => {
const { getByTestId} = renderWithContext(<Counter />)
fireEvent.click(getByTestId('button-down'))
expect(getByTestId('counter')).toHaveTextContent('1')})Copy the code
As in the React Redux section, we use the same method here, creating a helper function named renderWithContext() to render the component. But this time, it only accepts components as parameters. To create a new context, we pass the CounterContext to the Provider.
Now we can test whether the counter was originally equal to 0. So, is the counter increasing or decreasing correctly?
As you can see, here we fire a click event to test that the counter has properly increased to 1 and decreased to -1.
That said, we can now move on to the next section and introduce the React Router.
7. Test the React Router
Let’s examine what testrouter.js looks like.
TestRouter.js
import React from 'react'
import { Link, Route, Switch, useParams } from 'react-router-dom'
const About = () => <h1>About page</h1>
const Home = () => <h1>Home page</h1>
const Contact = () => {
const { name } = useParams()
return <h1 data-testid="contact-name">{name}</h1>
}
const TestRouter = () => {
const name = 'John Doe'
return (
<>
<nav data-testid="navbar">
<Link data-testid="home-link" to="/">Home</Link>
<Link data-testid="about-link" to="/about">About</Link>
<Link data-testid="contact-link" to={`/contact/${name}`}>Contact</Link>
</nav>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/about:name" component={Contact} />
</Switch>
</>
)
}
export default TestRouterCopy the code
Here, you test whether the page information corresponding to the route is correct.
TestRouter.test.js
import React from 'react'
import "@testing-library/jest-dom/extend-expect";
import { Router } from 'react-router-dom'
import { render, fireEvent } from '@testing-library/react'
import { createMemoryHistory } from 'history'
import TestRouter from '.. /components/TestRouter'
const renderWithRouter = (component) => {
const history = createMemoryHistory()
return {
...render (
<Router history= {history}>
{component}
</Router>
)
}
}
it('should render the home page', () => {
const { container, getByTestId } = renderWithRouter(<TestRouter />)
const navbar = getByTestId('navbar')
const link = getByTestId('home-link')
expect(container.innerHTML).toMatch('Home page')
expect(navbar).toContainElement(link)
})
it('should navigate to the about page', ()=> {
const { container, getByTestId } = renderWithRouter(<TestRouter />)
fireEvent.click(getByTestId('about-link'))
expect(container.innerHTML).toMatch('About page')
})
it('should navigate to the contact page with the params', ()=> {
const { container, getByTestId } = renderWithRouter(<TestRouter />)
fireEvent.click(getByTestId('contact-link'))
expect(container.innerHTML).toMatch('John Doe')})Copy the code
To test the React Router, we must first have a navigation history. Therefore, we use createMemoryHistory() to create the navigation history.
Next, we use the helper function renderWithRouter() to render the component and pass the history to the router component. This way, we can now test whether the page loaded at the beginning is the home page. And whether the navigation bar loads the expected links.
Test if it navigates to another page with parameters when we click on the link:
Now, to check that the navigation works, we must trigger the click event on the navigation link.
For the first test, we check that the content is equal to the text in the About page, and for the second test, we test the routing parameter and check that it passes correctly.
Now we can move on to the final section and learn how to test the Axios request.
8. Test the HTTP request
Let’s examine what testrouter.js looks like.
import React from 'react'
import axios from 'axios'
const TestAxios = ({ url }) => {
const [data, setData] = React.useState()
const fetchData = async () => {
const response = await axios.get(url)
setData(response.data.greeting)
}
return (
<>
<button onClick={fetchData} data-testid="fetch-data">Load Data</button>
{
data ?
<div data-testid="show-data">{data}</div>:
<h1 data-testid="loading">Loading... </h1> } </> ) }export default TestAxios
Copy the code
As you can see here, we have a simple component that has a button for making a request. If the data is not available, it displays a load message.
Now, let’s write the test.
To verify that the data is correctly retrieved and displayed:
TextAxios.test.js
import React from 'react'
import "@testing-library/jest-dom/extend-expect";
import { render, waitForElement, fireEvent } from '@testing-library/react'
import axiosMock from 'axios'
import TestAxios from '.. /components/TestAxios'
jest.mock('axios')
it('should display a loading text', () => {
const { getByTestId } = render(<TestAxios />)
expect(getByTestId('loading')).toHaveTextContent('Loading... ')
})
it('should load and display the data', async () => {
const url = '/greeting'
const { getByTestId } = render(<TestAxios url={url} />)
axiosMock.get.mockResolvedValueOnce({
data: { greeting: 'hello there' },
})
fireEvent.click(getByTestId('fetch-data'))
const greetingData = await waitForElement(() => getByTestId('show-data'))
expect(axiosMock.get).toHaveBeenCalledTimes(1)
expect(axiosMock.get).toHaveBeenCalledWith(url)
expect(greetingData).toHaveTextContent('hello there')})Copy the code
This test case is a little different because we have to handle HTTP requests. To do this, we must simulate the AXIos request with the help of jest. Mock (‘axios’).
Now we can use axiosMock and apply the get() method to it. Finally, we’ll use the Jest function mockResolvedValueOnce() to pass mock data as a parameter.
Now, for the second test, we can click the button to get the data and parse it with async/await. Now we’re going to test three things:
- If the HTTP request has been completed correctly
- If an HTTP request is completed using a URL
- If the data obtained meets the expectations.
For the first test, we only check whether the load message is displayed when there is no data to display.
That said, we’ve now completed eight simple steps to test your React application.
See the React Testing Library documentation for more examples.
conclusion
The React Testing Library is a great tool for Testing React applications. It gives us access to the Jest-DOM matter-and best practices that we can use to test our components more effectively. Hopefully this article was useful and will help you build more robust React applications in the future.
Author: Zhang Boxuan