web-dev-qa-db-fra.com

Est-ce une mauvaise pratique de modifier le code strictement à des fins de test

J'ai un débat avec un collègue programmeur pour savoir si c'est une bonne ou une mauvaise pratique de modifier un morceau de code de travail uniquement pour le rendre testable (via des tests unitaires par exemple).

Mon avis est que c'est OK, dans les limites du maintien de bonnes pratiques orientées objet et génie logiciel bien sûr (pas de "tout rendre public", etc.).

L'opinion de mon collègue est que la modification du code (qui fonctionne) uniquement à des fins de test est incorrecte.

Juste un exemple simple, pensez à ce morceau de code utilisé par certains composants (écrit en C #):

public void DoSomethingOnAllTypes()
{
    var types = Assembly.GetExecutingAssembly().GetTypes();

    foreach (var currentType in types)
    {
        // do something with this type (e.g: read it's attributes, process, etc).
    }
}

J'ai suggéré que ce code puisse être modifié pour appeler une autre méthode qui fera le travail réel:

public void DoSomething(Assembly asm)
{
    // not relying on Assembly.GetExecutingAssembly() anymore...
}

Cette méthode utilise un objet Assembly sur lequel travailler, ce qui permet de passer votre propre Assembly pour effectuer le test. Mon collègue ne pensait pas que c'était une bonne pratique.

Qu'est-ce qui est considéré comme une bonne pratique courante?

79
liortal

Modifier le code pour le rendre plus testable présente des avantages au-delà de la testabilité. En général, un code plus testable

  • Est plus facile à entretenir,
  • Est plus facile à raisonner,
  • Est plus faiblement couplé, et
  • A une meilleure conception globale, architecturalement.
136
Robert Harvey

Il y a (apparemment) des forces opposées en jeu.

  • D'une part, vous souhaitez appliquer l'encapsulation
  • D'autre part, vous souhaitez pouvoir tester le logiciel

Les partisans de la confidentialité de tous les "détails de mise en œuvre" sont généralement motivés par le désir de maintenir l'encapsulation. Cependant, tout garder verrouillé et indisponible est une approche mal comprise de l'encapsulation. Si garder tout indisponible était le but ultime, le seul vrai code encapsulé serait le suivant:

static void Main(string[] args)

Votre collègue propose-t-il d'en faire le seul point d'accès dans votre code? Tous les autres codes devraient-ils être inaccessibles aux appelants externes?

À peine. Alors qu'est-ce qui permet de rendre publiques certaines méthodes? N'est-ce pas finalement une décision de conception subjective?

Pas assez. Ce qui tend à guider les programmeurs, même à un niveau inconscient, est, encore une fois, le concept d'encapsulation. Vous vous sentez en sécurité d'exposer une méthode publique lorsqu'elle protège correctement ses invariants .

Je ne voudrais pas exposer une méthode privée qui ne protège pas ses invariants, mais souvent vous pouvez la modifier pour qu'elle le fasse protéger ses invariants, et alors l'exposer au public (bien sûr, avec TDD, vous le faites dans l'autre sens).

Ouvrir une API pour la testabilité est une bonne chose , parce que ce que vous c'est vraiment appliquer le principe ouvert/fermé .

Si vous n'avez qu'un seul appelant de votre API, vous ne savez pas à quel point votre API est vraiment flexible. Les chances sont, c'est assez rigide. Les tests agissent comme un deuxième client, vous donnant de précieux commentaires sur la flexibilité de votre API .

Ainsi, si les tests suggèrent que vous devez ouvrir votre API, faites-le; mais maintenez l'encapsulation, non pas en masquant la complexité, mais en exposant la complexité de manière sûre.

59
Mark Seemann

On dirait que vous parlez injection de dépendance . C'est vraiment commun, et IMO, assez nécessaire pour la testabilité.

Pour répondre à la question plus large de savoir si c'est une bonne idée de modifier le code juste pour le rendre testable, pensez-y de cette façon: le code a de multiples responsabilités, y compris a) pour être exécuté, b) pour être lu par les humains, et c) pour être testé. Les trois sont importants et si votre code ne remplit pas les trois responsabilités, je dirais que ce n'est pas un très bon code. Alors modifiez loin!

21
Jason Swett

C'est un peu un problème de poulet et d'oeufs.

L'une des principales raisons pour lesquelles il est bon d'avoir une bonne couverture de test de votre code est qu'il vous permet de refactoriser sans crainte. Mais vous êtes dans une situation où vous devez refactoriser le code afin d'obtenir une bonne couverture de test! Et votre collègue a peur.

Je vois le point de vue de votre collègue. Vous avez du code qui (vraisemblablement) fonctionne, et si vous allez le refactoriser - pour une raison quelconque - il y a un risque que vous le cassiez.

Mais si c'est du code qui devrait faire l'objet d'une maintenance et d'une modification continues, vous allez courir ce risque chaque fois que vous y travaillez. Et refactoriser maintenant et obtenir une couverture de test maintenant vous permettra de prendre ce risque, dans des conditions contrôlées, et de mettre le code en meilleure forme pour une modification future.

Je dirais donc, à moins que cette base de code particulière ne soit assez statique et que l'on ne s'attende pas à ce que des travaux importants soient effectués à l'avenir, que ce que vous voulez faire soit de bonnes pratiques techniques.

Bien sûr, que ce soit bon affaires la pratique est une toute autre boîte de vers.

13
Carson63000

Cela peut être juste une différence d'accentuation par rapport aux autres réponses, mais je dirais que le code ne doit pas être refactorisé strictement pour améliorer la testabilité. La testabilité est très importante pour la maintenance, mais la testabilité n'est pas une fin en soi. En tant que tel, je différerais une telle refactorisation jusqu'à ce que vous puissiez prédire que ce code aura besoin de maintenance pour poursuivre la fin de l'activité.

Au moment où vous déterminez que ce code nécessitera une certaine maintenance, que serait un bon moment pour refactoriser la testabilité. Selon votre analyse de rentabilisation, il peut être une hypothèse valide que tout le code nécessitera éventuellement une certaine maintenance, auquel cas la distinction que je fais avec les autres réponses ici (par exemple. réponse de Jason Swett ) disparaît.

Pour résumer: la testabilité seule n'est pas (IMO) une raison suffisante pour refactoriser une base de code. La testabilité a un rôle précieux pour permettre la maintenance sur une base de code, mais c'est une exigence commerciale de modifier la fonction de votre code qui devrait conduire votre refactoring. S'il n'y a pas une telle exigence commerciale, il serait probablement préférable de travailler sur quelque chose qui intéressera vos clients.

(Le nouveau code, bien sûr, est activement géré, il doit donc être écrit pour être testable.)

7
Aidan Cully

Votre problème ici est que vos outils de test sont de la merde. Vous devriez pouvoir simuler cet objet et appeler votre méthode de test sans le changer - car bien que cet exemple simple soit vraiment simple et facile à modifier, que se passe-t-il lorsque vous avez quelque chose de beaucoup plus compliqué.

Beaucoup de gens ont modifié leur code pour introduire IoC, DI et les classes basées sur l'interface simplement pour permettre les tests unitaires à l'aide des outils de test de simulation et de test qui nécessitent ces changements de code. Je ne pense pas qu'ils sont une chose saine, pas quand vous voyez du code qui était assez simple et se transforme simplement en un cauchemar d'interactions complexes entièrement motivées par la nécessité de faire en sorte que chaque méthode de classe soit totalement découplée de tout le reste . Et pour ajouter l'insulte à l'injure, nous avons alors de nombreux arguments pour savoir si les méthodes privées doivent être testées à l'unité ou non! (bien sûr, ils devraient le faire, quel est l'intérêt des tests unitaires si vous ne testez qu'une partie de votre système) mais ces arguments sont plus motivés du point de vue qu'il est difficile de tester ces méthodes à l'aide des outils existants - imaginez si votre outil de test pourrait exécuter des tests contre une méthode privée aussi facilement qu'une méthode publique - tout le monde les testerait sans se plaindre.

Le problème, bien sûr, est dans la nature de l'outillage de test.

Il existe maintenant de meilleurs outils qui pourraient mettre ces modifications de conception au lit pour toujours. Microsoft a Fakes (nee Moles) qui vous permet de bloquer des objets concrets, y compris des objets statiques, de sorte que vous n'avez plus besoin de modifier votre code pour l'adapter à l'outil. Dans votre cas, si vous avez utilisé Fakes, vous remplaceriez l'appel GetTypes par le vôtre qui a renvoyé des données de test valides et non valides - ce qui est assez important, votre modification suggérée ne le prévoit pas du tout.

Pour répondre: votre collègue a raison, mais peut-être pour les mauvaises raisons. Ne changez pas le code à tester, changez votre outil de test (ou toute votre stratégie de test pour avoir plus de tests unitaires de style d'intégration au lieu de ces tests à grain fin).

Martin Fowler a une discussion à ce sujet dans son article Les simulacres ne sont pas des talons

2
gbjbaanb

J'ai utilisé des outils de couverture de code dans le cadre de tests unitaires pour vérifier si tous les chemins à travers le code sont exercés. En tant que bon codeur/testeur, je couvre habituellement 80 à 90% des chemins de code.

Quand j'étudie les chemins découverts et fais un effort pour certains d'entre eux, c'est là que je découvre des bugs tels que des cas d'erreur qui "n'arriveront jamais". Donc, oui, la modification du code et la vérification de la couverture des tests améliorent le code.

2
Sam Gamoran

Je pense que votre collègue a tort.

D'autres ont déjà mentionné les raisons pour lesquelles c'est une bonne chose, mais tant que l'on vous donne le feu vert pour le faire, ça devrait aller.

La raison de cette mise en garde est que toute modification du code se fait au prix d'un nouveau test du code. Selon ce que vous faites, ce travail de test peut être en soi un gros effort.

Ce n'est pas nécessairement à vous de prendre la décision de refactoriser ou de travailler sur de nouvelles fonctionnalités qui bénéficieront à votre entreprise/client.

2
ozz

Il existe de sérieuses différences entre vos exemples. Dans le cas de DoSomethingOnAllTypes(), il y a une implication que do something Est applicable aux types dans l'assembly actuel. Mais DoSomething(Assembly asm) indique explicitement que vous pouvez lui passer any Assembly.

La raison pour laquelle je le signale est que beaucoup d'injection de dépendance à des fins de test uniquement dépasse les limites de l'objet d'origine. Je sais que vous avez dit " ne pas 'rendre tout public' ", mais c'est l'une des plus grosses erreurs de ce modèle, suivie de près par celle-ci: ouvrir le méthodes d'objet jusqu'aux utilisations auxquelles elles ne sont pas destinées.

1
Ross Patterson

Une bonne pratique courante consiste à utiliser tests unitaires et journaux de débogage. Les tests unitaires garantissent que si vous apportez d'autres modifications au programme, votre ancienne fonctionnalité ne se casse pas. Les journaux de débogage peuvent vous aider à tracer le programme au moment de l'exécution.
Il arrive parfois que même au-delà de cela, nous ayons besoin de quelque chose uniquement à des fins de test. Il n'est pas rare de changer le code pour cela. Mais il faut veiller à ce que le code de production ne soit pas affecté à cause de cela. En C++ et C, ceci est réalisé en utilisant la macro , qui est l'entité de temps de compilation. Ensuite, le code de test n'apparaît pas du tout dans l'environnement de production. Je ne sais pas si une telle disposition existe en C #.
De plus, lorsque vous ajoutez du code de test dans votre programme, il devrait être clairement visible que cette partie du code soit ajoutée à des fins de test. Ou bien le développeur essayant de comprendre le code va simplement transpirer sur cette partie du code.

1
Manoj R

Votre question n'a pas donné beaucoup de contexte dans lequel votre collègue a fait valoir, il y a donc place à la spéculation

"mauvaise pratique" ou non dépend de comment et quand les modifications sont apportées.

À mon avis, votre exemple pour extraire une méthode DoSomething(types) est correct.

Mais j'ai vu du code qui est pas ok comme ceci:

public void DoSomethingOnAllTypes()
{
  var types = (!testmode) 
      ? Assembly.GetExecutingAssembly().GetTypes() 
      : getTypesFromTestgenerator();

  foreach (var currentType in types)
  {
     if (!testmode)
     {
        // do something with this type that made the unittest fail and should be skipped.
     }
     // do something with this type (e.g: read it's attributes, process, etc).
  }
}

Ces modifications ont rendu le code plus difficile à comprendre car vous avez augmenté le nombre de chemins de code possibles.

Ce que je veux dire avec comment et quand:

si vous avez une implémentation qui fonctionne et pour "implémenter des capacités de test" vous avez fait les changements alors vous devez tester à nouveau votre application car vous avez peut-être cassé votre méthode DoSomething().

La if (!testmode) est plus difficile à comprendre et à tester que la méthode extraite.

0
k3b