Duplication de constantes entre tests et code de production?

20

Est-il bon ou mauvais de dupliquer les données entre les tests et le vrai code? Par exemple, supposons que j'ai une classe Python FooSaverqui enregistre des fichiers avec des noms particuliers dans un répertoire donné:

class FooSaver(object):
  def __init__(self, out_dir):
    self.out_dir = out_dir

  def _save_foo_named(self, type_, name):
    to_save = None
    if type_ == FOOTYPE_A:
      to_save = make_footype_a()
    elif type == FOOTYPE_B:
      to_save = make_footype_b()
    # etc, repeated
    with open(self.out_dir + name, "w") as f:
      f.write(str(to_save))

  def save_type_a(self):
    self._save_foo_named(a, "a.foo_file")

  def save_type_b(self):
    self._save_foo_named(b, "b.foo_file")

Maintenant, dans mon test, je voudrais m'assurer que tous ces fichiers ont été créés, donc je veux dire quelque chose comme ceci:

foo = FooSaver("/tmp/special_name")
foo.save_type_a()
foo.save_type_b()

self.assertTrue(os.path.isfile("/tmp/special_name/a.foo_file"))
self.assertTrue(os.path.isfile("/tmp/special_name/b.foo_file"))

Bien que cela duplique les noms de fichiers à deux endroits, je pense que c'est bien: cela m'oblige à écrire exactement ce que j'attends à l'autre bout, il ajoute une couche de protection contre les fautes de frappe et me donne généralement confiance que les choses fonctionnent exactement comme je l'attends. Je sais que si je passe a.foo_fileà type_a.foo_filel'avenir, je vais devoir faire des recherches et des remplacements dans mes tests, mais je ne pense pas que ce soit trop important. Je préfère avoir des faux positifs si j'oublie de mettre à jour le test en échange de m'assurer que ma compréhension du code et des tests est synchronisée.

Un collègue pense que cette duplication est mauvaise et a recommandé que je refactorise les deux côtés à quelque chose comme ceci:

class FooSaver(object):
  A_FILENAME = "a.foo_file"
  B_FILENAME = "b.foo_file"

  # as before...

  def save_type_a(self):
    self._save_foo_named(a, self.A_FILENAME)

  def save_type_b(self):
    self._save_foo_named(b, self.B_FILENAME)

et dans le test:

self.assertTrue(os.path.isfile("/tmp/special_name/" + FooSaver.A_FILENAME))
self.assertTrue(os.path.isfile("/tmp/special_name/" + FooSaver.B_FILENAME))

Je n'aime pas cela parce que cela ne me donne pas confiance que le code fait ce que j'attendais --- Je viens de dupliquer l' out_dir + nameétape à la fois du côté de la production et du côté du test. Il ne découvrira pas une erreur dans ma compréhension du +fonctionnement des chaînes et n'attrapera pas les fautes de frappe.

D'un autre côté, c'est clairement moins fragile que d'écrire deux fois ces chaînes, et il me semble un peu mal de dupliquer des données sur deux fichiers comme ça.

Y a-t-il un précédent clair ici? Est-il correct de dupliquer des constantes entre les tests et le code de production, ou est-ce trop fragile?

Patrick Collins
la source

Réponses:

16

Je pense que cela dépend de ce que vous essayez de tester, qui va à ce qu'est le contrat de la classe.

Si le contrat de la classe est exactement celui qui FooSavergénère a.foo_fileet b.foo_fileà un emplacement particulier, vous devez tester cela directement, c'est-à-dire dupliquer les constantes dans les tests.

Si, cependant, le contrat de la classe est qu'elle génère deux fichiers dans une zone temporaire, les noms de chacun pouvant être facilement modifiés, en particulier au moment de l'exécution, alors vous devez tester de manière plus générique, en utilisant probablement des constantes factorisées hors des tests.

Vous devriez donc discuter avec votre collègue de la véritable nature et du contrat de la classe dans une perspective de conception de domaine de niveau supérieur. Si vous n'êtes pas d'accord, je dirais que c'est une question de compréhension et de niveau d'abstraction de la classe elle-même, plutôt que de la tester.

Il est également raisonnable de constater que le contrat de la classe change pendant la refactorisation, par exemple, pour que son niveau d'abstraction soit augmenté au fil du temps. Au début, il s'agit des deux fichiers spécifiques dans un emplacement temporaire particulier, mais au fil du temps, vous pouvez trouver qu'une abstraction supplémentaire est justifiée. À ce moment, modifiez les tests pour les synchroniser avec le contrat de la classe. Il n'est pas nécessaire de trop construire le contrat de la classe tout de suite juste parce que vous le testez (YAGNI).

Lorsque le contrat d'une classe n'est pas bien défini, le tester peut nous amener à remettre en question la nature de la classe, mais il en va de même pour son utilisation. Je dirais que vous ne devriez pas mettre à niveau le contrat de la classe juste parce que vous le testez; vous devez mettre à niveau le contrat de la classe pour d'autres raisons, telles que, c'est une abstraction faible pour le domaine, et sinon, testez-le tel quel.

Erik Eidt
la source
4

Ce que @Erik a suggéré - pour vous assurer que vous êtes clair sur ce que vous testez - devrait certainement être votre premier point de réflexion.

Mais si votre décision vous conduit à prendre en compte les constantes, cela laisse la partie intéressante de votre question (paraphraser) "Pourquoi devrais-je troquer les constantes de duplication pour du code de duplication?". (En référence à l'endroit où vous parlez de "dupliquer [ing] l'étape out_dir + name".)

Je crois que (modulo les commentaires de Erik) la plupart des situations font bénéficier de la suppression des constantes en double. Mais vous devez le faire de manière à ne pas dupliquer le code. Dans votre exemple particulier, c'est facile. Au lieu de traiter un chemin comme des chaînes "brutes" dans votre code de production, traitez un chemin comme un chemin. C'est un moyen plus robuste de joindre des composants de chemin que la concaténation de chaînes:

os.path.join(self.out_dir, name)

Dans votre code de test, en revanche, je recommanderais quelque chose comme ça. Ici, l'accent est mis sur le fait que vous avez un chemin d'accès et que vous "branchez" un nom de fichier feuille:

self.assertTrue(os.path.isfile("/tmp/special_name/{0}".format(FooSaver.A_FILENAME)))

Autrement dit, en sélectionnant plus soigneusement les éléments de langage, vous pouvez automatiquement éviter la duplication de code. (Pas tout le temps, mais très souvent d'après mon expérience.)

Michael Sorens
la source
1

Je suis d'accord avec la réponse d'Erik Eidt, mais il y a une troisième option: masquer la constante dans le test, donc le test réussit même si vous modifiez la valeur de la constante dans le code de production.

(voir stubbing a constant in python unittest )

foo = FooSaver("/tmp/special_name")
foo.save_type_a()
foo.save_type_b()

with mock.patch.object(FooSaver, 'A_FILENAME', 'unique_to_your_test_a'):
  self.assertTrue(os.path.isfile("/tmp/special_name/unique_to_your_test_a"))
with mock.patch.object(FooSaver, 'B_FILENAME', 'unique_to_your_test_b'):
  self.assertTrue(os.path.isfile("/tmp/special_name/unique_to_your_test_b"))

Et lorsque je fais des choses comme ça, je m'assure généralement de faire un test de santé mentale où withj'exécute les tests sans l' instruction et je m'assure que je vois "'a.foo_file'! = 'Unique_to_your_test_a'" ", puis je remets l' withinstruction dans le test donc ça passe encore.

alexanderbird
la source