Commencer à tester vos applications React à l'aide React Testing Library et Jest

Mar 12, 2020☕ ☕ 20 min Follow me on Twitter

Subscribe to receive the free weekly article

Les tests sont souvent considérés comme quelque chose de fastidieux. C'est du code supplémentaire et dans certains cas, pour être honnête, ce n'est pas nécessaire. Cependant, chaque développeur doit connaître au moins les bases des tests, car cela augmente la confiance dans le produit et pour la plupart des entreprises, c'est une exigence.

Dans le monde de React, il y a une bibliothèque appelée react-testing-library qui aide à tester les applications React plus efficacement en combinaison avec Jest. Dans cet article, nous allons voir les 8 étapes simples pour commencer à tester vos applications React comme un boss.

Conditions préalables

Ce didacticiel suppose que vous avez au moins une compréhension de base de React. Je me concentrerai uniquement sur la partie test. Et pour suivre, vous devez cloner le projet en exécutant dans votre terminal:

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

Ensuite, exécutez:

  yarn

Ou, si vous utilisez NPM:

npm install

Et c'est tout, commencons avec quelques bases.

Bases

Il y a certains éléments clés qui seront beaucoup utilisés dans cet article, et la compréhension de leur rôle peut vous aider à mieux comprendre les tests.

  • it or test: décrit le test en tant que tel. Il prend comme paramètres le nom du test et une fonction qui contient les tests.
  • expect: c'est la condition que le test doit passer. Il comparera le paramètre reçu avec un matcher.
  • un matcher: c'est une fonction qui est appliquée à la condition attendue.
  • render: c'est la méthode utilisée pour rendre un composant donné.
import React from 'react'
import {render} from '@testing-library/react'
import App from './App'

 it('should take a snapshot', () => {
    const { asFragment } = render(<App />)

    expect(asFragment(<App />)).toMatchSnapshot()
   })
});

Comme vous pouvez le voir, nous décrivons le test avec it, puis, on utilise render pour afficher le composant App et attendons a ce que asFragment(<App />) corresponde à toMatchSnapshot() (le matcher fourni par jest -dom). Au passage, la méthode render retourne plusieurs méthodes que nous pouvons utiliser pour tester nos fonctionnalités. Nous avons également utilisé la déstructuration pour obtenir la méthode.

Cela étant dit, passons à autre chose et définissons React Testing Library dans la section suivante.

Qu'est-ce que React Testing Library?

React Testing Library est une bibliothèque très légère créée par Kent C. Dodds. Il remplace Enzyme et fournit des fonctions utilitaires légères en plus de react-dom et react-dom/test-utils. React Testing Library est une bibliothèque de tests de DOM, ce qui signifie qu'au lieu de traiter les instances de composants React rendus, elle gère les éléments DOM et leur comportement devant les utilisateurs réels. C'est une excellente bibliothèque que j'aime, elle est (relativement) facile à démarrer et elle encourage les bonnes pratiques de test et vous pouvez également l'utiliser sans Jest.

"Plus vos tests ressemblent à la façon dont votre logiciel est utilisé, plus ils vous donneront confiance."

Commençons donc à l'utiliser dans la section suivante. En passant, vous n'avez pas besoin d'installer de packages car create-react-app est fourni avec la bibliothèque et ses dépendances.

1. Comment créer une capture (snapshot) de test?

Un snapshot, comme son nom l'indique, nous permet d'enregistrer une capture d'un composant donné. Cela aide beaucoup lorsque vous mettez à jour ou effectuez une refactorisation et que vous souhaitez obtenir ou comparer le changement.

Maintenant, prenons une capture du fichier App.js.

  • 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(<App />)).toMatchSnapshot()
   })
});

Pour prendre une capture, nous devons d'abord importer render et cleanup. Ces deux méthodes seront beaucoup utilisées durant cet article. render, comme vous pouvez le deviner aide à rendre un composant React. Et cleanup est passé en paramètre à afterEach pour simplement tout nettoyer après chaque test pour éviter les fuites de mémoire.

Ensuite, nous pouvons maintenant rendre le composant App avec render et récupérer asFragment en tant que valeur renvoyée par la méthode. Et enfin, attendre à ce que le fragment du composant App corresponde à l'instantané.

Maintenant, pour exécuter le test, il faut ouvrir votre terminal et naviguer à la racine du projet et exécuter la commande suivante:

  yarn test

Ou, si vous utilisez npm:

  npm test

En conséquence, il créera un nouveau dossier __snapshots__ et un fichier App.test.js.snap dans le src qui ressemblera à ceci:

  • App.test.js.snap
  • App.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Take a snapshot should take a snapshot 1`] = `
<DocumentFragment>
  <div class="App">
    <h1>Testing</h1>
  </div>
