Aujourd'hui, j'ai discuté avec mon coéquipier des tests unitaires. Tout a commencé quand il m'a demandé "hé, où sont les tests pour cette classe, je n'en vois qu'un?". Toute la classe était un manager (ou un service si vous préférez l'appeler comme ça) et presque toutes les méthodes déléguaient simplement des choses à un DAO donc c'était similaire à:
SomeClass getSomething(parameters) {
return myDao.findSomethingBySomething(parameters);
}
Une sorte de passe-partout sans logique (ou du moins je ne considère pas une délégation aussi simple que logique) mais un passe-partout utile dans la plupart des cas (séparation des couches, etc.). Et nous avons eu une discussion assez longue sur la question de savoir si je devrais le tester unitaire (je pense qu'il vaut la peine de mentionner que j'ai fait un test unitaire complet du DAO). Ses principaux arguments étant qu'il ne s'agissait pas de TDD (évidemment) et que quelqu'un pourrait vouloir voir le test pour vérifier ce que fait cette méthode (je ne sais pas comment cela pourrait être plus évident) ou qu'à l'avenir quelqu'un pourrait vouloir changer le implémentation et ajouter une nouvelle logique (ou plus comme "any") (auquel cas je suppose que quelqu'un devrait simplement tester cela logique).
Cela m'a cependant fait réfléchir. Faut-il viser le pourcentage de couverture de test le plus élevé? Ou est-ce simplement un art pour l'art alors? Je ne vois tout simplement aucune raison de tester des choses comme:
Évidemment, un test pour une telle méthode (avec des simulations) me prendrait moins d'une minute, mais je suppose que c'est encore du temps perdu et une milliseconde de plus pour chaque CI.
Y a-t-il des raisons rationnelles/non "inflammables" pour lesquelles on devrait tester chaque ligne de code (ou autant qu'il le peut)?
Je me fie à la règle empirique de Kent Beck:
Bien sûr, c'est subjectif dans une certaine mesure. Pour moi, les getters/setters triviaux et les one-liners comme le vôtre ci-dessus ne valent généralement pas la peine. Mais là encore, je passe la plupart de mon temps à écrire des tests unitaires pour le code hérité, ne rêvant que d'un projet TDD Nice greenfield ... Sur de tels projets, les règles sont différentes. Avec le code hérité, l'objectif principal est de couvrir autant de terrain avec le moins d'effort possible, de sorte que les tests unitaires ont tendance à être de niveau supérieur et plus complexes, plus comme les tests d'intégration si l'on est pédant sur la terminologie. Et lorsque vous avez du mal à obtenir une couverture de code globale de 0%, ou que vous parvenez simplement à la dépasser de 25%, le test unitaire des getters et setters est le moindre de vos soucis.
OTOH dans un projet greenfield TDD, il peut être plus pratique d'écrire des tests même pour de telles méthodes. D'autant plus que vous avez déjà écrit le test avant d'avoir la chance de commencer à vous demander "cette ligne vaut-elle un test dédié?". Et au moins ces tests sont triviaux à écrire et rapides à exécuter, donc ce n'est pas un gros problème de toute façon.
Il existe quelques types de tests unitaires:
Si vous deviez d'abord écrire votre test, cela aurait plus de sens - comme vous vous attendez à appeler une couche d'accès aux données. Le test échouerait initialement. Vous écririez alors du code de production pour réussir le test.
Idéalement, vous devriez tester le code logique, mais les interactions (objets appelant d'autres objets) sont tout aussi importantes. Dans votre cas, je
Actuellement, il n'y a pas de logique, mais ce ne sera pas toujours le cas.
Cependant, si vous êtes sûr qu'il n'y aura pas de logique dans cette méthode et qu'elle restera probablement la même, je considérerais d'appeler la couche d'accès aux données directement depuis le consommateur. Je ne ferais cela que si le reste de l'équipe est sur la même page. Vous ne voulez pas envoyer un mauvais message à l'équipe en disant "Hé les gars, c'est bien d'ignorer la couche de domaine, appelez simplement la couche d'accès aux données directement".
Je me concentrerais également sur le test d'autres composants s'il y avait un test d'intégration pour cette méthode. Je n'ai pas encore vu une entreprise avec des tests d'intégration solides.
Cela dit, je ne testerais pas tout aveuglément. J'établirais les points chauds (composants à haute complexité et à haut risque de rupture). Je me concentrerais alors sur ces composants. Il est inutile d'avoir une base de code où 90% de la base de code est assez simple et couverte par des tests unitaires, alors que les 10% restants représentent la logique de base du système et ils ne sont pas couverts par des tests unitaires en raison de leur complexité.
Enfin, quel est l'avantage de tester cette méthode? Quelles sont les implications si cela ne fonctionne pas? Sont-ils catastrophiques? Ne vous efforcez pas d'obtenir une couverture de code élevée. La couverture du code devrait être un sous-produit d'une bonne suite de tests unitaires. Par exemple, vous pouvez écrire un test qui parcourra l'arbre et vous donnera une couverture à 100% de cette méthode, ou vous pouvez écrire trois tests unitaires qui vous donneront également une couverture à 100%. La différence est qu'en écrivant trois tests, vous testez des cas Edge, au lieu de simplement parcourir l'arbre.
Voici une bonne façon de penser à la qualité de votre logiciel:
Pour les fonctions standard et triviales, vous pouvez compter sur la vérification de type pour faire son travail, et pour le reste, vous avez besoin de cas de test.
À mon avis, la complexité cyclomatique est un paramètre. Si une méthode n'est pas assez complexe (comme les getters et setters). Aucun test unitaire n'est nécessaire. Le niveau de complexité cyclomatique de McCabe doit être supérieur à 1. Un autre mot doit contenir au moins 1 bloc.
Bien controversé, mais je dirais que quiconque répond "non" à cette question manque un concept fondamental de TDD.
Pour moi, la réponse est un retentissant oui si vous suivez TDD. Si vous ne l'êtes pas, alors non est une réponse plausible.
TDD est souvent cité comme ayant les principaux avantages.
En tant que programmeurs, il est terriblement tentant de considérer les attributs comme quelque chose de significatif et les getters et setter comme une sorte de surcharge.
Mais les attributs sont un détail d'implémentation, tandis que les setters et les getters sont l'interface contractuelle qui fait réellement fonctionner les programmes.
Il est beaucoup plus important d'épeler qu'un objet doit:
Permettre à ses clients de changer son état
et
Autoriser ses clients à interroger son état
puis comment cet état est réellement stocké (pour lequel un attribut est le plus courant, mais pas le seul).
Un test tel que
(The Painter class) should store the provided colour
est important pour la partie documentation de TDD.
Le fait que l'implémentation éventuelle soit triviale (attribut) et ne comporte aucun avantage défense ne devrait pas vous être connu lorsque vous écrivez le test.
L'un des principaux problèmes dans le monde du développement de systèmes est le manque d'ingénierie aller-retour 1 - le processus de développement d'un système est fragmenté en sous-processus disjoints dont les artefacts (documentation, code) sont souvent incohérents.
1Brodie, Michael L. "John Mylopoulos: graines de couture de la modélisation conceptuelle." Modélisation conceptuelle: fondements et applications. Springer Berlin Heidelberg, 2009. 1-9.
C'est la partie documentation de TDD qui garantit que les spécifications du système et son code sont toujours cohérents.
Dans TDD, nous écrivons d'abord le test d'acceptation échoué, puis écrivons le code qui les a laissés passer.
Dans le BDD de niveau supérieur, nous écrivons d'abord des scénarios, puis les faisons passer.
Pourquoi devriez-vous exclure les setters et les getter?
En théorie, il est parfaitement possible au sein de TDD pour une personne d'écrire le test, et une autre pour implémenter le code qui le fait passer.
Alors demandez-vous:
La personne qui écrit les tests d'une classe doit-elle mentionner les getters et setter?.
Les getters et setters étant une interface publique pour une classe, la réponse est évidemment yes, ou il n'y aura aucun moyen de définir ou d'interroger l'état d'un objet.
Évidemment, si vous écrivez d'abord le code, la réponse peut ne pas être aussi claire.
Il y a des exceptions évidentes à cette règle - des fonctions qui sont des détails de mise en œuvre clairs et qui ne font clairement pas partie de la conception du système.
Par exemple, une méthode locale 'B ()':
function A() {
// B() will be called here
function B() {
...
}
}
Ou la fonction privée square()
ici:
class Something {
private:
square() {...}
public:
addAndSquare() {...}
substractAndSquare() {...}
}
Ou toute autre fonction qui ne fait pas partie d'une interface public
qui a besoin d'orthographe dans la conception du composant système.
Face à une question philosophique, revenez aux exigences de conduite.
Votre objectif est-il de produire des logiciels raisonnablement fiables à un coût compétitif?
Ou est-ce pour produire des logiciels de la plus haute fiabilité possible, peu importe le coût?
Jusqu'à un certain point, les deux objectifs de qualité et de vitesse/coût de développement s'alignent: vous passez moins de temps à rédiger des tests qu'à corriger des défauts.
Mais au-delà de ce point, ils ne le font pas. Il n'est pas si difficile d'accéder à, disons, un bogue signalé par développeur et par mois. Réduire ce nombre à un par deux mois ne libère qu'un budget d'un jour ou deux, et des tests supplémentaires ne diminueront probablement pas de moitié votre taux de défauts. Ce n'est donc plus un simple gagnant/gagnant; vous devez le justifier sur la base du coût du défaut pour le client.
Ce coût variera (et, si vous voulez être mauvais, leur capacité à vous faire appliquer ces coûts, que ce soit par le biais du marché ou d'une action en justice). Vous ne voulez pas être mauvais, alors vous comptez ces coûts en entier; parfois, certains tests continuent de rendre le monde plus pauvre de par leur existence.
En bref, si vous essayez d'appliquer aveuglément les mêmes normes à un site Web interne que le logiciel de vol d'avion de ligne, vous vous retrouverez soit en faillite, soit en prison.
C'est une question délicate.
À strictement parler, je dirais que ce n'est pas nécessaire. Il vaut mieux écrire des tests de niveau d'unité et de système de style BDD qui garantissent que les exigences commerciales fonctionnent comme prévu dans les scénarios positifs et négatifs.
Cela dit, si votre méthode n'est pas couverte par ces cas de test, vous devez vous demander pourquoi elle existe en premier lieu et si elle est nécessaire, ou s'il existe des exigences cachées dans le code qui ne sont pas reflétées dans votre documentation ou dans les user stories qui doit être codé dans un scénario de test de style BDD.
Personnellement, j'aime garder la couverture par ligne à environ 85-95% et les check-ins de porte à la ligne principale pour assurer que la couverture de test unitaire existante par ligne atteint ce niveau pour tous les fichiers de code et qu'aucun fichier n'est découvert.
En supposant que les meilleures pratiques de test sont suivies, cela donne beaucoup de couverture sans forcer les développeurs à perdre du temps à essayer de comprendre comment obtenir une couverture supplémentaire sur du code difficile à exercer ou un code trivial simplement pour le plaisir de la couverture.
Votre réponse à cela dépend de votre philosophie (croyez-vous que c'est Chicago vs Londres? Je suis sûr que quelqu'un va le chercher). Le jury est toujours à ce sujet sur l'approche la plus efficace en termes de temps (car, après tout, c'est le plus grand facteur de réduction du temps passé sur les correctifs).
Certaines approches disent tester uniquement l'interface publique, d'autres disent tester l'ordre de chaque appel de fonction dans chaque fonction. Beaucoup de guerres saintes ont été menées. Mon conseil est d'essayer les deux approches. Choisissez une unité de code et faites-la comme X et une autre comme Y. Après quelques mois de test et d'intégration, revenez en arrière et voyez celle qui correspond le mieux à vos besoins.