Que se passe-t-il avec les tests de méthodes lorsque cette méthode devient privée après une nouvelle conception dans TDD?

29

Disons que je commence à développer un jeu de rôle avec des personnages qui attaquent d'autres personnages et ce genre de choses.

En appliquant TDD, je fais quelques cas de test pour tester la logique à l'intérieur de la Character.receiveAttack(Int)méthode. Quelque chose comme ça:

@Test
fun healthIsReducedWhenCharacterIsAttacked() {
    val c = Character(100) //arg is the health
    c.receiveAttack(50) //arg is the suffered attack damage
    assertThat(c.health, is(50));
}

Disons que j'ai 10 méthodes de test receiveAttack. Maintenant, j'ajoute une méthode Character.attack(Character)(qui appelle receiveAttackméthode), et après quelques cycles TDD la testant, je prends une décision: Character.receiveAttack(Int)devrait l'être private.

Que se passe-t-il avec les 10 cas de test précédents? Dois-je les supprimer? Dois-je garder la méthode public(je ne pense pas)?

Cette question n'est pas de savoir comment tester des méthodes privées, mais comment les gérer après une nouvelle conception lors de l'application de TDD

Hector
la source
2
Copie possible de Test de méthodes privées protégées
gnat
10
Si c'est privé, vous ne le testez pas, c'est aussi simple que cela. Retirez et faites la danse
kayess
6
Je vais probablement à contre-courant ici. Mais, j'évite généralement les méthodes privées à tout prix. Je préfère plus de tests que moins de tests. Je sais ce que les gens pensent "Quoi, donc vous n'avez jamais aucune fonctionnalité que vous ne voulez pas exposer au consommateur?". Oui, j'en ai plein que je ne veux pas exposer. Au lieu de cela, lorsque j'ai une méthode privée, je la refactorise à la place dans sa propre classe et utilise ladite classe de la classe d'origine. La nouvelle classe peut être marquée comme internalou l'équivalent de votre langue pour éviter qu'elle ne soit exposée. En fait, la réponse de Kevin Cline est ce genre d'approche.
user9993
3
@ user9993 vous semblez l'avoir à l'envers. S'il est important que vous ayez plus de tests, la seule façon de vous assurer que vous n'avez rien oublié d'important est d'exécuter une analyse de couverture. Et pour les outils de couverture, peu importe que la méthode soit privée ou publique ou autre. Espérer que rendre des choses publiques compensera en quelque sorte le manque d'analyse de la couverture donne un faux sentiment de sécurité, je le crains
gnat
2
@gnat Mais je n'ai jamais dit "ne pas avoir de couverture"? Mon commentaire sur "Je préfère plus de tests que moins de tests" aurait dû rendre cela évident. Je ne sais pas exactement où vous voulez en venir, bien sûr, je teste également le code que j'ai extrait. Exactement.
user9993

Réponses:

52

Dans TDD, les tests servent de documentation exécutable de votre conception. Votre conception a changé, alors évidemment, votre documentation doit aussi!

Notez que, dans TDD, la seule façon dont la attackméthode aurait pu apparaître, est le résultat de l'échec d'un test. Ce qui signifie, attackest testé par un autre test. Ce qui signifie qu'indirectement receiveAttack est couvert par attackles tests de. Idéalement, toute modification apportée à receiveAttackdevrait casser au moins l'un des attacktests de.

Et si ce n'est pas le cas, alors il y a des fonctionnalités receiveAttackqui ne sont plus nécessaires et ne devraient plus exister!

Donc, receiveAttackétant déjà testé attack, peu importe que vous continuiez ou non vos tests. Si votre infrastructure de test facilite le test de méthodes privées et si vous décidez de tester des méthodes privées, vous pouvez les conserver. Mais vous pouvez également les supprimer sans perdre la couverture et la confiance des tests.

