Passer au contenu principal

Tests d'unité

Pour tout projet JDP, la fonctionnalité de test d’unité est intégrée. Vous devriez constater la présence de @jutro/test dans package.json de la section devDependencies.

Une fois que le projet a exécuté npm install ou npm ci, vous pouvez vérifier la fonctionnalité de test d’unité en lançant la commande npm run test dans l’invite de commande. Vous devriez voir les tests ayant été exécutés, les tests ayant éventuellement échoué et un rapport de garantie de test à la fin.

Notez qu’il est possible de voir des erreurs TypeScript pour certaines méthodes de test, par exemple toBeInTheDocument() après la génération.

Par ailleurs, la plupart des méthodes d’assertion de la bibliothèque de tests ne sont pas disponibles avec la configuration telle qu’elle est fournie (méthodes liées à Jest Dom).

Une application Jutro exécute Jest via l'interface react-app-rewired.

Exécution de tests d'unité​

Jest exécute nos tests d’application. Il peut donc être très utile de garder à portée de main la documentation Options de la CLI Jest.

Vous pouvez exécuter tous les tests d’unité de votre application à partir de la racine de votre projet avec la commande. Dans la racine de l'application, exécutez :

npm run test

Si nous observons cette commande dans package.json, nous pouvons constater qu’elle utilise la CLI Jutro. Nous pouvons donc également exécuter tous les tests d'unité de l’application de cette manière :

jutro app:tests

La principale différence entre les deux exemples ci-dessus est l'indicateur --collectCoverage. Nous pouvons utiliser des indicateurs liés à jest directement avec cette commande.

Pour en savoir plus sur l’utilisation de la CLI Jutro à des fins de test, cliquez ici.

L’indicateur --collectCoverage est un alias de --coverage[=<boolean>], ce qui indique que les informations sur la garantie de test doivent être collectées et signalées dans la sortie. En utilisant cet indicateur, vous avez accès à un rapport de garantie de test.

Cet indicateur génère également un nouveau rapport de garantie que vous pouvez voir en ouvrant coverage\lcov-report\index.html.

Exécuter des tests pour un fichier spécifique​

Vous pouvez exécuter des tests pour un fichier ou un composant spécifique afin d’accélérer votre processus de test.

Vous pouvez exécuter un seul test comme celui-ci :

npm run test <TestFileName>
----
jutro app:tests <TestFileName>
----
jutro app:tests path/to/<TestFileName>.test.tsx

N’oubliez pas que npm run test <TestFileName> utilisera --collectCoverage.

Si vous souhaitez exécuter plusieurs tests, il vous suffit d’ajouter autant de noms de fichiers de test que nécessaire aux commandes ci-dessus.

Vous pouvez également exécuter des tests pour un fichier spécifique dans Jest en utilisant l’option --findRelatedTests suivie du chemin d’accès au fichier cible.

npm test -- --findRelatedTests path/to/your-test-file.test.js

Par exemple, si un fichier de test se trouve à l’emplacement src/js/components/Tools.test.tsx, vous pouvez exécuter les tests dans ce fichier à l’aide de la commande suivante :

npm test -- --findRelatedTests src/js/components/Tools.test.tsx

Conventions de fichiers​

Ajoutez des tests aux composants et aux pages selon vos besoins. Des exemples sont disponibles dans :

  • src/app/__tests__
  • src/Pages/CodelessForm/__tests__

Exemple de test d'unité de composant

Il existe quelques cas de tests courants qui peuvent être utilisés pour tester un composant, tels que :

  1. L’élément de test existe/n’existe pas :
expect(screen.getByRole('button')).toBeInTheDocument();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
  1. Le contenu du test correspond :
