Formateur: Fabio Ginja
{
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest",
"preview": "vite preview"
},
}
yarn add -D vitest
On ajoute également la commande test à notre package.json:
On peut désormais commencer à écrire des tests dans notre projet:
import { describe, it, expect } from 'vitest';
describe('something truthy and falsy', () => {
it('true to be true', () => {
expect(true).toBe(true);
});
test('addition to be correct', () => {
expect(2 + 1).toBe(3);
});
});
Pour tester une application react, on utilisera react-testing-library. Pour ce faire, il faudra installer certaines dépendances et lier cette dernière à Vitest.
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import matchers from '@testing-library/jest-dom/matchers';
// ajoute les méthodes react-testing-library à Vitest pour l'autocompletion
expect.extend(matchers);
afterEach(() => {
// Après chaque test, on peut executer des fonctions au besoin
cleanup();
}); // Default on import: runs it after each test.
yarn add -D jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event
On va créer un fichier tests/setup.ts afin d'ajouter les méthodes de react-testing-library à Vitest:
Enfin, on va ajouter à notre vite.config.ts la configuration suivante:
/// <reference types="vitest" />
/// <reference types="vite/client" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom', // tells Vitest to run our tests in a mock browser environment rather than the default Node environment
setupFiles: './tests/setup.ts',
},
});
On va créer un fichier tests/setup.ts afin d'ajouter les méthodes de react-testing-library à Vitest:
Le premier principe de React testing library:
The more your tests resemble the way your software is used, the more confidence they can give you.
Cela nous indique qu'on devrait tester notre application comme un utilisateur le ferait, sans forcément tester les détails d'implémentation.
La librairie nous permet d'interagir avec le dom pour simuler le comportement d'un utilisateur.
Les queries sont les méthodes qui vont nous permettre de trouver nos éléments sur la page HTML.
On a plusieurs type de queries: get, find et query.
Selon ce que l'on souhaite sélectionner, on devra utiliser une méthode plutôt qu'une autre.
La méthode getByX retourne le nœud demandé si et seulement si un seul nœud est trouvé.
Si 0 ou plusieurs nœuds sont trouvés alors cela throw une erreur.
import { render, screen } from '@testing-library/react'
test('should show login form', () => {
render(<Login />)
const input = screen.getByLabelText('Username')
// ❌ Si 0 ou plus
// ✅ Si 1
})
La méthode getAllByX retourne un tableau contenant un ou plusieurs nœuds correspondant.
Si aucun nœud est trouvé alors la méthode throw une erreur.
⚠️L'élément doit déjà exister lors du premier render de la page.
La méthode queryByX retourne le nœud demandé si et seulement si un seul nœud est trouvé.
Si aucun nœud n'est trouvé, alors retourne null.
Si plusieurs nœuds sont trouvés alors cela throw une erreur.
import { render, screen } from '@testing-library/react'
test('should show login form', () => {
render(<Login />)
const input = screen.queryByLabelText('Username')
// ✅ Si 0 ou 1
// ❌ Si > 1
})
La méthode getAllByX retourne un tableau contenant un ou plusieurs nœuds correspondant.
Si aucun nœud est trouvé alors retourne un tableau vide.
C'est donc la méthode qu'on utilisera pour vérifier qu'un élément est absent de la page.
La méthode findByX retourne une promesse qui retournera le nœud demandé si et seulement si un seul nœud est trouvé.
Si aucun nœud n'est trouvé, alors retourne null.
Si plusieurs nœuds sont trouvés alors cela throw une erreur.
import { render, screen } from '@testing-library/react'
test('should show login form', () => {
render(<Login />)
const input = screen.findByLabelText('Username')
// ✅ Si 0 ou 1 après 1000ms
// ❌ Si > 1 après 1000ms
})
La méthode findAllByX retourne une promesse un tableau contenant un ou plusieurs nœuds correspondant.
Si aucun nœud est trouvé alors retourne un tableau vide.
⏱La promesse a une durée maximum par défaut de 1000ms.
Les queries suivantes sont disponibles, et devraient être utilisées selon ces priorités:
getByRole('button', {name: /submit/i})
Deux sélecteur HTML5 et ARIA compliant sont disponibles:
<div data-testid="custom-element" />
Test IDs, à utiliser en dernier recours:
const element = screen.getByTestId('custom-element')
Screen correspond à notre document.body
(approche recommandée), mais il est possible de sélectionner un élément de notre DOM:
import {render, screen, getByLabelText} from '@testing-library/dom'
render(
<div id="app">
<label htmlFor="Username">Username:</label>
<input id="Username" />
</div>,
)
// With screen:
const inputNode1 = screen.getByLabelText('Username')
// ⚠️ Without screen, you need to provide a container:
const container = document.querySelector('#app')
const inputNode2 = getByLabelText(container, 'Username')
// ⚠️ Non recommandé
const {container} = render(<MyComponent />)
const foo = container.querySelector('[data-foo="bar"]'
Prenons l'exemple suivant:
On peut tester des combinaisons avec ce playground
import {render, screen} from '@testing-library/dom'
render(<div>Hello World</div>)
// ✅ Matching a string:
screen.getByText('Hello World') // full string match
screen.getByText('llo Worl', {exact: false}) // substring match
screen.getByText('hello world', {exact: false}) // ignore case
// ✅ Matching a regex:
screen.getByText(/World/) // substring match
screen.getByText(/world/i) // substring match, ignore case
screen.getByText(/^hello world$/i) // full string match, ignore case
screen.getByText(/Hello W?oRlD/i) // substring match, ignore case, searches for "hello world" or "hello orld"
// ✅ Matching with a custom function:
screen.getByText((content, element) => content.startsWith('Hello'))
// ❌ function looking for a span when it's actually a div:
screen.getByText((content, element) => {
return element.tagName.toLowerCase() === 'span' && content.startsWith('Hello')
})
Il existe également les méthodes suivantes:
import {render, screen} from '@testing-library/dom'
const element = screen.getByText('Hello World') // full string match
expect(element).toBeDisabled()
expect(element).toBeEnabled()
expect(element).toBeEmptyDOMElement()
expect(element).toBeInTheDocument()
expect(element).toBeValid()
expect(element).toBeInvalid()
expect(element).toBeRequired()
expect(element).toBeVisible()
expect(element).toContainElement(getByTestId('descendant'))
expect(element).toContainHTML('<span data-testid="child"></span>')
expect(element).toHaveAttribute('type', 'submit')
expect(element).toHaveClass()
expect(element).toHaveFocus()
expect(getByTestId('login-form')).toHaveFormValues({username: 'jane.doe',})
expect(element).toHaveStyle({'background-color': 'green',})
expect(element).toHaveTextContent('Content')
expect(element).toHaveValue()
expect(element).toHaveDisplayValue('banana')
expect(element).toBeChecked()
expect(element).toBePartiallyChecked()
expect(element).toHaveAccessibleDescription()
Pour nous aider à debugger, on a deux options.
On peut afficher le code HTML dans la console grâce à screen.debug()
import {render, screen} from '@testing-library/dom'
render(<div>Hello World</div>)
screen.debug()
On peut afficher le code HTML dans un testing playground grâce à screen.logTestingPlaygroundURL()
import {render, screen} from '@testing-library/dom'
render(<div>Hello World</div>)
screen.logTestingPlaygroundURL()
Console:
Open this URL in your browser
https://testing-playground.com/#markup=randomString
L'API userEvent permet de simuler le comportement d'un utilisateur. Par exemple userEvent.type
déclanche tout les évenement suivants: change,
keyDown
, keyPress
, et keyUp
.
const user = userEvent.setup()
expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();
await user.type(screen.getByRole('textbox'), 'JavaScript');
expect(
screen.getByText(/Searches for JavaScript/)
).toBeInTheDocument();
function setup(jsx) {
return {
user: userEvent.setup(),
...render(jsx),
}
}
test('render with a setup function', async () => {
const {user} = setup(<MyComponent />)
})
On peut également créer un setup fonction:
Lorsqu'on veut tester qu'une fonction passée à un composant est bien appelé, on peut le faire avec les outils de mock de vitest ou jest:
type ButtonProps = {callback?: () => unknown}
export const Button = ({ callback }: ButtonProps) => (
<button onClick={callback}>Button</button>
)
describe('Button', () => {
it('calls the onClick callback handler', async () => {
const onClick = vi.fn(); // replace vi by jest if using jest
const user = userEvent.setup()
render(<Button onClick={onClick} />);
await user.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
});
Dans notre fichier test:
Permet d'afficher du JSX ou un autre component en attendant le chargement du 'lazy' component.
Il prend deux paramètres:
import React, { Suspense, lazy } from 'react';
import Profile from './components/Profile';
const User = lazy(() => import('./components/User'));
const App = () => (
<React.Fragment>
<Profile />
<Suspense fallback={<div>Loading...</div>}>
<User />
</Suspense>
</React.Fragment>
);
Pure Components est une alternative à l'utilisation de la méthode shouldComponentUpdate des class components.
Ce dernier effectue un shallow check du state et des props. S'il n'y a eu aucun changement, le composant ne sera pas mis à jour.
import React, { PureComponent} from 'react';
class SomeComponent extends PureComponent {
// Some Code
render() {
return (
// Some JSX
);
};
};
Très efficace pour éviter des rendu inutiles.
N'utiliser que PureComponent si les props ou le state du parent peuvent vraiment provoquer un render inutile du component enfant.
React Memo est une alternative à l'utilisation de la méthode shouldComponentUpdate pour les functional components.
Même objectif que les PureComponent.
Attention, n'effectue qu'une shallow comparaison et peut ne pas détecter des changements à l'intérieur d'un tableau, objet (reference types).
import React from 'react';
const SomeComponent = (props) => {
// Some Code
return (
// Some JSX
);
};
export default React.memo(SomeComponent);
useCallback permet de renvoyer une fonction de rappel mémoïsée. Pour cela on doit passer la fonction que l'on souhaite mémoïser, ainsi qu'un tableau de dépendance indiquant les entrées de notre fonction:
import React, { useCallback } from 'react';
const SomeComponent = (props) => {
// ...
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
// ...
};
Cela permet d'éviter des rendu inutiles lorsqu'on passe une fonction à un composant utilisant shouldComponentUpdate, React.memo, ou PureComponent.
useMemo permet de renvoyer une valeur mémoïsée. Pour cela on doit passer la fonction dont on souhaite obtenir le résultat, ainsi qu'un tableau de dépendance indiquant les entrées de notre fonction:
import React, { useMemo } from 'react';
const SomeComponent = (props) => {
// ...
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// ...
};
Si les inputs n'ont pas changé, alors on gardera la valeur précédente du retour de la fonction.
Cela est très utile lorsqu'on fait appel à une fonction qui fait des calcul coûteux.
Si on ne fournis aucun tableau, une nouvelle valeur sera calculée à chaque appel.
describe('Test suite', () => {
test('Test case', () => {
expect(true).toBe(true) // assertion
})
test('Test case', () => {
})
})
Jest (ou Vitest) est un test runner qui contient également des méthodes afin de réaliser des tests double tels que des spies, stubs, ou mocks.
npm run test
# Ou encore
yarn test
describe("Login Component", () => {
it("some method", () => {
const dummyOnLogin = () => {}
render(<Login onLogin={dummyOnLogin} />);
const emailInput: HTMLInputElement = screen.getByRole("textbox", { name: /email/i})
expect(emailInput).toBeInTheDocument();
});
});
Un dummy est simplement un placeholder dont a besoin une méthode que l'on souhaite tester mais qui n'interfère pas dans notre test.
describe('Test suite', () => {
test('Test case', () => {
expect(true).toBe(true) // assertion
})
test('Test case', () => {
})
})
Un stub est un spy auquel on attache un comportement preprogrammer
npm run test
# Ou encore
yarn test
describe('Test suite', () => {
test('Test case', () => {
expect(true).toBe(true) // assertion
})
test('Test case', () => {
})
})
Le mock sert a remplacer une dependance externe.
npm run test
# Ou encore
yarn test