Tests
Comment l'application est testée : unitaire (Jest), architecture (dependency-cruiser) et E2E (Detox).
Les quatre axes
| Axe | Outil | Commande |
|---|---|---|
| Unitaire / composant | Jest + @testing-library/react-native |
yarn test / yarn jest |
| Statique / lint | ESLint, TypeScript | yarn lint, yarn typecheck |
| Architecture | dependency-cruiser | yarn depcruise |
| E2E | Detox | yarn detox-* |
Tests unitaires (Jest)
Config : jest.config.js.
- preset :
react-native. - testMatch :
**/*.test.tset**/*.test.tsx— les tests sont co-localisés près du code source, pas dans un dossier__tests__/. (__tests__/ne contient que l'E2E + le scaffolding.) - transform :
babel-jestviababel.config.js. Les alias de chemins (components,store, …) sont résolus par le module-resolver de babel, pas par unmoduleNameMapperJest. - coverageThreshold : un plancher global (lines 80 / functions 80 / statements 80 / branches 70) plus une exigence de 100 % par fichier sur
src/features/**/*.{ts,tsx}(statements / branches / functions / lines). La CI lanceyarn jest --coverage, donc passer sous l'un ou l'autre fait échouer le build. - collectCoverageFrom est limité à
src/features/**, en excluant*.test.*, le barrelindex.ts, lestypes.ts(types uniquement),queries/**(déclarations gql) ettest-utils/**. Ainsi--coveragemesure et verrouille toute la couche feature à 100 %. Pour itérer sur une seule feature, ciblez-la avec--collectCoverageFrom='src/features/<name>/**/*.{ts,tsx}'(sinon les autres features reportent 0 % et échouent). Voir.claude/commands/test.md. - Il n'y a aucun
setupFilesAfterEachni registre de mocks global.
Commandes
| Commande | Ce qu'elle fait |
|---|---|
yarn test |
jest — la suite unitaire |
yarn jest |
jest -u --forceExit — met à jour les snapshots, force la sortie (handles/timers ouverts) |
yarn jest --coverage |
ce que lance la CI ; impose le plancher global 80/80/70/80 et 100 % sur chaque fichier src/features |
yarn typecheck |
tsc --noEmit |
yarn lint |
eslint src/ |
Mocks
Le __mocks__/ racine est minimal — async-storage-mock.ts, fileMock.js, styleMock.js. Il n'y a aucun fichier de setup central câblant les mocks, donc le mocking se fait en ligne par test avec jest.mock(...). Il n'existe pas de mocks partagés pour BLE / navigation / Apollo — mockez-les au cas par cas dans le test qui en a besoin.
Les modules de feature embarquent leurs propres helpers de test, ex. src/features/unitPreference/test-utils/ (mockUseUnitPreferences.ts, mockUnitPreferenceState.ts). Suivez ce pattern : placez les mocks réutilisables dans un dossier test-utils/ à l'intérieur de la feature.
Patterns
Il n'y a aucun render / wrapper de provider personnalisé — les tests rendent directement les composants feuilles et mockent les dépendances en ligne. Deux patterns canoniques :
Test de composant — mocker le hook de thème :
import { fireEvent, render } from '@testing-library/react-native';
import { UnitSelector } from './UnitSelector';
jest.mock('components/Navigation/types/theme', () => ({
useAppTheme: () => ({ roundness: 4, colors: { /* … */ } }),
}));
it('calls onChange with the selected unit', () => {
const onChange = jest.fn();
const { getByTestId } = render(<UnitSelector units={units} value="mm" onChange={onChange} />);
fireEvent.press(getByTestId('unit-selector-button-cm'));
expect(onChange).toHaveBeenCalledWith('cm');
});
Test de hook — mocker Apollo + réinitialiser le store zustand :
import { useMutation, useQuery } from '@apollo/client';
import { renderHook } from '@testing-library/react-native';
jest.mock('@apollo/client', () => ({
...jest.requireActual('@apollo/client'),
useQuery: jest.fn(),
useMutation: jest.fn(),
}));
beforeEach(() => {
jest.clearAllMocks();
useUnitPreferenceStore.getState().reset(); // réinitialise l'état zustand local à la feature
(useQuery as jest.Mock).mockReturnValue(defaultQueryResult);
(useMutation as jest.Mock).mockReturnValue([mockUpdateUnitPreference]);
});
Conventions distillées des tests réels :
- Assertez sur
testID,accessibilityStateet les props — pas les snapshots (il y a zéro fichier.snapdans le dépôt). - Les hooks Apollo sont mockés directement (
jest.mock('@apollo/client', …)), pas viaMockedProvider. - Les stores zustand sont réinitialisés dans
beforeEachviagetState().reset(). - La logique de domaine pure (
domain/, mappers, ducks) est testée sans React — voirunitPreference/domain/*.test.tsetstore/ducks/token.test.ts. - Les flux asynchrones utilisent
await act(async () => …).
L'essentiel de la couverture actuelle vit dans src/features/* et src/store/ducks/. Le nouveau code de feature doit livrer des tests co-localisés (DangerJS avertit quand des fichiers src/ sont ajoutés sans fichier .test. correspondant — voir ci-cd.md).
Tests d'architecture (dependency-cruiser)
yarn depcruise valide .dependency-cruiser.js. Il échoue sur les dépendances circulaires à l'intérieur de src/features/ et avertit sur les modules orphelins. Lancé en CI ; DangerJS fait fail() sur la PR à la moindre error dep-cruiser. Voir architecture.md.
E2E (Detox)
Config : .detoxrc.js. Le test runner est Jest avec __tests__/e2e/jest.config.js (maxWorkers: 1, testTimeout: 120000). Les specs vivent dans __tests__/e2e/tests/ (actuellement homeButtons.test.js, navbarButtons.test.js) et utilisent device.launchApp(), element(by.id(...)), .tap(), expect(...).toBeVisible().
L'application doit être buildée et servie par Metro avant de lancer l'E2E :
yarn detox-build-ios-debug # build l'app pour le simulateur
yarn detox-test-ios-debug # lance les specs
# variantes android : detox-build-android-debug / detox-test-android-debug
Warning
Deux scripts sont mal nommés : detox-build-ios-release build en réalité ios.sim.debug, et detox-build-android-release build android.emu.debug. Ne vous fiez pas aveuglément au nom -release.
Fichiers clés
jest.config.js,babel.config.js(résolution des alias)__mocks__/,src/features/unitPreference/test-utils/.detoxrc.js,__tests__/e2e/.dependency-cruiser.js