Devrions-nous tester toutes nos méthodes?


62

Donc, aujourd’hui, j’ai parlé avec mon coéquipier au sujet des tests unitaires. Tout a commencé quand il m'a demandé "hé, où sont les tests pour ce cours, je n'en vois qu'un?". Toute la classe était un manager (ou un service si vous préférez l'appeler ainsi) et presque toutes les méthodes consistaient simplement à déléguer des tâches à un DAO. Cela ressemblait donc à:

SomeClass getSomething(parameters) {
    return myDao.findSomethingBySomething(parameters);
}

Une sorte de passe-partout sans logique (ou du moins, je ne considère pas cette simple délégation comme une logique), mais un passe-passe utile dans la plupart des cas (séparation des couches, etc.). Et nous avons eu une assez longue discussion sur la question de savoir si je devais ou non effectuer un test unitaire (je pense qu’il est utile de mentionner que j’ai fait le test unitaire complet du DAO). Ses arguments principaux sont qu’il ne s’agissait pas de TDD (à l’évidence) et que quelqu'un voudrait peut-être 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 voudra peut-être modifier la méthode. implémentation et y ajouter une nouvelle logique (ou plus semblable à "n’importe laquelle") (auquel cas je suppose que quelqu'un devrait simplement tester cette logique ).

