
Table des matières
Ouvrir table des matières
La meilleure suite de tests est celle que vous n’écrivez pas
Voici une situation que tout intégrateur connaît, dans un domaine ou un autre : le système d’en face — un régulateur, un partenaire, une place de marché — applique une batterie de contrôles à vos fichiers, et rejette sans pitié tout ce qui ne passe pas. Longtemps, la seule façon d’apprendre qu’un fichier violait une règle était… de l’envoyer, et de lire le rejet. Tester en production chez l’autre, en somme. Charmant.
Et puis un jour, le système d’en face publie ses contrôles. Les vrais. Ceux-là mêmes qu’il exécutera sur vos dépôts. À partir de ce jour-là, une évidence devrait s’imposer : la meilleure suite de tests que vous puissiez écrire pour vos sorties est celle que vous n’écrivez pas — exécutez SES contrôles sur VOS fichiers, avant chaque envoi.
C’est ce que j’ai fait sur une plateforme de facturation électronique, le jour où l’administration a publié ses règles de contrôle. Cet article raconte ce que cette bascule a révélé dès le premier jour — deux bugs de conversion invisibles de toute ma suite de tests — et les pièges très concrets d’industrialisation d’artefacts de validation qu’on n’a pas le droit de modifier. Parce que oui, il y a un vrai savoir-faire là-dedans, et il vaut bien au-delà de la facture électronique.
Le décor : trois étages de validation, et un trou au milieu
Le contexte, pour ceux qui n’ont pas suivi ma série sur la réforme française : les plateformes agréées déposent des flux auprès du portail public, et celui-ci leur applique une batterie de contrôles publiée sous forme de schematrons — plusieurs centaines d’assertions XPath. Une assertion violée, et c’est le dépôt rejeté. Ces fichiers de règles sont publics, téléchargeables par quiconque.
Ma plateforme, pourtant, validait déjà beaucoup — et c’est le point intéressant. Trois familles de contrôles existaient : le schéma XSD (la structure du XML), les règles sémantiques de la norme européenne (la cohérence de la facture source), et la suite de tests interne. Trois étages sérieux, tous verts.
Mais regardez bien le trou : personne ne validait les octets réellement déposés avec les règles réellement appliquées en face. L’XSD juge la structure, pas les règles métier du portail. Les règles sémantiques jugent la facture source, pas le fichier converti qui part réellement. Trois familles de gardes, et aucune ne jugeait la bonne chose au bon endroit. Si vous avez lu mes 600 tests verts, le motif va vous parler : une couverture impeccable de tout… sauf du produit final.
La décision : la porte d’émission
Le jour où l’administration a publié ses schematrons, la décision a été immédiate : les exécuter en porte d’émission — le tout dernier contrôle avant tout pas irréversible, archivage ou dépôt. Avec deux principes non négociables :
- Zéro assertion neutralisée. Pas de liste d’exclusions « le temps de corriger », pas de règle commentée. Les règles d’en face, toutes, telles quelles — sinon la porte ne prouve plus rien.
- Fail-closed. Si le moteur de validation est indisponible, le contrôle est marqué « sauté » et loggé — jamais compté comme passé. Les lecteurs de mon autopsie d’un échec silencieux reconnaîtront le principe : un contrôle qui n’a pas tourné se dit sauté, il ne se déguise pas en contrôle vert.