Jörg W Mittag
la source
14
Ceci est une bonne réponse, sauf pour "Si votre framework de test facilite le test des méthodes privées, et si vous décidez de tester les méthodes privées, vous pouvez les conserver." Les méthodes privées sont des détails d'implémentation et ne devraient jamais, jamais être testées directement.
David Arno
20
@DavidArno: Je ne suis pas d'accord, par ce raisonnement les internes d'un module ne devraient jamais être testés. Cependant, les internes d'un module peuvent être d'une grande complexité et donc avoir des tests unitaires pour chaque fonctionnalité interne individuelle peut être précieux. Les tests unitaires sont utilisés pour vérifier les invariants d'un élément de fonctionnalité, si une méthode privée a des invariants (pré-conditions / post-conditions), alors un test unitaire peut être utile.
Matthieu M.
8
" Par ce raisonnement, les composants internes d'un module ne devraient jamais être testés ". Ces éléments internes ne devraient jamais être testés directement . Tous les tests ne doivent tester que les API publiques. Si un élément interne est inaccessible via une API publique, supprimez-le car il ne fait rien.
David Arno
28
@DavidArno Selon cette logique, si vous construisez un exécutable (plutôt qu'une bibliothèque), vous ne devriez avoir aucun test unitaire. - "Les appels de fonction ne font pas partie de l'API publique! Seuls les arguments de ligne de commande le sont! Si une fonction interne de votre programme est inaccessible via un argument de ligne de commande, supprimez-la car elle ne fait rien." - Bien que les fonctions privées ne fassent pas partie de l'API publique de la classe, elles font partie de l'API interne de la classe. Et bien que vous n'ayez pas nécessairement besoin de tester l'API interne d'une classe, vous pouvez, en utilisant la même logique pour tester l'API interne d'un exécutable.
RM
7
@RM, si je devais créer un exécutable de façon non modulaire, alors je serais obligé de choisir entre des tests fragiles des internes, ou uniquement en utilisant des tests d'intégration utilisant l'exécutable et les E / S d'exécution. Par conséquent, par ma logique actuelle, plutôt que par votre version paille, je le créerais de manière modulaire (par exemple via un ensemble de bibliothèques). Les API publiques de ces modules peuvent ensuite être testées de manière non fragile.
David Arno
23

Si la méthode est suffisamment complexe pour nécessiter des tests, elle doit être publique dans une classe. Vous refactorisez donc:

public class X {
  private int complexity(...) {
    ...
  }
  public void somethingElse() {
    int c = complexity(...);
  }
}

à:

public class Complexity {
  public int calculate(...) {
    ...
  }
}

public class X {
  private Complexity complexity;
  public X(Complexity complexity) { // dependency injection happiness
    this.complexity = complexity;
  }

  public void something() {
    int c = complexity.calculate(...);
  }
}

Déplacez le test actuel de X.complexity vers ComplexityTest. Ensuite, envoyez X.quelque chose en se moquant de Complexité.

D'après mon expérience, la refactorisation vers des classes plus petites et des méthodes plus courtes rapporte d'énormes avantages. Ils sont plus faciles à comprendre, à tester et finissent par être réutilisés plus que ce à quoi on pourrait s'attendre.

Kevin Cline
la source
Votre réponse explique beaucoup plus clairement l'idée que j'essayais d'expliquer dans mon commentaire sur la question d'OP. Bonne réponse.
user9993
3
Merci pour votre réponse. En fait, la méthode receiveAttack est assez simple ( this.health = this.health - attackDamage). Peut-être que l'extraire dans une autre classe est une solution sur-conçue, pour le moment.
Héctor
1
C'est définitivement trop exagéré pour l'OP - il veut conduire au magasin, pas voler vers la lune - mais une bonne solution dans le cas général.
Si la fonction est aussi simple, c'est peut-être trop d'ingénierie qu'elle est même définie comme une fonction en premier lieu.
David K
1
il est peut-être exagéré aujourd'hui, mais dans 6 mois, quand il y aura une tonne de changements à ce code, les avantages seront clairs. Et dans n'importe quel IDE décent ces jours-ci, l'extraction de code dans une classe distincte ne devrait être que quelques touches au maximum, une solution sur-conçue, étant donné que dans le binaire d'exécution, tout se résumera de la même façon.
Stephen Byrne
6

Disons que j'ai 10 méthodes pour tester la méthode receiveAttack. Maintenant, j'ajoute une méthode Character.attack (Character) (qui appelle la méthode receiveAttack), et après quelques cycles TDD la testant, je prends une décision: Character.receiveAttack (Int) devrait être privé.

Il faut garder à l'esprit que la décision que vous prenez est de supprimer une méthode de l'API . La courtoisie de la rétrocompatibilité suggérerait

  1. Si vous n'avez pas besoin de le supprimer, laissez-le dans l'API
  2. Si vous n'avez pas besoin de l' enlever encore , alors marquer comme dépréciée et si le document possible lorsque la fin de vie se produira
  3. Si vous devez le supprimer, vous avez un changement de version majeur

Les tests sont supprimés / ou remplacés lorsque votre API ne prend plus en charge la méthode. À ce stade, la méthode privée est un détail d'implémentation que vous devriez pouvoir refactoriser.

À ce stade, vous revenez à la question standard de savoir si votre suite de tests doit accéder directement aux implémentations, plutôt interagir uniquement via l'API publique. Une méthode privée est quelque chose que nous devrions pouvoir remplacer sans que la suite de tests ne gêne . Je m'attendrais donc à ce que le couple de tests disparaisse - soit retiré, soit déplacé avec l'implémentation vers un composant testable séparément.

VoiceOfUnreason
la source
3
La dépréciation n'est pas toujours une préoccupation. De la question: "Disons que je commence à développer ..." si le logiciel n'est pas encore sorti, la dépréciation n'est pas un problème. De plus: "un jeu de rôle" implique qu'il ne s'agit pas d' une bibliothèque réutilisable, mais d'un logiciel binaire destiné aux utilisateurs finaux. Alors que certains logiciels pour utilisateurs finaux ont une API publique (par exemple MS Office), la plupart n'en ont pas. Même le logiciel qui fait une API publique a seulement une partie de celui - ci exposée pour les plugins, les scripts (par exemple les jeux avec l' extension LUA), ou d' autres fonctions. Néanmoins, il vaut la peine d'évoquer l'idée du cas général décrit par le PO.