expect(screen.getByRole('heading').textContent).toBe('Hello World');
expect(screen.getByRole('buttonOne').textContent.toBe(getTranslation(messages.buttonTitle));
  1. La valeur du test est vraie/fausse :
expect(screen.getByDisplayValue('flag').nodeValue).toBeTrue
expect(screen.getByDisplayValue('flag').nodeValue).toBeFalse
  1. Testez la fonction qui a été appelée x fois :
const onChange= jest.fn();
render(<MyDropdown onChange={onChange} />);
userEvent.click(dropdown);
expect(onChange).toHaveBeenCalledTimes(1);

Configuration supplémentaire pour les tests d'unité​

Jest a une configuration prédéfinie pour les applications Jutro. Si vous souhaitez ajouter vos propres options de configuration, vous devez d'abord créer un fichier appelé jest.config.js. Suivez ensuite l'une des deux méthodes ci-dessous :

  • Exportez un objet. Avec cette méthode, votre configuration est fusionnée avec la configuration de base.
jest.config.js
module.exports = {
setupFilesAfterEnv: [
require.resolve('src/setupTests.js'), // path to extra setup file.
],
};
  • Exportez une fonction, où la configuration de base est le premier argument.
jest.config.js
module.exports = (basicConfig) => {
const newConf = { ...basicConfig };
newConf.setupFilesAfterEnv.push(require.resolve('src/setupTests.js'));
return newConf;
};

Assistants dans les tests d'unité​

Le package Jutro @jutro/test propose un certain nombre de fonctions d'aide qui peuvent être utiles pour exécuter des tests, en particulier sur des composants personnalisés.

Voir Applications auxiliaires dans les tests Jutro

Dépendances fictives​

Les fonctions fictives vous permettent de tester les liens entre le code en effaçant l’implémentation réelle d’une fonction, en capturant les appels à la fonction (et les paramètres transmis dans ces appels) et en permettant la configuration des valeurs renvoyées au moment du test.

En outre, dans certains scénarios, vous devrez simuler les dépendances pour éviter tout comportement inattendu. Par exemple, le test d’unité sera à court de mémoire lors de l’appel de la fonction schema getter à partir du SDK Digital Jutro. Pour éviter les fuites de mémoire, nous simulons la fonction schema getter.

Il existe deux façons de simuler des fonctions. Pour ce faire, vous pouvez :

  • Créer une fonction fictive à utiliser dans le code de test.
  • Rédiger une simulation manuelle pour remplacer une dépendance de module.

Simulation d'appels d'API​

Pour simuler des appels d'API, vous pouvez utiliser le package jest.mock. Pour en savoir plus sur la simulation avec Jest, reportez-vous à la documentation Jest sur les fonctions fictives.

Supposons par exemple que votre composant affiche un avatar de l'utilisateur à l'aide du package de transport Jutro.

UserAvatar.js
import React, { useEffect, useState } from 'react';
import { createHttpRequest } from '@jutro/transport';

const baseUrl = 'https://jsonplaceholder.typicode.com';

const getUserData = () => {
return createHttpRequest(baseUrl, true).get('/users/1', {});
};

export const UserAvatar = () => {
const [name, setName] = useState();

useEffect(() => {
getUserData().then((userData) => {
setName(userData.name);
});
}, []);

return (
<div
style={{
fontSize: '2rem',
padding: '3rem',
display: 'flex',
gap: '1rem',
flexDirection: 'column',
}}>
<div>{name}</div>
</div>
);
};

Pour rédiger un test pour ce composant, simulez la fonction get() de createHttpRequest :

UserAvatar.test.js
import React from 'react';
import { render, screen } from '@jutro/test';
import { UserAvatar } from '../UserAvatar';

const explorerName = 'Marco Polo';
jest.mock('@jutro/transport', () => ({
...jest.requireActual('@jutro/transport'),
createHttpRequest: () => ({
get: async () => ({
name: explorerName,
}),
}),
}));

describe('UserAvatar', () => {
it('Displays the name of the famous explorer', async () => {
render(<UserAvatar />);
expect(await screen.findByText(explorerName)).toBeInTheDocument();
});
});

Simulation de configuration​

Pour tester différentes valeurs de configuration, vous pouvez utiliser jest.mock. Supposons par exemple que vous avez un composant appelé ConfigReader qui affiche les valeurs à partir de la configuration Jutro :

ConfigReader.js
import React from 'react';
import { useTranslator } from '@jutro/locale';
import { getConfigValue } from '@jutro/config';

import messages from './ConfigReader.messages';

export const ConfigReader = () => {
const translator = useTranslator();

const configValue = getConfigValue('SOME_CONFIG', '<default value>');

return (
<div data-testid="config-display">
{translator(messages.displayConfigValue, { configValue })}
</div>
);
};

Vous pouvez le tester à l'aide du code suivant :

ConfigReader.test.js
import React from 'react';
import { render, screen, getTranslation } from '@jutro/test';
import { getConfigValue } from '@jutro/config';

import { ConfigReader } from '../ConfigReader';

// Before running tests in this file we need to mock the `getConfigValue` function.
// This particular function requires a bit more handling, as it is used during setup
// and needs a basic implementation.
jest.mock('@jutro/config', () => ({
...jest.requireActual('@jutro/config'),
getConfigValue: jest.fn((_path, defaultValue) => defaultValue),
}));

describe('handling env variables', () => {
describe('using default values', () => {
beforeEach(() => {
getConfigValue.mockImplementation((_path, defaultValue) => defaultValue);
});

it('renders default config value', () => {
render(<ConfigReader />);

expect(
screen.getByText(getTranslation('Config value is: <default value>'))
).toBeInTheDocument();
});
});

describe('using mocked values', () => {
const mockedEnvVariables = {
SOME_CONFIG: '<mocked env var>',
};

beforeEach(() => {
getConfigValue.mockImplementation(
(path, defaultValue) => mockedEnvVariables[path] ?? defaultValue
);
});

it('renders mocked config value', () => {
render(<ConfigReader />);

expect(
screen.getByText(getTranslation('Config value is: <mocked env var>'))
).toBeInTheDocument();
});
});
});

Simulation de la configuration globale​

Vous pouvez simuler la configuration globale de tous les tests de votre application en ajoutant un fichier src/__mocks__/@jutro/config.js.

const actual = jest.requireActual('@jutro/config');

module.exports = {
...actual,
getConfigValue: (path, defaultValue) => {
if (
path === 'accessibleDisabled.all' ||
path === 'accessibleDisabled.button'
) {
return true;
}

return actual.getConfigValue(path, defaultValue);
},
};

Test de composants apparaissant/disparaissant​

Pour tester si quelque chose apparaît/disparaît après une interaction spécifique, utilisons une fenêtre modale Jutro :

MyModalExample.js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslator } from '@jutro/locale';
import { ModalNext } from '@jutro/components';