Jour 1 : deux bugs que toute la suite de tests ignorait
Premier réflexe une fois la porte construite : l’exécuter sur nos propres fichiers « dorés » — les fixtures de référence de la suite de tests, celles qui passent partout depuis toujours. Résultat : deux défauts, immédiatement. Sur les fichiers de référence. Ceux qui incarnaient notre définition du « correct ».
Bug n°1 : l’asymétrie entre deux convertisseurs
Mon pipeline sait produire deux syntaxes XML pour le même modèle sémantique — c’est le lot commun du domaine, UBL et CII pour ne pas les nommer. Deux convertisseurs, donc, censés être équivalents.
Le validateur d’en face a révélé qu’ils ne l’étaient pas. Un champ optionnel dans la norme mais exigé par les contrôles du portail — le prix brut unitaire — était fabriqué avec une valeur de repli par l’un des convertisseurs quand la source ne le portait pas… et simplement recopié tel quel par l’autre. Résultat : dans une des deux syntaxes, chaque ligne de facture sans remise explicite aurait été rejetée fatalement en production.
Pourquoi aucun test ne le voyait ? Parce que toutes les fixtures portaient le champ. Toutes. Les fichiers dorés avaient été construits par la même équipe que le code, avec les mêmes hypothèses implicites — des factures « bien remplies ». La suite validait à merveille un monde où le problème ne pouvait pas exister.
Bug n°2 : l’élément creux
Le second est un classique de sérialisation que je vous laisse savourer. Le sérialiseur XML, configuré pour ne pas supprimer les nœuds vides, émettait <DueDate/> quand la date d’échéance était absente de la source. Un élément présent mais vide, sur un champ typé date — violation fatale du XSD : « ” n’est pas une date ».
Le pire n’était pas le bug, mais son symptôme : la facture restait bloquée dans un état intermédiaire pendant que le flux parallèle — qui ne traverse pas ce convertisseur — partait normalement. Vu de loin : « un des deux canaux a un problème de transport ». La panne se lisait à des kilomètres de sa cause, dans la mauvaise couche.
La correction : structurelle, pas au cas par cas
La tentation, c’était deux rustines — un if pour le prix brut, un if pour la date. J’ai préféré corriger la classe entière du problème : un élagage récursif des nœuds non déclarés, au point unique de sérialisation. Tout champ optionnel futur hérite de la garantie, sans qu’on y pense.
On veut du code ! Le pattern, réécrit en générique :
// Au point UNIQUE de sérialisation : élaguer l'absence, préserver le déclaré.
function pruneUnstated(node: Record<string, unknown>): void {
for (const [key, value] of Object.entries(node)) {
if (value === null || value === undefined) {
delete node[key]; // absent → ne doit produire AUCUN élément
} else if (typeof value === "object") {
pruneUnstated(value as Record<string, unknown>);
if (Object.keys(value as object).length === 0) delete node[key];
}
// '' et 0 sont des valeurs DÉCLARÉES : elles survivent.
}
}La nuance qui fait tout, c’est le commentaire de la dernière ligne : une valeur déclarée — chaîne vide, zéro — survit à l’élagage ; seule l’absence est élaguée. « Le client a mis zéro » et « le client n’a rien dit » sont deux informations différentes, et un élagage qui confondrait les deux fabriquerait des bugs plus subtils que ceux qu’il corrige. Et pour prouver la non-régression : les sorties de référence sont restées byte-identiques après le correctif — l’élagage ne change rien aux fichiers sains, il ne supprime que ce qui n’aurait jamais dû exister.
Industrialiser un artefact qu’on n’a pas le droit de toucher
Reste la partie la moins glamour et la plus instructive : faire de ces fichiers de règles un composant de production. Car les artefacts d’un régulateur ont une double particularité : ils sont imparfaits — et intouchables. On ne patche pas les règles de l’arbitre. Trois pièges concrets, trois parades.
Les assertions n’ont pas d’identifiant exploitable. Pour tracer « quelle règle a rejeté ce fichier », le réflexe serait d’annoter les fichiers du régulateur. Interdit — on veut pouvoir jurer qu’on exécute leurs règles, byte pour byte. La parade : l’identifiant de règle est lu dans le texte du message d’erreur produit, et les fichiers officiels restent intacts, épinglés par hash. Le jour où quelqu’un doute, le hash répond.
La compilation se fait au build, jamais au runtime. Un schematron se compile en XSLT avant exécution. Cette compilation vit dans la chaîne de build, avec un compilateur épinglé en version — parce qu’une validation dont le résultat dépend de l’outillage présent au runtime n’est pas reproductible, et qu’une porte d’émission non reproductible est une porte qui dit parfois oui, parfois non, au même fichier.
La sélection des règles se fait par les données, jamais par le calendrier. Le piège le plus sournois des trois. La réglementation a deux trajectoires datées — des jeux de règles qui changent à une date de bascule. La tentation : if (today >= DATE_BASCULE) dans le sélecteur de feuille de contrôles. C’est une bombe à retardement : le jour J, à minuit, des dépôts parfaitement légaux auraient été rejetés en masse par un if que personne ne regardait plus. La parade : sélectionner par les propriétés du document — sa syntaxe, son profil émis. Le comportement devient testable aujourd’hui pour les deux trajectoires, et correct après la bascule sans redéploiement. La logique calendaire dans un sélecteur de règles, c’est non — et j’aurais aimé lire cette phrase avant de faillir l’écrire.
Ce que j’en retiens
Vos fixtures partagent vos angles morts. Une suite de tests construite par la même équipe que le code encode les mêmes hypothèses — mes fichiers dorés portaient tous le champ dont l’absence tuait. Le validateur de l’adversaire (au sens : celui qui décidera du rejet) n’a pas vos hypothèses, et c’est exactement ce qui le rend précieux.
Validez la bonne chose : les octets qui partent, pas le modèle interne. Un pipeline peut avoir trois étages de validation et laisser passer un fichier invalide, si aucun étage ne regarde le produit final avec les règles finales. La question à se poser sur chaque gate : qu’est-ce qu’elle juge, exactement, et est-ce bien ce qui sort ?
Un artefact de validation externe se vendorise épinglé et intouchable. Hash, compilation reproductible au build, identifiants lus sans modifier les fichiers. Le jour où le régulateur corrige SA version, on re-vendorise et on ré-épingle — jamais de patch local, jamais.
Jamais de logique calendaire dans un sélecteur de règles. Sélectionner par les propriétés du document rend le comportement testable aujourd’hui et correct après la date de bascule, sans redéploiement ni sueur froide à minuit.
Épilogue
Récapitulons le voyage : un régulateur qui publie ses contrôles, une porte d’émission qui les exécute — zéro assertion neutralisée, fail-closed — et, dès le premier jour, deux bugs de conversion que des années de suite de tests n’avaient jamais vus, parce que les fixtures partageaient les hypothèses du code. Puis le travail d’orfèvre : un élagage structurel au point unique de sérialisation, et trois disciplines pour industrialiser des règles qu’on n’a pas le droit de toucher — épinglées par hash, compilées au build, sélectionnées par les données.
La morale tient en une phrase, et elle dépasse largement mon domaine : quand celui qui décidera du rejet publie ses règles, les exécuter chez vous n’est pas une option, c’est la définition même de « prêt à envoyer ». Tout le reste — vos tests, vos validations intermédiaires — reste utile, mais juge autre chose.
Et vous, votre « adversaire » publie-t-il ses contrôles — schematrons réglementaires, contrats d’API, suites de conformité ? Les exécutez-vous sur vos sorties, ou découvrez-vous les rejets en production ? Racontez-moi ça en commentaire, je me ferai une joie de vous répondre.