Cela m'a fait penser, cependant. Devrions-nous nous efforcer d’obtenir le taux 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:

  • les getters et les setters (à moins qu'ils aient une logique en eux)
  • code "passe-partout"

Évidemment, un test pour une telle méthode (avec des simulacres) me prendrait moins d'une minute, mais j'imagine que c'est toujours du temps perdu et une milliseconde de plus pour chaque IC.

Y a-t-il des raisons rationnelles / non "inflammables" pour lesquelles on devrait tester chaque ligne de code (ou autant qu'il peut)?


2
Je suis toujours en train de me décider sur cette question, mais voici un discours de quelqu'un qui a décidé que la réponse était "non". Ian Cooper: TDD, où tout cela a-t-il mal tourné? Pour résumer cette discussion intéressante, vous devriez tester dehors et dedans et tester de nouveaux comportements et non de nouvelles méthodes.
Daniel Kaplan

C'est vraiment une excellente conversation, à voir absolument, une révélation pour beaucoup de gens, j'adore. Mais je pense que la réponse n’est pas "non". C'est "oui, mais indirectement". Ian Cooper parle d’architecture hexagonale et de fonctionnalités / comportements de test moquant / sabotant les ports. Dans ce cas, ces ports sont les DAO et ce "gestionnaire / service" est testé non pas avec un test unitaire individuel uniquement pour cette classe, mais avec un "test unitaire" (unité dans la définition de Ian Cooper avec laquelle je suis tout à fait d'accord) qui teste certaines fonctionnalités dans votre domaine qui utilise ce gestionnaire / service.
AlfredoCasado


Cela dépendra dans une certaine mesure de votre système. Si vous développez un système avec un niveau de certification de sécurité allant de modéré à élevé, vous devrez couvrir toutes les méthodes indépendamment de la trivialité
jk.

Réponses:


49

Je me fie à la règle de base de Kent Beck:

Testez tout ce qui pourrait éventuellement casser.

Bien sûr, c'est subjectif dans une certaine mesure. Pour moi, les getters / setters et les one-liners triviaux comme le vôtre ne valent généralement rien. Mais là encore, je passe le plus clair de mon temps à écrire des tests unitaires pour le code hérité, ne rêvant que d’un beau projet TDD dans un environnement vierge ... Sur de tels projets, les règles sont différentes. Avec le code existant, l’objectif principal est de couvrir le plus de terrain possible avec le moins d’effort possible. Les tests unitaires ont donc tendance à être de niveau plus élevé et plus complexes, plus comme des tests d’intégration si on est pédant sur la terminologie. Et lorsque vous avez du mal à obtenir une couverture globale du code supérieure à 0%, ou que vous réussissez à la dépasser de plus de 25%, le test de vos unités est le dernier de vos soucis.

OTOH dans un nouveau projet 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à passé le test avant d'avoir la chance de commencer à vous demander "est-ce que cette ligne mérite un test dédié?". Et au moins, ces tests sont faciles à écrire et rapides à exécuter. Ce n'est donc pas grave.


Ah j'ai totalement oublié cette citation! J'imagine que je vais l'utiliser comme principal argument parce que franchement, qu'est-ce qui peut casser ici? Pas vraiment beaucoup. La seule chose qui peut casser est l'invocation de la méthode et si cela se produit, cela signifie que quelque chose de vraiment grave s'est passé. Merci!
Zenzen

5
@Zenzen: "Qu'est-ce qui peut casser ici? Pas vraiment beaucoup." - Alors ça peut casser. Juste une petite faute de frappe. Ou quelqu'un ajoute du code. Ou gâche la dépendance. Je pense vraiment que Beck prétendrait que votre principal exemple peut être considéré comme cassable. Getters et setters, moins, bien que je me sois pris au piège dans une erreur de copier / coller, même dans ce cas. La vraie question est, si c'est trop trivial pour écrire un test, pourquoi existe-t-il même?
pdr

1
Le temps que vous avez passé à y penser vous aurait déjà pu passer le test. Je dis écrire le test, ne laissez pas quand ne pas écrire un test comme une zone grise, plus de fenêtres brisées apparaîtront.
kett_chup

1
J'ajouterai que selon mon expérience générale, les tests sur les getters et les setters sont plutôt utiles à long terme, mais peu prioritaires. La raison en est qu’il n’a aucune chance de trouver un bogue maintenant, vous ne pouvez donc pas garantir qu’un autre développeur n’ajoute rien dans trois mois ("une simple déclaration if") qui aurait une chance de rompre. . Avoir une unité de test en place protège contre cela. En même temps, la priorité n’est pas vraiment élevée, car vous n’allez rien trouver de cette façon.
Déclits le

7
Tuer aveuglément tout ce qui pourrait casser n'a pas de sens. Il doit exister une stratégie selon laquelle les composants à haut risque sont d'abord testés.
CodeART

13

Il existe peu de types de tests unitaires:

  • État basé. Vous agissez puis affirmez contre l'état de l'objet. Par exemple, je fais un dépôt. Je vérifie ensuite si l'équilibre a augmenté.
  • Valeur de retour basée. Vous agissez et affirmez contre la valeur de retour.
  • Basé sur l'interaction. Vous vérifiez que votre objet a appelé un autre objet. Cela semble être ce que vous faites dans votre exemple.

Si vous deviez écrire votre test en premier, cela aurait plus de sens - comme vous pouvez vous attendre à appeler une couche d'accès aux données. Le test échouerait initialement. Vous écririez ensuite le 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 également importantes. Dans votre cas, je voudrais

  • Vérifiez que j'ai appelé la couche d'accès aux données avec le paramètre exact qui a été transmis.
  • Vérifiez qu'il n'a été appelé qu'une fois.
  • Vérifiez que je retourne exactement ce qui m'a été donné par la couche d'accès aux données. Sinon, je pourrais aussi bien renvoyer null.

Actuellement, il n'y a pas de logique là-bas, 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 inchangée, je considérerais d'appeler la couche d'accès aux données directement à partir du consommateur. Je ne ferais cela que si le reste de l'équipe est sur la même page. Vous ne voulez pas envoyer un message erroné à l'équipe en lui disant "Hé les gars, il est bon 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 cependant.

Cela dit, je ne testerais pas aveuglément tout. J'établirais les points chauds (composants très complexes et risquant de se casser). Je me concentrerais ensuite sur ces composants. Il ne sert à rien d'avoir une base de code où 90% de la base de code est assez simple et couverte par les tests unitaires, les 10% restants représentant la logique centrale du système et ne le sont pas en raison de leur complexité.

Enfin, quel est l'intérêt de tester cette méthode? Quelles sont les implications si cela ne fonctionne pas? Sont-ils catastrophiques? N'essayez pas d'obtenir une couverture de code élevée. La couverture de code devrait être un sous-produit d'une bonne suite de tests unitaires. Par exemple, vous pouvez écrire un test qui parcourt l’arbre et vous donner une couverture de 100% de cette méthode, ou vous pouvez écrire trois tests unitaires qui vous donneront également une couverture de 100%. La différence est qu'en écrivant trois tests, vous testez des cas extrêmes, au lieu de simplement parcourir l'arborescence.


Pourquoi voudriez-vous vérifier que votre DAL n’a été appelé qu’une fois?
Marjan Venema

9

Voici un bon moyen de réfléchir à la qualité de votre logiciel:

  1. la vérification de type est une partie du problème.
  2. les tests vont gérer le reste

Pour les fonctions standard et triviales, vous pouvez vous fier à la vérification de type, et pour le reste, vous avez besoin de scénarios de test.


Bien sûr, la vérification des types ne fonctionne que si vous utilisez des types spécifiques dans votre code et si vous travaillez avec un langage compilé ou si vous vous assurez par ailleurs qu'une vérification d'analyse statique est exécutée fréquemment, par exemple dans le cadre de CI.
bdsl

6

À mon avis, la complexité cyclomatique est un paramètre. Si une méthode n'est pas assez complexe (comme les accesseurs et les setters). Aucun test unitaire n'est nécessaire. Le niveau de complexité cyclomatique de McCabe devrait être supérieur à 1. Un autre mot devrait comporter au moins 1 énoncé bloc.


N'oubliez pas que certains geters ou setters ont des effets secondaires (bien que cela soit découragé et considéré comme une mauvaise pratique dans la plupart des cas), une modification de votre code source peut également l'affecter.
Andrzej Bobak

3

Un retentissant OUI avec TDD (et à quelques exceptions près)

Bien controversé, mais je dirais que quiconque répond «non» à cette question manque un concept fondamental du TDD.

Pour moi, la réponse est un oui retentissant si vous suivez TDD. Si vous ne l'êtes pas, non est une réponse plausible.

La DDD en TDD

TDD est souvent cité comme ayant les principaux avantages.

  • La défense
    • S'assurer que le code peut changer mais pas son comportement .
    • Cela permet la pratique toujours aussi importante de la refactorisation .
    • Vous gagnez ce TDD ou non.
  • Conception
    • Vous spécifiez ce que doit faire quelque chose, comment il doit se comporter avant de le mettre en œuvre .
    • Cela signifie souvent des décisions de mise en œuvre plus éclairées .
  • Documentation
    • La suite de tests doit servir de documentation de spécification (exigences).
    • L'utilisation de tests à cette fin signifie que la documentation et la mise en œuvre sont toujours dans un état cohérent - un changement de l'un signifie un changement de l'autre. Comparez avec les exigences de conservation et la conception sur un document Word séparé.

Séparer la responsabilité de la mise en œuvre

En tant que programmeurs, il est terriblement tentant de considérer les attributs comme quelque chose d’important et d’attirer et de définir 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 permet aux programmes de fonctionner.

Il est bien plus important d’épeler qu’un objet doit:

Autoriser ses clients à changer d'état

et

Autoriser ses clients à interroger son état

puis comment cet état est réellement stocké (pour lequel un attribut est le plus commun, mais pas le seul moyen).

Un test tel que

(The Painter class) should store the provided colour

est important pour la partie documentation de TDD.

Le fait que la mise en œuvre éventuelle soit triviale (attribut) et ne comporte aucun avantage en termes de défense devrait vous être inconnu lorsque vous écrivez le test.

Le manque d'ingénierie aller-retour ...

L'un des problèmes majeurs du 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.

1 Brodie, Michael L. "John Mylopoulos: coudre des graines de modélisation conceptuelle." Modélisation conceptuelle: fondements et applications. Springer Berlin Heidelberg, 2009. 1-9.

... et comment TDD le résout

C’est la partie documentation de TDD qui garantit la cohérence des spécifications du système et de son code.

Concevoir d'abord, mettre en œuvre plus tard

Dans TDD, nous écrivons d’abord le test d’acceptation ayant échoué, puis nous écrivons le code qui les laisse passer.

Au sein du BDD de niveau supérieur, nous écrivons d’abord des scénarios, puis nous les faisons passer.

Pourquoi devriez-vous exclure les setters et les getter?

En théorie, au sein de TDD, il est parfaitement possible à une personne d’écrire le test et à une autre d’implémenter le code qui le fait passer.

Alors demandez-vous:

La personne qui écrit les tests pour une classe doit-elle mentionner les accesseurs et les passeurs.

Comme les getters et les setters sont une interface publique avec une classe, la réponse est évidemment oui , sinon il n'y aura aucun moyen de définir ou d'interroger l'état d'un objet.

De toute évidence, si vous écrivez le code en premier, la réponse risque de ne pas être aussi claire.

Exceptions

Il existe des exceptions évidentes à cette règle - des fonctions qui sont détaillées dans la mise en œuvre et qui ne font manifestement pas partie de la conception du système.

Par exemple, a la 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 ne faisant pas partie d'une publicinterface nécessitant une orthographe dans la conception du composant système.


1

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 un logiciel avec la plus grande fiabilité possible, quel que soit 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 à écrire des tests qu'à réparer des défauts.

Mais au-delà, ils ne le font pas. Il n’est pas si difficile d’obtenir par exemple un bug signalé par développeur et par mois. Réduire de moitié ce nombre en un mois sur deux ne libère qu'un budget d'environ un jour ou deux, et de nombreux tests supplémentaires ne réduiront probablement pas votre taux de défauts. Donc, ce n'est plus un simple gagnant / gagnant; vous devez le justifier en fonction du coût du défaut pour le client.

Ce coût variera (et, si vous voulez être pervers, leur capacité à vous faire supporter ces coûts, que ce soit par le biais du marché ou par le biais d'une action en justice). Vous ne voulez pas être méchant, alors vous comptez ces coûts dans leur intégralité; parfois, certains tests continuent globalement à rendre le monde plus pauvre 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 pour avion de ligne, vous vous retrouverez soit en faillite, soit en prison.


0

Votre réponse à ce sujet dépend de votre philosophie (croyez-vous que ce sera Chicago vs Londres? Je suis sûr que quelqu'un le vérifiera). Le jury n’a toujours pas choisi l’approche la plus efficace en termes de temps (car, après tout, c’est le facteur le plus déterminant de la réduction du temps passé sur les solutions).

Certaines approches ne testent que l'interface publique, d'autres testent 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 pour voir laquelle correspond le mieux à vos besoins.


0

C'est une question délicate.

Strictement parlant, je dirais que ce n'est pas nécessaire. Il est préférable d’écrire des tests au niveau système et au niveau système BDD pour s’assurer que les exigences de l’entreprise 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 et si elle est nécessaire, ou si le code contient des exigences cachées qui ne sont pas reflétées dans votre documentation ou dans les user stories. devrait être codé dans un scénario de test de style BDD.

Personnellement, j'aime bien garder la couverture par ligne à environ 85-95% et laisser les enregistrements à la ligne principale pour s'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 soient suivies, cela donne beaucoup de couverture sans obliger les développeurs à perdre du temps à essayer de trouver comment obtenir une couverture supplémentaire sur du code difficile à exercer ou un code trivial simplement pour le plaisir de la couverture.


-1

Le problème est la question elle-même, vous n'avez pas besoin de tester tous les "methdos" ou toutes les "classes" dont vous avez besoin pour tester toutes les fonctionnalités de vos systèmes.

Sa pensée clé en termes de caractéristiques / comportements au lieu de penser en termes de méthodes et de classes. Bien sûr, une méthode est là pour fournir un support pour une ou plusieurs fonctionnalités, à la fin tout votre code est testé, au moins tout le code est important dans votre base de code.

Dans votre scénario, probablement cette classe "manager" est redondante ou inutile (comme toutes les classes dont le nom contient le mot "manager"), ou peut-être pas, mais semble être un détail d'implémentation, probablement cette classe ne mérite pas une unité test car cette classe n'a pas de logique métier pertinente. Vous avez probablement besoin de cette classe pour que certaines fonctionnalités fonctionnent, le test de cette fonctionnalité couvre cette classe. Vous pouvez ainsi refactoriser cette classe et faire en sorte que ce qui compte, vos fonctionnalités, fonctionne toujours après le refactor.

Pensez dans les fonctionnalités / comportements non dans les classes de méthodes, je ne peux pas le répéter suffisamment de fois.


-4

Cela m'a fait penser, cependant. Devrions-nous nous efforcer d’obtenir le taux de couverture de test le plus élevé?

Oui, idéalement à 100%, mais certaines choses ne sont pas testables à l’unité.

les getters et les setters (à moins qu'ils aient une logique en eux)

Les Getters / Setters sont stupides - mais ne les utilisez pas. Au lieu de cela, mettez votre variable de membre à la section publique.

code "passe-partout"

Extrayez le code commun et testez-le à l'unité. Cela devrait être aussi simple que cela.

Y a-t-il des raisons rationnelles / non "inflammables" pour lesquelles on devrait tester chaque ligne de code (ou autant qu'il peut)?

En ne le faisant pas, vous risquez de rater des bugs très évidents. Les tests unitaires sont comme un filet sûr pour attraper certains types de bugs, et vous devriez les utiliser autant que possible.

Et la dernière chose: je suis sur un projet où les gens ne voulaient pas perdre leur temps à écrire des tests unitaires pour du "code simple", mais ils ont ensuite décidé de ne pas écrire du tout. À la fin, certaines parties du code se sont transformées en une grosse boule de boue .


Disons-le clairement: je ne voulais pas dire que je n’utilisais pas les tests TDD / d’écriture. Plutôt l'inverse. Je sais que les tests peuvent trouver un bug auquel je n'ai pas pensé, mais qu'y a-t-il à tester ici? Je pense simplement que cette méthode est l’une des méthodes "non testables en unité". Comme l'a dit Péter Török (citant Kent Beck), vous devriez tester des choses qui peuvent casser. Que pourrait éventuellement casser ici? Pas vraiment beaucoup (il n'y a qu'une simple délégation dans cette méthode). Je peux écrire un test unitaire mais il aura simplement une maquette du DAO et une assertion, pas beaucoup de tests. En ce qui concerne les getters / setters, certains cadres l’exigent.
Zenzen

1
De plus, comme je ne l’avais pas remarqué, «récupérez le code commun et testez-le à l’unité. Cela devrait être aussi simple que cela. Que veux-tu dire par là? C'est une classe de service (dans une couche de service entre l'interface graphique et le DAO), elle est commune à l'ensemble de l'application. Je ne peux pas vraiment le rendre plus générique (car il accepte certains paramètres et appelle une certaine méthode dans le DAO). La seule raison pour laquelle il existe est de respecter l'architecture en couches de l'application afin que l'interface graphique n'appelle pas directement le DAO.
Zenzen

20
-1 pour "Les Getters / Setters sont stupides - mais ne les utilisez pas. Mettez plutôt votre variable de membre dans une section publique." - Très mal. Cela a été discuté à plusieurs reprises sur SO . Utiliser des champs publics partout est en réalité pire que d'utiliser des accesseurs et des setters partout.
Péter Török le
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.