import { messages } from './MyModalExample.messages';
import { OpenModalTrigger } from './OpenModalTrigger';

export const MyModalExample = () => {
const translator = useTranslator();

const [showModal, setShowModal] = useState(false);

return (
<React.Fragment>
<OpenModalTrigger
onClick={() => setShowModal((value) => !value)}
isOpen={showModal}
/>
<ModalNext isOpen={showModal}>
<span>{translator(messages.modalContent)}</span>
</ModalNext>
</React.Fragment>
);
};

MyModalExample.propTypes = {
title: PropTypes.string,
};

MyModalExample.defaultProps = {
title: 'sample',
};
OpenModalTrigger.js
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@jutro/components';

import { messages } from '../MyModalExample.messages';

export const OpenModalTrigger = ({ onClick, isOpen }) => (
<Button
data-testid="youDontNeedThisId"
onClick={onClick}>
{isOpen ? messages.hideModalButton : messages.showModalButton}
</Button>
);

OpenModalTrigger.propTypes = {
onClick: PropTypes.func,
isOpen: PropTypes.bool,
};

Vous pouvez tester la fenêtre modale à l'aide du code suivant. Notez les commentaires dans le code qui expliquent plus en détail des tests spécifiques.

MyModalExample.test.js
import React from 'react';
import { render, screen, getTranslation, within } from '@jutro/test';
import userEvent from '@testing-library/user-event';

import { messages } from '../MyModalExample.messages';
import { MyModalExample } from '../MyModalExample';

describe('Testing something appears/disappears', () => {
it('does not show modal by default', () => {
render(<MyModalExample />);

expect(
screen.queryByText(getTranslation(messages.modalContent))
).not.toBeInTheDocument();
});

it('toggles modal after clicking the button', async () => {
render(<MyModalExample />);

// When testing for an element not being present on a page,
// be the least specific you can.
// Otherwise it's easy to write a test that doesn't work properly.
// Imagine you want to test that there are no modals on the page, so you wrote a test:
// "I expect there is no modal with title 'My modal' that is also blue."
// "Correct" (but you won't know that there is a modal, but red).
// If you had instead:
// "I expect there is no modal."
// The answer would be "Incorrect, there is a red modal" and you know to fix the component or test.
// In testing if something exists it's the opposite: provide as many details as it makes sense.
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

await userEvent.click(
screen.getByRole('button', {
name: getTranslation(messages.showModalButton),
})
);

expect(screen.getByRole('dialog')).toBeInTheDocument();

// Another thing:
// If you are testing for an element not being present,
// also include an opposite assertion with the same query.
// You might make a mistake and look for an element that could never exist,
// so the test will always pass.
// It's not enough to do:
// "I expect there is no 'thingumajig' on the page."
// "Correct, there is no 'thingumajig'."
// Besides that, you also need an opposite assertion:
// <try to enable the thing>
// "This time I expect there is 'thingumajig' on the page."
// "Incorrect, there is still no 'thingumajig'."
// This is how you know the element you are toggling is actually 'thingummy',
// and the test will catch when someone changes 'thingummy' to 'thingumabob' in the future.
userEvent.click(
screen.getByRole('button', {
name: getTranslation(messages.hideModalButton),
})
);

expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});

it('modal contains the expected content', () => {
render(<MyModalExample />);

userEvent.click(
screen.getByRole('button', {
name: getTranslation(messages.showModalButton),
})
);

// if you have multiple similar elements found by the same query, `within` can help with that.
// Don't overuse within if you don't need to
expect(
within(screen.getByRole('dialog')).getByText(
getTranslation(messages.modalContent)
)
).toBeInTheDocument();
});
});

Sélection d'une option dans une liste déroulante​

Pour tester la sélection dans un DropdownSelectField, créons le composant suivant :

MyDropdown.js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { DropdownSelectField } from '@jutro/legacy/components';

import styles from './MyDropdown.module.scss';
import { messages } from './MyDropdown.messages';

const availableValues = [
{
code: 'option-1',
name: { id: 'option-1.name', defaultMessage: 'Option 1' },
},
{
code: 'option-2',
name: { id: 'option-2.name', defaultMessage: 'Option 2' },
},
{
code: 'option-3',
name: { id: 'option-another.name', defaultMessage: 'Another option' },
},
];

export const MyDropdown = ({ onValueChange }) => {
const [selectedValue, setSelectedValue] = useState(null);

return (
<DropdownSelectField
id="test-dropdown"
label={messages.label}
value={selectedValue}
onValueChange={(newValue) => {
setSelectedValue(newValue);
onValueChange?.(newValue);
}}
availableValues={availableValues}
/>
);
};

MyDropdown.propTypes = {
onValueChange: PropTypes.func,
};

Vous pouvez vous inspirer de ce qui suit pour vos tests de liste déroulante.

MyDropdown.test.js
import React from 'react';
import {
render,
screen,
getTranslation,
checkA11yViolations,
userEvent,
} from '@testing-library/user-event';

import { messages } from '../MyDropdown.messages';
import { MyDropdown } from '../MyDropdown';

describe('Testing a dropdown', () => {
it('exists', () => {
render(<MyDropdown />);

screen.getByRole('combobox', { name: getTranslation(messages.label) });
});

it('contains all expected options', () => {
const expectedOptions = [
getTranslation('Option 1'),
getTranslation('Option 2'),
getTranslation('Another option'),
];

render(<MyDropdown />);

const dropdown = screen.getByRole('combobox', {
name: getTranslation(messages.label),
});

userEvent.click(dropdown);

expectedOptions.forEach((expectedOption) => {
expect(
screen.getByRole('option', { name: expectedOption })
).toBeInTheDocument();
});
});

it('updates the value when selected', () => {
const onChange = jest.fn();

render(<MyDropdown onChange={onChange} />);

const dropdown = screen.getByRole('combobox', {
name: getTranslation(messages.label),
});

userEvent.click(dropdown);

const firstOption = screen.getByRole('option', {
name: getTranslation('Option 1'),
});
const secondOption = screen.getByRole('option', {
name: getTranslation('Option 2'),
});

userEvent.click(firstOption);

// assert
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith('option-1');
expect(dropdown).toHaveValue('option-1');

userEvent.click(dropdown);
userEvent.click(secondOption);

// assert
expect(onChange).toHaveBeenCalledTimes(2);
expect(onChange).toHaveBeenCalledWith('option-2');
expect(dropdown).toHaveValue('option-2');
});
});

Saisie de données dans un composant DateInput​

Pour tester DateInput, commençons par le composant suivant :

MyDateComponent.js
import React, { useCallback, useState } from 'react';
import { DateInput } from '@jutro/components';
import { useTranslator } from '@jutro/locale';

import messages from './MyDateComponent.messages';

