Blog
Stratégie de tests Symfony et React : retours rapides, stabilité et coût de maintenance
Choisir les tests Symfony et React selon les risques, obtenir des retours rapides et refactoriser sereinement en maîtrisant le coût de la CI et de la maintenance.
Les tests doivent faciliter les changements
Prenons une évolution d’un éditeur d’articles : un brouillon peut être enregistré sans titre, mais sa publication exige un contenu complet. Symfony applique la règle, React l’explique à l’utilisateur. Relancer toute la suite navigateur après chaque modification finira par révéler un parcours cassé. C’est une façon coûteuse de découvrir une mauvaise condition dans une règle métier.
Le raccourci inverse coûte lui aussi : tester la condition isolément, puis supposer que l’éditeur, l’API et la base fonctionnent ensemble. Une stratégie réfléchie attribue un test adapté à chaque risque et conserve des vérifications aux frontières entre les couches.
Le bénéfice est concret : moins d’interruptions, des échecs plus faciles à comprendre pendant un refactoring et moins de régressions pour les utilisateurs. Mais les tests demandent des données, des environnements et de la maintenance. Le diagnostic de leurs échecs mobilise du temps. Il faut maîtriser ce coût sans perdre une confiance utile. Les fonctionnalités décrites ici sont hypothétiques, pas des projets clients de GiSoft.
La rapidité du retour dépend de l’architecture
Le délai utile va de la modification du code au moment où l’on comprend si elle est sûre. Il inclut l’attente d’un runner CI, la préparation de la base et l’analyse d’un échec obscur. Si le résultat arrive pendant une autre tâche, il faut aussi reconstruire le contexte précédent.
Une modification de calcul devrait généralement recevoir une réponse rapide de l’analyse statique et de tests unitaires ciblés. Une modification de repository demande les tests d’intégration correspondants. Le navigateur peut ensuite vérifier le parcours concerné. Commencer par tout exécuter revient à payer le plus gros coût de préparation avant de poser les questions simples.
Mesurez ces coûts dans votre dépôt. L’analyse statique n’est pas toujours rapide ; un petit test d’intégration peut coûter moins qu’un test unitaire saturé de mocks. Suivez le délai jusqu’au premier échec exploitable autant que la durée totale du pipeline. La rapidité ne vaut quelque chose que si le défaut pertinent est détecté.
Partir de ce qui peut échouer
Les équipes nomment leurs suites différemment. La distinction utile porte sur les éléments réels exécutés et ceux remplacés. Décrivez un défaut possible, puis choisissez le test le moins coûteux capable de l’observer :
Défaut possible Première frontière utile
Calcul métier incorrect Unitaire : entrées et résultat
Requête Doctrine incorrecte Intégration : base de test
Permission API absente Fonctionnel : requête et effet
Saisie perdue après erreur Composant : interaction
Format de réponse modifié Contrat : producteur et client
Parcours complet interrompu Navigateur : parcours choisi
Déploiement indisponible Smoke : point d’entrée déployéC’est un point de départ, pas une affectation rigide. Une règle d’autorisation peut nécessiter beaucoup de cas unitaires et un test HTTP prouvant que la route l’applique. Les niveaux supérieurs vérifient les raccordements ; ils n’ont pas à répéter toutes les variantes des niveaux inférieurs.
Les contrôles statiques apportent des résultats tôt. PHPStan et TypeScript détectent types incompatibles, hypothèses dangereuses sur null et violations des interfaces déclarées. Le linting et les règles d’import explicites repèrent certaines confusions entre code client et serveur. Les tests d’architecture peuvent imposer un sens de dépendance convenu, par exemple exclure les types HTTP des règles métier. Ils ne vérifient que le code analysé et les règles configurées, pas le comportement métier à l’exécution.
Avec Symfony, tester les décisions directement et l’infrastructure réellement
Les tests unitaires conviennent aux objets-valeurs, calculs, règles, mappers et validateurs propres au projet. Un objet simple raccourcit le chemin entre l’entrée et la cause de l’échec. Il ne demande généralement ni kernel ni base de données.
Cet exemple Pest suppose une classe illustrative PublishPostPolicy. Sa méthode reçoit titre et contenu, puis refuse la publication si l’un reste vide après suppression des espaces aux extrémités. L’implémentation et l’import sont omis : ce n’est ni une API Symfony ni une fonction existante du projet. La syntaxe correspond à la pile Pest 3/PHPUnit 11 du dépôt :
it('exige un contenu avant publication', function (
string $title,
string $body,
bool $allowed,
): void {
$policy = new PublishPostPolicy();
expect($policy->canPublish(title: $title, body: $body))
->toBe($allowed);
})->with([
['', 'Article text', false],
['Title', '', false],
[' ', 'Article text', false],
['Title', 'Article text', true],
]);Le cas valide est nécessaire : une règle refusant toute publication passerait sinon le test. On vérifie ici que le contenu est prêt, pas que l’appelant a le droit de publier. L’autorisation demeure une responsabilité distincte du serveur.
Les tests d’intégration justifient leur coût quand la frontière elle-même peut échouer : mapping Doctrine et DQL, assemblage du conteneur, configuration du sérialiseur, adaptateur de cache ou routage Messenger. Utilisez l’infrastructure réelle concernée. Simuler QueryBuilder ne révèle pas une jointure qui duplique les articles. La base de test doit être isolée, avec un moteur et un schéma adaptés ; donnez la priorité aux requêtes importantes ou complexes.
Les tests fonctionnels d’API font passer des requêtes par Symfony. Ils vérifient routage, méthodes, justificatifs d’authentification, autorisation, décodage, validation, réponse publique et effets sélectionnés. Pour publier, il faut trois résultats : refus anonyme selon le contrat, refus sans modification pour un utilisateur reconnu mais non autorisé, succès avec modification pour un utilisateur autorisé. Un 403 ne prouve pas à lui seul une authentification réussie ; point d’entrée et configuration déterminent la réponse. Le client de test Symfony ne vérifie ni le proxy déployé ni un navigateur réel.
L’article GiSoft Comment tester les API Symfony avec Pest développe ces exemples HTTP. Ici, la décision consiste à vérifier la règle à faible coût, puis à prouver que l’endpoint l’utilise. Un test de sérialiseur ou de repository ne remplace pas ce raccordement.
Avec React, vérifier l’usage et la reprise après erreur
Fonctions pures, reducers, formatters et transitions d’état se prêtent aussi aux tests unitaires. Les tests de composants deviennent utiles lorsque rendu et interaction se combinent : validation, chargement, boutons désactivés, échec récupérable et résultat réussi.
Le dépôt utilise déjà Vitest, React Testing Library et user-event, avec les matchers jest-dom dans la préparation des tests. L’extrait suppose un DraftEditor illustratif dont l’implémentation n’est pas fournie. Il reçoit initialTitle et le callback asynchrone saveDraft({ title }), puis affiche les libellés anglais utilisés ici :
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect, it, vi } from 'vitest';
import { DraftEditor } from './DraftEditor';
it('conserve le texte modifié après un échec', async () => {
const user = userEvent.setup();
const saveDraft = vi.fn().mockRejectedValue(new Error('Unavailable'));
render(<DraftEditor initialTitle="Release notes" saveDraft={saveDraft} />);
const title = screen.getByRole('textbox', { name: 'Title' });
await user.clear(title);
await user.type(title, 'Revised notes');
await user.click(screen.getByRole('button', { name: 'Save draft' }));
expect(await screen.findByRole('alert'))
.toHaveTextContent('Could not save');
expect(screen.getByRole('textbox', { name: 'Title' }))
.toHaveValue('Revised notes');
expect(screen.getByRole('button', { name: 'Save draft' })).toBeEnabled();
expect(saveDraft).toHaveBeenCalledWith({ title: 'Revised notes' });
});Le test protège une promesse précise : après l’échec, le texte modifié reste présent et l’utilisateur peut réessayer. Il n’inspecte ni état interne d’un hook, ni fonction privée, ni imbrication des composants. Les recherches par rôle et nom accessible expriment généralement mieux l’intention que des sélecteurs CSS liés à la mise en page. Elles ne démontrent pas à elles seules la conformité en matière d’accessibilité.
Testez le chargement séparément avec une promesse contrôlée, laissée en attente puis terminée explicitement. Un bouton désactivé peut empêcher un second clic dans cette interface ; il ne prouve pas l’idempotence du backend. Masquer la publication n’impose aucune autorisation serveur. Les grands snapshots de pages brouillent souvent ces distinctions, même si un petit snapshot convient lorsque le rendu lui-même constitue le contrat.
Vérifier le contrat entre Symfony et Next.js
Les tests de contrat vérifient l’accord entre producteur et consommateur : statuts, champs obligatoires ou nullables, dates, codes d’erreur, pagination et hypothèses d’authentification. Ils peuvent s’appuyer sur un schéma, une vérification du fournisseur ou des exemples contrôlés des deux côtés. Une fixture frontend inventée indépendamment du backend prouve seulement sa propre cohérence avec le test.
TypeScript décrit les attentes du code compilé. Affirmer qu’un JSON décodé respecte une interface ne valide pas les données. À la frontière de l’adaptateur API, testez une réponse valide et une incompatibilité significative avec la validation d’exécution déjà adoptée. Si Symfony transforme publishedAt d’une chaîne ISO en nombre, le consommateur doit prendre en charge le changement convenu ou le refuser de manière prévisible. Un schéma ne détecte pas une réponse correctement structurée contenant les données d’un autre utilisateur.
Next.js ajoute une frontière d’exécution. Navigation côté client, chargement complet et rendu serveur peuvent obtenir différemment les données en cache et les informations d’authentification. Un fetch serveur n’hérite pas automatiquement des cookies entrants du navigateur ou de son en-tête Authorization. Testez la transmission prévue sans recopier des en-têtes arbitraires. Gardez le mapping pur facilement testable et vérifiez le comportement serveur dans un environnement Next.js adapté.
La prise en charge des Server Components dépend des outils et de leurs versions. Le guide Vitest de Next.js 16 indique que les Server Components asynchrones ne sont pas pris en charge et recommande des tests E2E. Un test de composant client réussi ne renseigne donc pas sur ce chemin serveur. L’article GiSoft Tester l’architecture, les défaillances et les parcours utilisateur avec Next.js traite plus largement ces frontières et la gestion des échecs.
Réserver le navigateur aux connexions importantes
Les tests navigateur/E2E peuvent vérifier ensemble navigation, rendu, authentification et backend. Leur périmètre de diagnostic est large : un sélecteur, une session expirée, des données absentes, le réseau ou un service peuvent expliquer le même échec. Ils sont particulièrement utiles pour un achat, une connexion ou une publication critique, complétés par un cas de reprise si l’échec coûte cher.
Utilisez le runner navigateur du projet. Playwright est une possibilité, mais le manifeste de ce frontend ne déclare pas actuellement de suite Playwright configurée. Un exemple d’article ne doit pas imposer une seconde pile de tests.
Précisez les éléments remplacés. Un test navigateur dont toutes les réponses API sont simulées vérifie le parcours dans le navigateur, pas l’intégration Symfony réelle. Reliez au moins le chemin critique pertinent à des données backend maîtrisées. En CI ordinaire, utilisez des doubles de fournisseurs ; séparez les vérifications volontaires contre leurs environnements de test. Paiements réels et services externes non maîtrisés n’ont pas leur place dans les exécutions courantes.
Les smoke tests répondent à une question de déploiement plus limitée : le point d’entrée répond-il, et une lecture sans risque ou une vérification essentielle de dépendance aboutit-elle ? Ils complètent la suite applicative et la supervision sans valider toutes les permissions et règles métier.
Des seuils de régression protègent un endpoint de liste sensible contre une hausse des requêtes, une réponse excessive ou une pagination ignorée. Définissez-les sur des données représentatives et excluez la préparation du périmètre mesuré. Un nombre borné de requêtes ne mesure pas la latence. Réservez le diagnostic N+1 et les performances aux tests ciblés et au profilage plutôt qu’à des chronométrages dans chaque parcours navigateur.
Organiser la CI pour obtenir des résultats exploitables
Un pipeline raisonnable commence par syntaxe, types et autres contrôles peu coûteux, puis couvre tests unitaires et composants ciblés, infrastructure, API et parcours navigateur sélectionnés. Les tâches indépendantes peuvent tourner en parallèle. Certains tests d’intégration sont assez légers pour démarrer immédiatement ; une bibliothèque partagée modifiée peut justifier une vérification large dès le départ.
Surveillez la durée d’attente et la consommation totale des runners. Tout paralléliser peut raccourcir le délai tout en augmentant le coût ou la contention sur une base commune. Isolez les données, espaces de noms de cache et ports nécessaires. Mettez les dépendances en cache selon leurs lockfiles, pas l’état mutable laissé par les tests.
Pendant le développement, exécutez les tests proches du changement. Avant fusion, adaptez le périmètre à son impact : sélectionner uniquement les fichiers modifiés peut ignorer les effets du code partagé, des schémas ou de la configuration. Gardez des contrôles plus larges à une étape adaptée. En cas d’échec navigateur, conservez traces et journaux utiles sans secrets. Une nouvelle tentative peut aider au diagnostic ; elle ne doit pas transformer silencieusement un test instable en résultat réputé fiable.
Mesurez attente, préparation, suites lentes, échecs intermittents et effort d’investigation. Améliorez le poste réellement coûteux avant d’acheter des runners ou de supprimer des assertions utiles. Aucune durée CI universelle ni proportion fixe entre tests unitaires et E2E ne suffit à définir une bonne stratégie.
Intégrer la maintenance dans la conception
Un test apporte de la valeur lorsqu’il échoue pour un changement significatif et aide à comprendre pourquoi. Des échecs répétés dus à un balisage sans rapport ou à des fixtures partagées prennent du temps et dégradent la confiance. Ce coût relève de la conception, pas seulement d’un futur chantier de nettoyage.
Gardez des fixtures réduites et explicites sur l’état important : propriétaire, rôle, publication, langue. Utilisez des identifiants déterministes et une horloge injectée lorsque le temps compte. Isolez l’état mutable et évitez les dépendances à l’ordre d’exécution. Une fonction utilitaire doit enlever les répétitions sans cacher authentification, écritures en base ou raison du succès attendu.
Remplacez une dépendance lorsqu’il faut contrôler sa réponse. Gardez-la réelle si son comportement constitue le risque. Trop de mocks peuvent faire réussir un test de service malgré un assemblage ou une persistance incorrects. Ils lient aussi les tests à l’ordre des appels et à la structure des collaborateurs. Privilégiez les résultats observables ; comptez les appels internes seulement si cette interaction est elle-même une exigence.
Ne payez pas plusieurs fois pour la même preuve. Pour un prix, les tests unitaires couvrent arrondis et limites ; l’API vérifie la représentation du montant et de la devise ; le navigateur confirme que le client termine son achat. Rejouer chaque arrondi dans le navigateur augmente préparation et diagnostic sans beaucoup mieux vérifier les connexions.
Les tests d’un comportement stable devraient largement survivre aux changements de classes ou de composition des composants. Si le comportement évolue volontairement, modifiez contrat et tests ensemble. Supprimez ou déplacez un test redondant après avoir identifié ce qui protège encore le risque. Un test intermittent demande un responsable et une échéance de réparation ; le désactiver indéfiniment ne fait que masquer le manque.
Appliquer la stratégie à une publication d’article
Un premier plan pour notre éditeur hypothétique peut rester limité :
- Règle : cas unitaires de contenu absent, d’espaces seuls et d’article publiable. Aucune préparation HTTP n’est nécessaire.
- Données et accès : intégration pour sélectionner article et traduction ; API pour le refus sans modification et la publication autorisée avec état enregistré.
- Éditeur : composants pour attente, validation, saisie conservée après échec et succès. Un test d’adaptateur vérifie les réponses et erreurs utilisées par ces états.
- Parcours connecté : le navigateur se connecte, modifie et publie, puis une nouvelle lecture confirme le résultat réel. Incluez un rechargement complet si le rendu serveur compte ; ajoutez une reprise lorsqu’elle couvre un risque distinct.
- Livraison : un smoke test sans danger vérifie la route déployée. Ajoutez cache ou budget de requêtes si la fonction introduit précisément ce risque.
Dans une application mature, commencez par le comportement à modifier. Protégez au besoin un parcours critique encore sans test, puis rapprochez les variantes détaillées de la règle à mesure que vous comprenez le code. Il n’est pas nécessaire de réécrire toute la suite ou d’atteindre un pourcentage arbitraire de couverture pour progresser.
Le prochain test doit lever une incertitude nommée. Placez-le là où il peut le faire de manière fiable et mesurez son utilité à la facilité de modifier le code et de diagnostiquer un échec.
Références techniques
- Tests avec Symfony 7.4 : https://symfony.com/doc/7.4/testing.html
- Interactions utilisateur avec Testing Library : https://testing-library.com/docs/user-event/intro/
- Next.js avec Vitest et limites actuelles : https://nextjs.org/docs/app/guides/testing/vitest
- Assertions de type TypeScript et limites à l’exécution : https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions
