Hoe u kunt beginnen met het testen van uw React-apps met behulp van de React-testbibliotheek en Jest

Testen wordt vaak gezien als een vervelend proces. Het is een extra code die u moet schrijven, en in sommige gevallen, om eerlijk te zijn, is deze niet nodig. Maar elke ontwikkelaar moet op zijn minst de basis van testen kennen. Het vergroot het vertrouwen in de producten die ze bouwen, en voor de meeste bedrijven is het een vereiste.

In de React-wereld is er een geweldige bibliotheek genaamd de react-testing-librarywaarmee je je React-apps efficiënter kunt testen. Je gebruikt het met Jest.

In dit artikel zullen we de 8 eenvoudige stappen zien die u kunt nemen om uw React-apps als een baas te testen.

  • Vereisten
  • Basics
  • Wat is React Testing Library?
  • 1. Hoe maak je een test snapshot?
  • 2. Testen van DOM-elementen
  • 3. Testen van evenementen
  • 4. Asynchrone acties testen
  • 5. React Redux testen
  • 6. React Context testen
  • 7. React Router testen
  • 8. HTTP-verzoek testen
  • Laatste gedachten
  • Volgende stappen

Vereisten

In deze tutorial wordt ervan uitgegaan dat je minimaal een basiskennis van React hebt. Ik zal me alleen concentreren op het testgedeelte.

En om mee te gaan, moet je het project klonen door het in je terminal uit te voeren:

 git clone //github.com/ibrahima92/prep-react-testing-library-guide 

Voer vervolgens uit:

 yarn 

Of, als u NPM gebruikt:

npm install 

En dat is het! Laten we nu eens kijken naar de basisprincipes.

Basics

Sommige belangrijke dingen zullen in dit artikel veel worden gebruikt, en als u hun rol begrijpt, kunt u dit beter begrijpen.

it or test: beschrijft de test zelf. Het neemt als parameters de naam van de test en een functie die de tests bevat.

expect: de voorwaarde waaraan de test moet voldoen. Het zal de ontvangen parameter vergelijken met een matcher.

a matcher: een functie die wordt toegepast op de verwachte conditie.

render: de methode die wordt gebruikt om een ​​bepaald onderdeel weer te geven.

import React from 'react' import {render} from '@testing-library/react' import App from './App' it('should take a snapshot', () => { const { asFragment } = render() expect(asFragment()).toMatchSnapshot() }) }); 

Zoals u kunt zien, beschrijven we de test met iten gebruiken renderwe vervolgens om de app-component weer te geven en verwachten we die asFragment()overeenkomsten toMatchSnapshot()(de matcher geleverd door jest-dom).

Overigens renderretourneert de methode verschillende methoden die we kunnen gebruiken om onze functies te testen. We hebben ook destructurering gebruikt om de methode te krijgen.

Dat gezegd hebbende, laten we verder gaan en meer leren over de React Testing Library in de volgende sectie.

Wat is de React-testbibliotheek?

De React Testing Library is een zeer lichtgewicht pakket gemaakt door Kent C. Dodds. Het is een vervanging voor Enzyme en biedt lichte utiliteitsfuncties bovenop react-domen react-dom/test-utils.

De React Testing Library is een DOM-testbibliotheek, wat betekent dat in plaats van gevallen van gerenderde React-componenten te behandelen, deze DOM-elementen verwerkt en hoe ze zich gedragen in het bijzijn van echte gebruikers.

Het is een geweldige bibliotheek, het is (relatief) gemakkelijk te gebruiken en het moedigt goede testmethoden aan. Let op - je kunt het ook zonder Jest gebruiken.

"Hoe meer uw tests lijken op de manier waarop uw software wordt gebruikt, hoe meer vertrouwen ze u kunnen geven."

Dus laten we het in de volgende sectie gaan gebruiken. U hoeft overigens geen pakketten te installeren, aangezien deze wordt create-react-appgeleverd met de bibliotheek en zijn afhankelijkheden.

1. Hoe u een testmomentopname maakt

Een snapshot, zoals de naam suggereert, stelt ons in staat om de snapshot van een bepaald onderdeel op te slaan. Het helpt veel wanneer u een update uitvoert of wat refactoring uitvoert en de wijzigingen wilt ophalen of vergelijken.

Laten we nu een momentopname van het App.jsbestand maken.

  • 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() expect(asFragment()).toMatchSnapshot() }) }); 

Om een foto te maken, moeten we eerst importeren renderen cleanup. Deze twee methoden zullen in dit artikel veel worden gebruikt.