export const MyDateComponent = ({ onValueChange: onValueChangeProp }) => {
const translator = useTranslator();

const [value, setValue] = useState();

const onValueChange = useCallback(
(newValue) => {
setValue(newValue);
onValueChangeProp(newValue);
},
[onValueChangeProp]
);

return (
<DateInput
id="myDate"
value={value}
onValueChange={onValueChange}
label={messages.dateLabel}
/>
);
};

Vous pouvez vous inspirer des tests suivants :

MyDateComponent.test.js
import React from 'react';
import { render, screen, getTranslation } from '@jutro/test';
import userEvent from '@testing-library/user-event';

import { MyDateComponent } from '../MyDateComponent';

const selectDate = async (date) => {
const dateInput = screen.getByRole('textbox', {
name: getTranslation('Select your date'),
});

await userEvent.type(dateInput, date);
};

const selectTime = async (time) => {
const timeInput = screen.getByRole('textbox', {
name: /Select your time/,
});

// await userEvent.type(timeInput, time);

await userEvent.click(timeInput);

const timeOption = screen.getByRole('option', {
name: time,
});

await userEvent.click(timeOption);
};

const selectTimezone = async (timezone) => {
const timezoneInput = screen.getByRole('combobox', {
name: getTranslation('Select your timezone'),
});

await userEvent.click(timezoneInput);

const timezoneOption = screen.getByRole('option', {
name: getTranslation(timezone),
});

await userEvent.click(timezoneOption);
};

describe('MyDateComponent', () => {
it('updates date, time and timezone', async () => {
const onValueChange = jest.fn();

render(<MyDateComponent onValueChange={onValueChange} />);

await selectDate('10/30/1994');
await selectTime('06:30 PM');
await selectTimezone('Antarctica/Troll');

expect(onValueChange).toHaveBeenCalledWith({
datetime: { year: 1994, month: 9, day: 30, hour: 18, minute: 30 },
timezone: 'Antarctica/Troll',
});
});
});

Points clés à prendre en compte en termes de test​

  • Essayez d’éviter d’obtenir l’élément par son ID : les ID peuvent être modifiés facilement et sont parfois générés automatiquement. Il est donc préférable d'obtenir plutôt un élément donné à partir de getByRole, queryByRole voire getByText et getByDisplayValue.

  • Testez le résultat, pas l’implémentation : certaines fonctions possèdent des logiques très complexes et peuvent avoir plusieurs dépendances. Ainsi, au lieu de tester la logique de fonction, concentrez-vous sur l’entrée et la sortie.

  • Utilisez des clés de traduction pour tester si le contenu correspond à la clé d’affichage : la traduction peut changer, donc lors du test des clés d’affichage, essayez de ne pas coder le texte en dur. Utilisez la fonction getTranslation fournie par la bibliothèque de tests Jutro pour obtenir la clé d’affichage à la place.

Mode React Strict​

Le mode React Strict est un composant React natif qui exécute des vérifications supplémentaires lorsqu'il est activé dans un environnement de développement, ce qui vous aide à trouver les bogues couramment rencontrés dans vos composants. Il offre les avantages suivants :

  • Il vérifie toute utilisation d'API obsolètes par vos composants
  • Il effectue un nouveau rendu de vos composants une fois de plus pour essayer de trouver les composants qui modifient involontairement leur comportement lorsqu'ils sont affichés une seconde fois
  • Il réexécute un cycle de configuration et de nettoyage supplémentaire pour tous les effets de votre code

Pour activer le mode React Strict, dans votre fichier .env, ajoutez JUTRO_REACT_STRICT_MODE=true.

Le mode strict n'est pas activé par défaut pour les applications préexistantes, mais la variable d'environnement sera activée dans le fichier .env pour toutes les nouvelles applications Jutro créées à partir d'un modèle d'application Jutro.

Lorsque JUTRO_REACT_STRICT_MODE=true est défini dans le fichier .env, il est lu par la commande jutro app:tests et transmis à l’assistant Jutro render dans @jutro/tests. Ensuite, dans render, la vérification suivante est exécutée :

const isInReactStrictMode = process.env.JUTRO_REACT_STRICT_MODE === 'true';

configure({
reactStrictMode: isInReactStrictMode,
});
Note: Si JUTRO_REACT_STRICT_MODE=true est défini dans le fichier .env, vous pouvez également exécuter la commande npm run test dans votre ligne de commande pour exécuter votre application en mode strict.