Logique de test dans le code de production.
Le code de production contient de la logique, des branches ou des membres qui n'existent que pour faciliter les tests, brouillant la frontière entre ce qui est livré et ce qui est seulement testé.
##Signs and Symptoms
Vous trouvez dans le système sous test (SUT) du code qui n'a d'importance que lorsqu'un test s'exécute. Signes révélateurs :
- Crochets de test / drapeaux de mode — des branches conditionnées par un drapeau
testing,isTest,NODE_ENV === 'test'oumockqui court-circuitent le comportement réel. - Membres « pour les tests uniquement » — setters, getters,
reset()ou constructeurs publics ajoutés uniquement pour qu'un test puisse atteindre un état interne, souvent étiquetés@VisibleForTesting/@TestOnlypuis réellement appelés depuis la production. - Pollution de l'égalité — une logique
equals()/ de comparaison ajoutée à une classe de production uniquement pour qu'une assertion puisse comparer deux objets. - Dépendance de test en production — des modules de production important un framework de test, une fixture ou une fabrique de mocks.
// SMELL : le code livré se comporte différemment lorsque « testing » est activé
class PaymentService {
charge(order: Order) {
if (process.env.NODE_ENV === 'test' || this.isTesting) {
return { status: 'ok', id: 'FAKE-TEST-ID' }; // la vraie passerelle n'est jamais exercée
}
return this.gateway.charge(order); // <-- le chemin réellement livré
}
}
La « vraie » branche est celle que rencontrent vos clients, et c'est exactement celle que vos tests sautent.
##Reasons for the Problem
Pourquoi cela arrive
- Le SUT est difficile à tester (il communique avec un réseau, une horloge, une passerelle de paiement ou un système de fichiers) et ajouter un raccourci
if (testing)est plus rapide que d'introduire une véritable couture (seam). - Un test a besoin d'observer ou de définir un état interne, alors un développeur l'expose « juste pour le test ».
- Le bouchonnage/mocking est malcommode, alors des données toutes prêtes sont codées en dur derrière un drapeau (flag).
Pourquoi c'est nuisible
- Fausse confiance. Les tests exercent la branche réservée aux tests, de sorte que le chemin de production est livré non testé. Des tests verts prouvent que le faux fonctionne, pas la chose réelle.
- Fiabilité / sûreté. Du code réservé aux tests qui survit jusqu'en production peut s'exécuter pour de vrai. La mise en garde canonique de Meszaros est Ariane 5 : du code destiné au sol, resté actif en vol, a déclenché la défaillance. Un
if (isTesting)laissé actif en est la version logicielle. - Sécurité. Les contournements de test sont des portes dérobées — un drapeau qui saute l'authentification, le paiement ou la validation est à une erreur de configuration d'être exploitable.
- Lisibilité et surcharge de l'API. Les setters/getters/
reset()réservés aux tests agrandissent la surface publique et trompent les vrais clients sur la vocation de la classe. - Maintenabilité. Deux comportements cohabitent dans une seule classe ; chaque modification doit raisonner à la fois sur le chemin de production et sur celui de test, et la divergence pourrit discrètement.
##Treatment
Sortez la logique de test du code de production en introduisant une véritable couture (seam) plutôt qu'un drapeau.
- Injectez la variation (injection de dépendances + doublure de test). Remplacez la branche codée en dur par un collaborateur que le test substitue. La production câble l'implémentation réelle ; le test câble un faux/bouchon/mock.
- Utilisez une sous-classe spécifique aux tests lorsque vous n'avez besoin de redéfinir qu'une seule méthode — redéfinissez-la dans une sous-classe vivant dans le code de test, et non via un
ifdans la classe de base. - Appliquez le patron Humble Object pour extraire la logique difficile à tester (horloge, E/S) derrière un adaptateur mince, afin que la logique centrale devienne directement testable sans crochets.
- Déplacez la logique de comparaison côté test. Au lieu de polluer la production avec un
equals()destiné aux assertions, utilisez un matcher/comparateur personnalisé ou assertez sur les champs qui vous intéressent. - Gardez le code de test hors du build. Utilisez une séparation par source-set / configuration de build afin que les helpers de test ne puissent pas être compilés dans l'artefact livré ; marquez les membres réellement visibles pour les tests avec
@VisibleForTesting/@TestOnlyet laissez un linter s'assurer que la production ne les appelle jamais.
// AVANT : crochet de test à l'intérieur du code de production
class PaymentService {
charge(order: Order) {
if (this.isTesting) return { status: 'ok', id: 'FAKE-TEST-ID' };
return this.gateway.charge(order);
}
}
// APRÈS : un seul chemin de code ; la passerelle est injectée et simulée dans le test
class PaymentService {
constructor(private gateway: PaymentGateway) {}
charge(order: Order) {
return this.gateway.charge(order); // même chemin en prod et en test
}
}
// test
const fakeGateway = { charge: () => ({ status: 'ok', id: 'FAKE-TEST-ID' }) };
const service = new PaymentService(fakeGateway);
Désormais, le chemin de production est le seul chemin, et le test contrôle le comportement depuis l'extérieur.
##Detected by
- codeql java/visible-for-testing-abuse — Utilisation de VisibleForTesting dans du code de production
- deepsource JAVA-A1067 — Les méthodes @VisibleForTesting/@TestOnly ne doivent pas être utilisées dans du code hors test
- android-lint VisibleForTests — Visible Only For Tests