render, zoals u wellicht vermoedt, helpt bij het renderen van een React-component. En cleanupwordt als parameter doorgegeven afterEachom alles na elke test op te ruimen om geheugenlekken te voorkomen.

Vervolgens kunnen we de app-component weergeven met renderen terugkrijgen asFragmentals een geretourneerde waarde van de methode. En zorg er ten slotte voor dat het fragment van de app-component overeenkomt met de momentopname.

Om de test uit te voeren, opent u uw terminal en navigeert u naar de root van het project en voert u de volgende opdracht uit:

 yarn test 

Of, als u npm gebruikt:

 npm test 

Als gevolg hiervan zal het een nieuwe map __snapshots__en een bestand App.test.js.snapin het srcdie zal er als volgt uitzien:

  • App.test.js.snap
// Jest Snapshot v1, //goo.gl/fbAQLP exports[`Take a snapshot should take a snapshot 1`] = ` 

Testing

`;

En als u nog een wijziging aanbrengt App.js, zal de test mislukken, omdat de momentopname niet langer overeenkomt met de voorwaarde. Om het te laten slagen, drukt uu gewoon op om het bij te werken. En je hebt de bijgewerkte momentopname in App.test.js.snap.

Laten we nu verder gaan en onze elementen gaan testen.

2. Testen van DOM-elementen

Om onze DOM-elementen te testen, moeten we eerst naar het TestElements.jsbestand kijken.

  • TestElements.js
import React from 'react' const TestElements = () => { const [counter, setCounter] = React.useState(0) return (  

{ counter }

setCounter(counter + 1)}> Up setCounter(counter - 1)}>Down ) } export default TestElements

Hier hoeft u alleen maar te behouden data-testid. Het wordt gebruikt om deze elementen uit het testbestand te selecteren. Laten we nu de unit-test schrijven:

Test of de teller gelijk is aan 0:

TestElements.test.js

import React from 'react'; import { render, cleanup } from '@testing-library/react'; import TestElements from './TestElements' afterEach(cleanup); it('should equal to 0', () => { const { getByTestId } = render(); expect(getByTestId('counter')).toHaveTextContent(0) }); 

Zoals je kunt zien, lijkt de syntaxis behoorlijk op de vorige test. Het enige verschil is dat we gebruiken getByTestIdom de benodigde elementen te selecteren (onthoud de data-testid) en controleren of het de test heeft doorstaan. Met andere woorden, we controleren of de tekst inhoud

{ counter }

is gelijk aan 0.

Test of de knoppen zijn in- of uitgeschakeld:

TestElements.test.js (voeg het volgende codeblok toe aan het bestand)

 it('should be enabled', () => { const { getByTestId } = render(); expect(getByTestId('button-up')).not.toHaveAttribute('disabled') }); it('should be disabled', () => { const { getByTestId } = render(); expect(getByTestId('button-down')).toBeDisabled() }); 

Hier gebruiken we, zoals gewoonlijk, getByTestIdom elementen te selecteren en voor de eerste test te controleren of de knop een disabledattribuut heeft. En voor de tweede, of de knop is uitgeschakeld of niet.

En als u het bestand opslaat of opnieuw uitvoert in uw terminal yarn test, zal de test slagen.

Proficiat! Je eerste test is geslaagd!

congrats

Laten we nu in de volgende sectie leren hoe we een evenement kunnen testen.

3. Testen van evenementen

Before writing our unit tests, let's first check what the TestEvents.js looks like.

  • TestEvents.js
import React from 'react' const TestEvents = () => { const [counter, setCounter] = React.useState(0) return (  

{ counter }

setCounter(counter + 1)}> Up setCounter(counter - 1)}>Down ) } export default TestEvents

Now, let's write the tests.

Test if the counter increments and decrements correctly when we click on buttons:

TestEvents.test.js

import React from 'react'; import { render, cleanup, fireEvent } from '@testing-library/react'; import TestEvents from './TestEvents' afterEach(cleanup); it('increments counter', () => { const { getByTestId } = render(); fireEvent.click(getByTestId('button-up')) expect(getByTestId('counter')).toHaveTextContent('1') }); it('decrements counter', () => { const { getByTestId } = render(); fireEvent.click(getByTestId('button-down')) expect(getByTestId('counter')).toHaveTextContent('-1') }); 

As you can see, these two tests are very similar except the expected text content.

The first test fires a click event with fireEvent.click() to check if the counter increments to 1 when the button is clicked.

And the second one checks if the counter decrements to -1 when the button is clicked.

fireEvent has several methods you can use to test events, so feel free to dive into the documentation to learn more.

Now that we know how to test events, let's move on and learn in the next section how to deal with asynchronous actions.

4. Testing asynchronous actions

An asynchronous action is something that can take time to complete. It can be an HTTP request, a timer, and so on.

Now, let's check the TestAsync.js file.

  • TestAsync.js
import React from 'react' const TestAsync = () => { const [counter, setCounter] = React.useState(0) const delayCount = () => ( setTimeout(() => { setCounter(counter + 1) }, 500) ) return (  

{ counter }

Up setCounter(counter - 1)}>Down ) } export default TestAsync

Here, we use setTimeout() to delay the incrementing event by 0.5s.

Test if the counter is incremented after 0.5s:

TestAsync.test.js

import React from 'react'; import { render, cleanup, fireEvent, waitForElement } from '@testing-library/react'; import TestAsync from './TestAsync' afterEach(cleanup); it('increments counter after 0.5s', async () => { const { getByTestId, getByText } = render(); fireEvent.click(getByTestId('button-up')) const counter = await waitForElement(() => getByText('1')) expect(counter).toHaveTextContent('1') }); 

To test the incrementing event, we first have to use async/await to handle the action because, as I said earlier, it takes time to complete.

Next, we use a new helper method getByText(). This is similar to getByTestId(), except that getByText() selects the text content instead of id or data-testid.

Now, after clicking to the button, we wait for the counter to be incremented with waitForElement(() => getByText('1')). And once the counter incremented to 1, we can now move to the condition and check if the counter is effectively equal to 1.

That being said, let's now move to more complex test cases.

Are you ready?

ready

5. Testing React Redux

If you're new to React Redux, this article might help you. Otherwise, let's check what the 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 (  

{ counter }

Up Down ) } export default connect(state => ({ counter: state.count }))(TestRedux)

And for the reducer:

  • 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 } } 

As you can see, there is nothing fancy – it's just a basic Counter Component handled by React Redux.

Now, let's write the unit tests.

Test if the initial state is equal to 0:

TestRedux.test.js

import React from 'react' 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 './TestRedux' const renderWithRedux = ( component, { initialState, store = createStore(reducer, initialState) } = {} ) => { return { ...render({component}), store, } } afterEach(cleanup); it('checks initial state is equal to 0', () => { const { getByTestId } = renderWithRedux() expect(getByTestId('counter')).toHaveTextContent('0') }) 

There are a couple of things we need to import to test React Redux. And here, we create our own helper function renderWithRedux() to render the component since it will be used several times.

renderWithRedux() receives as parameters the component to render, the initial state, and the store. If there is no store, it will create a new one, and if it doesn't receive an initial state or a store, it returns an empty object.

Next, we use render() to render the component and pass the store to the Provider.

That being said, we can now pass the component TestRedux to renderWithRedux() to test if the counter is equal to 0.

Test if the counter increments and decrements correctly:

TestRedux.test.js (add the following code block to the file)

it('increments the counter through redux', () => { const { getByTestId } = renderWithRedux(, {initialState: {count: 5} }) fireEvent.click(getByTestId('button-up')) expect(getByTestId('counter')).toHaveTextContent('6') }) it('decrements the counter through redux', () => { const { getByTestId} = renderWithRedux(, { initialState: { count: 100 }, }) fireEvent.click(getByTestId('button-down')) expect(getByTestId('counter')).toHaveTextContent('99') }) 

To test the incrementing and decrementing events, we pass an initial state as a second argument to renderWithRedux(). Now, we can click on the buttons and test if the expected result matches the condition or not.

Now, let's move to the next section and introduce React Context.

React Router and Axios will come next – are you still with me?

of-course

6. Testing React Context

If you're new to React Context, check out this article first. Otherwise, let's check the TextContext.js file.

  • TextContext.js
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 (    ) } export const Counter = () => { const { counter, increment, decrement } = React.useContext(CounterContext) return (  

{ counter }

Up Down ) } export default CounterProvider

Now, the counter state is managed through React Context. Let's write the unit test to check if it behaves as expected.

Test if the initial state is equal to 0:

TextContext.test.js

import React from 'react' import { render, cleanup, fireEvent } from '@testing-library/react' import CounterProvider, { CounterContext, Counter } from './TestContext' const renderWithContext = ( component) => { return { ...render(  {component} ) } } afterEach(cleanup); it('checks if initial state is equal to 0', () => { const { getByTestId } = renderWithContext() expect(getByTestId('counter')).toHaveTextContent('0') }) 

As in the previous section with React Redux, here we use the same approach, by creating a helper function renderWithContext() to render the component. But this time, it receives only the component as a parameter. And to create a new context, we pass CounterContext to the Provider.

Now, we can test if the counter is initially equal to 0 or not.

Test if the counter increments and decrements correctly:

TextContext.test.js (add the following code block to the file)

 it('increments the counter', () => { const { getByTestId } = renderWithContext() fireEvent.click(getByTestId('button-up')) expect(getByTestId('counter')).toHaveTextContent('1') }) it('decrements the counter', () => { const { getByTestId} = renderWithContext() fireEvent.click(getByTestId('button-down')) expect(getByTestId('counter')).toHaveTextContent('-1') }) 

As you can see, here we fire a click event to test if the counter increments correctly to 1 and decrements to -1.

That being said, we can now move to the next section and introduce React Router.

7. Testing React Router

If you want to dive into React Router, this article might help you. Otherwise, let's check the TestRouter.js file.

  • TestRouter.js
import React from 'react' import { Link, Route, Switch, useParams } from 'react-router-dom' const About = () =>

About page

const Home = () =>

Home page

const Contact = () => { const { name } = useParams() return

{name}

} const TestRouter = () => { const name = 'John Doe' return ( Home About Contact ) } export default TestRouter

Here, we have some components to render when navigating the Home page.

Now, let's write the tests:

  • TestRouter.test.js
import React from 'react' import { Router } from 'react-router-dom' import { render, fireEvent } from '@testing-library/react' import { createMemoryHistory } from 'history' import TestRouter from './TestRouter' const renderWithRouter = (component) => { const history = createMemoryHistory() return { ...render (  {component}  ) } } it('should render the home page', () => { const { container, getByTestId } = renderWithRouter() const navbar = getByTestId('navbar') const link = getByTestId('home-link') expect(container.innerHTML).toMatch('Home page') expect(navbar).toContainElement(link) }) 

To test React Router, we have to first have a navigation history to start with. Therefore we use createMemoryHistory() to well as the name guessed to create a navigation history.

Next, we use our helper function renderWithRouter() to render the component and pass history to the Router component. With that, we can now test if the page loaded at the start is the Home page or not. And if the navigation bar is loaded with the expected links.

Test if it navigates to other pages with the parameters when we click on links:

TestRouter.test.js (add the following code block to the file)

it('should navigate to the about page', ()=> { const { container, getByTestId } = renderWithRouter() 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() fireEvent.click(getByTestId('contact-link')) expect(container.innerHTML).toMatch('John Doe') }) 

Now, to check if the navigation works, we have to fire a click event on the navigation links.

For the first test, we check if the content is equal to the text in the About Page, and for the second, we test the routing params and check if it passed correctly.

We can now move to the final section and learn how to test an Axios request.

We're almost done!

still-here

8. Testing HTTP Request

As usual, let's first see what the TextAxios.js file looks like.

  • TextAxios.js
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 (  Load Data { data ? {data} : 

Loading...

} ) } export default TestAxios

As you can see here, we have a simple component that has a button to make a request. And if the data is not available, it will display a loading message.

Now, let's write the tests.

Test if the data are fetched and displayed correctly:

TextAxios.test.js

import React from 'react' import { render, waitForElement, fireEvent } from '@testing-library/react' import axiosMock from 'axios' import TestAxios from './TestAxios' jest.mock('axios') it('should display a loading text', () => { const { getByTestId } = render() expect(getByTestId('loading')).toHaveTextContent('Loading...') }) it('should load and display the data', async () => { const url = '/greeting' const { getByTestId } = render() 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') }) 

This test case is a bit different because we have to deal with an HTTP request. And to do that, we have to mock an axios request with the help of jest.mock('axios').

Now, we can use axiosMock and apply a get() method to it. Finally we will use the Jest function mockResolvedValueOnce() to pass the mocked data as a parameter.

With that, now for the second test we can click to the button to fetch the data and use async/await to resolve it. And now we have to test 3 things:

  1. If the HTTP request has been done correctly
  2. If the HTTP request has been done with the url
  3. If the data fetched matches the expectation.

And for the first test, we just check if the loading message is displayed when we have no data to show.

That being said, we're now done with the 8 simple steps to start testing your React Apps.

Don't be scared to test anymore.

not-scared

Final Thoughts

The React Testing Library is a great package for testing React Apps. It gives us access to jest-dom matchers we can use to test our components more efficiently and with good practices. Hopefully this article was useful, and it will help you build robust React apps in the future.

You can find the finished project here

Thanks for reading it!

Read more articles  -  Subscribe to my newsletter   -   Follow me on twitter

You can read other articles like this on my blog.

Next Steps

React Testing Library docs

React Testing Library Cheatsheet

Jest DOM matchers cheatsheet

Jest Docs