Réservé aux testeurs.
Le code de production contient des méthodes, des accesseurs d’état ou des joints qui n’existent que pour être utilisés par les tests, polluant la véritable API et incitant à des tests qui vérifient les détails internes plutôt que le comportement.
##Signs and Symptoms
Vous trouvez dans le code de production des membres dont les seuls appelants vivent dans des fichiers de test. Signes révélateurs :
- Des méthodes, des getters/setters, des exports ou des paramètres de constructeur utilisés exclusivement depuis
*.test.ts/*.spec.ts. - Des noms ou des marqueurs à saveur de test :
getStateForTest,resetForTesting,__getInternal,FTO_*,forTest, des commentaires comme// only used by tests, ou des annotations telles que@VisibleForTesting/@TestOnly/@internal. - Une visibilité relâchée (un champ rendu
public/exporté, un#privatepassé enprotected) uniquement pour qu’un test puisse atteindre l’état interne. - Des « joints » supplémentaires — un
setClock(...), unsetRandom(...)ou unreset()— ajoutés uniquement parce qu’un test en avait besoin, et non parce que la conception réelle l’exige.
// payment-service.ts (code de PRODUCTION)
export class PaymentService {
#ledger: Entry[] = [];
charge(amount: number) { /* ... */ }
// Rien en production n'appelle jamais ceci — seuls les tests le font :
getLedgerForTest() { return this.#ledger; } // expose les détails internes
setClockForTest(now: () => Date) { this.now = now; } // joint réservé aux tests
FTO_reset() { this.#ledger = []; } // "Réservé aux tests"
}
Une vérification rapide : grep -rn 'forTest\|ForTesting\|FTO_' src/ qui renvoie des résultats dans le code source de production, ou une passe de détection de code mort (par exemple Knip en mode production) qui signale un export comme inutilisé alors qu’un test l’importe clairement.
##Reasons for the Problem
Pourquoi cela arrive
- Greffer des tests sur du code non testable. Lorsque du code hérité n’a pas été conçu pour la testabilité, le moyen le plus rapide de vérifier un résultat est de percer un trou dans le SUT et d’en lire les détails internes — Meszaros cite cela comme la cause principale de For Tests Only.
- API asymétriques. Les vrais clients utilisent un objet d’une seule façon (écriture) ; les tests l’utilisent de façon symétrique (écrire puis relire pour vérifier), de sorte que les testeurs « ont besoin » d’accesseurs dont aucun appelant de production n’a besoin.
- Pression des délais. Ajouter une porte dérobée coûte moins cher sur le moment que de refactorer vers une conception où le comportement est observable via le contrat public.
Pourquoi c’est nuisible
- Lisibilité. L’API publique ne dit plus la vérité — les mainteneurs ne peuvent plus distinguer le vrai contrat de l’échafaudage de test, et chaque lecteur doit se demander « cette méthode est-elle réellement utilisée ? »
- Encapsulation et fiabilité. L’état interne devient accessible et modifiable dans le code livré. Un appel intempestif à
FTO_reset()ou un setter qui fuit peut corrompre l’état en production ; la surface supplémentaire alourdit aussi le bundle et élargit la surface d’attaque. - Fausse confiance. Les tests qui sondent l’état privé vérifient l’implémentation, pas le comportement. Ils peuvent rester verts alors que le contrat public est cassé, et ils cassent lors de refactorings inoffensifs — des tests fragiles qui testent la mauvaise chose.
- Maintenabilité. Les membres réservés aux tests ressemblent à du code mort mais ne peuvent pas être supprimés ; chaque changement doit tenir compte d’appelants fantômes, et l’odeur tend à se multiplier à mesure que d’autres tests réutilisent la porte dérobée.
##Treatment
Considérez la porte dérobée comme un signal de conception, et non comme un détail de fixture.
- Testez d’abord via le comportement observable. Vérifiez les valeurs de retour, les événements émis, la sortie persistée ou les interactions avec les collaborateurs (via des doublures de test) plutôt que d’aller fouiller dans les détails internes. La plupart des accesseurs
getXForTestdisparaissent une fois que vous vérifiez ce que l’objet fait, et non ce qu’il contient. - Utilisez une sous-classe spécifique au test lorsque vous avez réellement besoin d’un accès interne. Étendez la classe dans le test pour exposer un membre
protected, au lieu d’élargir la visibilité en production. - Faites des joints une partie de la conception réelle, et non des trappes réservées aux tests. Injecter une horloge ou un RNG via le constructeur normal est une injection de dépendances légitime ; un mutateur
setClockForTest()est une odeur. Si un joint n’a de sens que pour les tests, poussez le comportement dans une Stratégie/un objet Null que la production installe par défaut et que le test remplace. - Si l’exposition est vraiment inévitable, signalez-la haut et fort et clôturez-la. Marquez-la
@VisibleForTesting/@internal(ou avec une convention de nommageFTO_) et imposez que le code de production ne l’appelle jamais (voir les détecteurs). Un joint marqué et protégé vaut mieux qu’un joint silencieux. - Traquez les contrevenants existants. Faites un
grepsurforTest/ForTesting/FTO_, et exécutez un outil d’usage/de code mort tel que Knip en mode production pour faire apparaître les exports référencés uniquement par des fichiers de test.
// AVANT — la production porte un accesseur réservé aux tests
export class Cart {
#items: Item[] = [];
add(i: Item) { this.#items.push(i); }
getItemsForTest() { return this.#items; } // réservé aux testeurs
}
// test
expect(cart.getItemsForTest()).toHaveLength(1);
// APRÈS — vérifier le comportement via le contrat réel
export class Cart {
#items: Item[] = [];
add(i: Item) { this.#items.push(i); }
get count() { return this.#items.length; }
get total() { return this.#items.reduce((s, i) => s + i.price, 0); }
}
// test
cart.add({ price: 10 });
expect(cart.count).toBe(1);
expect(cart.total).toBe(10);
Si vous avez encore besoin d’un accès interne, restreignez le joint aux tests avec une sous-classe plutôt qu’au monde entier :
// la production reste propre : queue est protected, pas public
export class Scheduler {
protected queue: Job[] = [];
enqueue(j: Job) { this.queue.push(j); }
}
// fichier de test uniquement
class TestScheduler extends Scheduler {
peek() { return this.queue; }
}
##Detected by
- sonar java:S5803 — Les membres « @VisibleForTesting » ne devraient pas être accédés depuis le code de production