</DocumentFragment>
`

Et si vous apportez une autre modification dans App.js, le test échouera, car la capture ne correspondra plus à la condition. Pour le faire passer à nouveau, if faut appuyer simplement sur u pour le mettre à jour. Et vous aurez le snapshot à jour dans App.test.js.snap.

Maintenant, passons à autre chose et commençons à tester nos éléments.

2. Test des éléments du DOM

Pour tester nos éléments, nous devons d'abord voir le fichier TestElements.js.

  • 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

Ici, la seule chose que vous devez retenir est data-testid. Il sera utilisé pour sélectionner ces éléments dans le fichier de test. Maintenant, écrivons le test unitaire:

  • Tester si le compteur est égal à 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(<TestElements />)
  expect(getByTestId("counter")).toHaveTextContent(0)
})

Comme vous pouvez le voir, la syntaxe est assez similaire au test précédent. La seule différence est que nous utilisons getByTestId pour sélectionner les éléments nécessaires (rappelez-vous le data-testid) et on vérifie s'il a réussi le test. En d'autres termes, nous vérifions si le contenu du texte contenu dans <h1 data-testid=" counter">{ counter }</h1> est égal à 0.

  • Tester si les boutons sont activés ou désactivés
  • TestElements.test.js (ajoutez le bloc de code suivant au fichier)
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()
})

Ici, comme d'habitude, nous utilisons getByTestId pour sélectionner les éléments et vérifier pour le premier test si le bouton a un attribut disabled. Et pour le second, si le bouton est désactivé ou non.

Et si vous enregistrez le fichier ou exécutez à nouveau dans votre terminal yarn test, le test passera à nouveau.

Félicitations! votre premier test est réussi!

Maintenant, apprenons comment tester un événement dans la section suivante.

3. Test d'événements

Avant d'écrire nos tests unitaires, vérifions d'abord à quoi ressemble TestEvents.js.

  • TestEvents.js
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

Maintenant, écrivons les tests.

  • Tester si le compteur augmente et diminue correctement lorsque nous cliquons sur les boutons
  • 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(<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")
})

Comme vous pouvez le voir, ces deux tests sont très similaires à l'exception du contenu de texte attendu.

Le premier test déclenche un événement de clic avec fireEvent.click() pour vérifier si le compteur augmente à 1 lorsqu'on clique sur le bouton.

Et le second, vérifie si le compteur diminue à -1 lorsqu'on clique sur le bouton.

fireEvent a plusieurs méthodes que vous pouvez utiliser pour tester les événements, n'hésitez pas à plonger dans la documentation.

Maintenant que nous savons comment tester des événements, il est temps de passer à autre chose et d'apprendre comment gérer les actions asynchrones dans la section suivante.

4. Test des actions asynchrones

Une action asynchrone est une action qui peut prendre du temps. Il peut s'agir d'une requête HTTP, d'une minuterie, etc.

Maintenant, vérifions le fichier TestAsync.js.

  • TestAsync.js
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

Ici, nous utilisons setTimeout() pour retarder l'événement d'incrémentation de 0,5 seconde.

  • Tester si le compteur est incrémenté après 0,5 seconde.
  • 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(<TestAsync />)

  fireEvent.click(getByTestId("button-up"))

  const counter = await waitForElement(() => getByText("1"))

  expect(counter).toHaveTextContent("1")
})

Pour tester l'événement d'incrémentation, nous devons d'abord utiliser async/await pour gérer l'action car, comme je l'ai dit plus tôt, cela prend du temps.

Ensuite, nous utilisons une nouvelle méthode d'assistance getByText() qui est similaire à getByTestId(), sauf que getByText() sélectionne le contenu du texte au lieu de id ou data-testid je devrais dire.

Maintenant, après avoir cliqué sur le bouton, nous attendons que le compteur soit incrémenté avec waitForElement(() => getByText('1')). Et une fois le compteur incrémenté à 1, nous pouvons maintenant passer à la condition et vérifier si le compteur est effectivement égal à 1.

Cela étant dit, passons maintenant à des cas de test plus complexes.

Es-tu prêt?

5. Test de React Redux

Si vous êtes nouveau sur React Redux, cet article pourrait vous aider. Sinon, vérifions à quoi ressemble le TestRedux.js.

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

Et pour le réducteur.

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

Comme vous pouvez le voir ici, il n'y a rien d'extraordinaire, c'est juste un Counter Component de base géré par React Redux.

Maintenant, écrivons les tests unitaires.

  • Tester si l'état initial est égal à 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(<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")
})

Il y a quelques éléments que nous devons importer pour tester React Redux. Et ici, nous créons notre propre fonction d'assistance renderWithRedux() pour rendre le composant car il sera utilisé plusieurs fois.

renderWithRedux() reçoit en paramètres le composant à restituer, l'état initial et le store. S'il n'y a pas de store, il en créera un nouveau et s'il ne reçoit pas d'état initial ou de store, il retourne un objet vide.

Ensuite, nous utilisons render() pour rendre le composant et passer le store au Provider.

Cela étant dit, nous pouvons maintenant passer le composant TestRedux à renderWithRedux() pour tester si le compteur est égal à0.

  • Tester si le compteur augmente et diminue correctement.
  • TestRedux.test.js (ajoutez le bloc de code suivant au fichier)
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")
})

Pour tester les événements d'incrémentation et de décrémentation, nous passons un état initial comme deuxième argument à renderWithRedux(). Et, nous pouvons maintenant cliquer sur les boutons et tester si le résultat attendu correspond ou non à la condition.

Passons maintenant à la section suivante et introduisons le contexte de React.

React Router et Axios viendront ensuite, êtes-vous toujours avec moi?

6. Test de React Context

Si vous êtes nouveau dans React Context, consultez d'abord cet article. Sinon, vérifions le fichier TextContext.js.

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

Désormais, l'état du compteur est géré via React Context. Il est temps d'écrire le test unitaire pour vérifier s'il se comporte comme prévu.

  • Tester si l'état initial est égal à 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(
      <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")
})

Comme dans la section précédente avec React Redux, nous utilisons ici la même approche, en créant une fonction d'assistance renderWithContext() pour rendre le composant. Mais cette fois, il ne reçoit que le composant en paramètre. Et pour créer un nouveau contexte, nous passons CounterContext au Provider.

Maintenant, nous pouvons tester si le compteur est initialement égal à 0 ou non.

  • Tester si le compteur augmente et diminue correctement.
  • TextContext.test.js (ajoutez le bloc de code suivant au fichier)
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")
})

Comme vous pouvez le voir, nous déclenchons ici un événement de clic pour tester si le compteur s'incrémente correctement à 1 et décrémente à -1.

Cela étant dit, nous pouvons maintenant passer à la section suivante et présenter React Router.

7. Test de React Router

Si vous souhaitez plonger dans React Router, cet article pourrait vous aider. Sinon, vérifions le fichier TestRouter.js.

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

Ici, nous avons quelques composants à rendre lors de la navigation et la page d'accueil.

Maintenant, écrivons les tests

  • TestRouter.test.js
  • 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(<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)
})

Pour tester React Router, nous devons d'abord avoir un historique de navigation. Par conséquent, nous utilisons createMemoryHistory() et comme le nom l'indique, il crée un historique de navigation.

Ensuite, nous utilisons notre fonction d'assistance renderWithRouter() pour rendre le composant et passer history au composant Router. Avec cela, nous pouvons maintenant tester si la page chargée au début est la page d'accueil ou non. Et si la barre de navigation est chargée avec les liens attendus.

  • Tester s'il navigue vers d'autres pages avec les paramètres quand on clique sur les liens.
  • TestRouter.test.js (ajoutez le bloc de code suivant au fichier)
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")
})

Maintenant, pour vérifier si la navigation fonctionne, nous devons déclencher un événement de clic sur les liens de navigation.

Pour le premier test, on vérifie si le contenu est égal au texte de la page À propos, et pour le second, nous testons les paramètres de routage et vérifions s'il est passé correctement.

Nous pouvons maintenant passer à la dernière section et apprendre à tester une requête Axios.

Nous avons presque terminé

8. Test de la requête HTTP (axios)

Comme d'habitude, voyons d'abord à quoi ressemble le fichier 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 (
    <>
      <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

Comme vous pouvez le voir ici, nous avons un composant simple qui a un bouton pour faire requête HTTP. Et si les données ne sont pas disponibles, il affichera un message de chargement.

Maintenant, écrivons les tests.

  • Tester si les données sont récupérées et affichées correctement.
  • 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(<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")
})

Ce cas de test est un peu différent car nous devons traiter une requête HTTP. Et pour ce faire, nous devons nous simuler la requête axios avec l'aide de jest.mock('axios').

Maintenant, nous pouvons utiliser axiosMock et lui appliquer une méthode get() et enfin utiliser la fonction de Jest mockResolvedValueOnce() pour passer les données simulées en paramètre.

Avec cela, nous pouvons maintenant pour le deuxième test, cliquer sur le bouton pour récupérer les données et utiliser async/await pour les résoudre. Et maintenant, nous devons tester 3 choses:

  1. Si la requête HTTP a été effectuée correctement
  2. Si la requête HTTP a été effectuée avec l'url
  3. Si les données extraites correspondent à l'attente.

Et pour le premier test, nous vérifions simplement si le message de chargement est affiché lorsqu'on a aucune donnée à afficher.

Cela étant dit, nous avons maintenant terminé avec les 8 étapes simples pour commencer à tester vos applications React.

N'ayez plus peur des tests

Dernières pensées

La bibliothèque de tests React est un excellent package pour tester les applications React. Il nous donne accès à des dispositifs de jest-dom que nous pouvons utiliser pour tester nos composants plus efficacement et avec de bonnes pratiques. J'espère que cet article a été utile et qu'il vous aidera à créer des applications React robustes à l'avenir.

Vous pouvez trouver le projet terminé ici

Merci de l'avoir lu.

Prochaines étapes

Documentation de React Testing Library

Aide-mémoire de React Testing Library

Aide-mémoire de Jest DOM matchers

Documentation de Jest

#react#testing

Support my work

Get articles in your inbox