web-dev-qa-db-fra.com

Aux prises avec des dépendances cycliques dans les tests unitaires

J'essaie de pratiquer le TDD, en l'utilisant pour développer un simple comme Bit Vector. Il se trouve que j'utilise Swift, mais c'est une question indépendante de la langue.

Mon BitVector est un struct qui stocke un seul UInt64, Et présente une API dessus qui vous permet de le traiter comme une collection. Les détails importent peu, mais c'est assez simple. Les 57 bits supérieurs sont des bits de stockage et les 6 bits inférieurs sont des bits de "comptage", qui vous indiquent combien de bits de stockage stockent réellement une valeur contenue.

Jusqu'à présent, j'ai une poignée de capacités très simples:

  1. Un initialiseur qui construit des vecteurs de bits vides
  2. Une propriété count de type Int
  3. Une propriété isEmpty de type Bool
  4. Un opérateur d'égalité (==). NB: il s'agit d'un opérateur d'égalité de valeurs semblable à Object.equals() en Java, pas d'un opérateur d'égalité de référence comme == En Java.

Je rencontre un tas de dépendances cycliques:

  1. Le test unitaire qui teste mon initialiseur doit vérifier que le BitVector nouvellement construit. Il peut le faire de trois manières:

    1. Cochez bv.count == 0
    2. Cochez bv.isEmpty == true
    3. Vérifiez que bv == knownEmptyBitVector

    La méthode 1 s'appuie sur count, la méthode 2 s'appuie sur isEmpty (qui elle-même s'appuie sur count, il est donc inutile de l'utiliser), la méthode 3 s'appuie sur ==. En tout cas, je ne peux pas tester mon initialiseur isolément.

  2. Le test de count doit fonctionner sur quelque chose, ce qui teste inévitablement mes initialiseurs

  3. L'implémentation de isEmpty repose sur count

  4. L'implémentation de == Repose sur count.

J'ai pu résoudre en partie ce problème en introduisant une API privée qui construit un BitVector à partir d'un modèle de bits existant (comme un UInt64). Cela m'a permis d'initialiser les valeurs sans tester les autres initialiseurs, afin de pouvoir "démarrer la sangle" en montant.

Pour que mes tests unitaires soient vraiment des tests unitaires, je me retrouve à faire un tas de hacks, ce qui complique considérablement ma prod et mon code de test.

Comment contournez-vous exactement ce genre de problèmes?

Vous vous inquiétez trop des détails de mise en œuvre.

Peu importe que dans votre implémentation actuelle, isEmpty s'appuie sur count (ou toute autre relation que vous pourriez avoir): tout ce dont vous devriez vous soucier est le interface publique. Par exemple, vous pouvez avoir trois tests:

  • Qu'un objet nouvellement initialisé a count == 0.
  • Qu'un objet nouvellement initialisé a isEmpty == true
  • Qu'un objet nouvellement initialisé est égal à l'objet vide connu.

Ce sont tous des tests valides et deviennent particulièrement importants si vous décidez de refactoriser les composants internes de votre classe afin que isEmpty ait une implémentation différente qui ne repose pas sur count - tant que votre les tests passent toujours, vous savez que vous n’avez rien régressé.

Des choses similaires s'appliquent à vos autres points - n'oubliez pas de tester l'interface publique, pas votre implémentation interne. Vous pouvez trouver TDD utile ici, car vous écririez alors les tests dont vous avez besoin pour isEmpty avant d'avoir écrit une implémentation pour cela.

66
Philip Kendall

Comment contournez-vous exactement ce genre de problèmes?

Vous révisez votre réflexion sur ce qu'est un "test unitaire".

Un objet qui gère des données mutables en mémoire est fondamentalement une machine à états. Ainsi, tout cas d'utilisation utile va, au minimum, invoquer une méthode pour placer des informations dans l'objet, et invoquer une méthode pour lire une copie de informations hors de l'objet. Dans les cas d'utilisation intéressants, vous allez également invoquer des méthodes supplémentaires qui modifient la structure des données.

En pratique, cela ressemble souvent à

// GIVEN
obj = new Object(...)

// THEN
assert object.read(...)

ou

// GIVEN
obj = new Object(...)

// WHEN
object.change(...)

// THEN
assert object.read(...)

La terminologie du "test unitaire" - eh bien, elle n'est pas très bonne depuis longtemps.

Je les appelle des tests unitaires, mais ils ne correspondent pas très bien à la définition acceptée des tests unitaires - Kent Beck, Test Driven Development by Example

Kent a écrit la première version de SUnit en 1994 , le portage vers JUnit date de 1998, la première version du livre TDD était début 2002. La confusion avait beaucoup de temps à se propager.

L'idée clé de ces tests (plus précisément appelés "tests programmeurs" ou "tests développeurs") est que les tests sont isolés les uns des autres. Les tests ne partagent aucune structure de données mutable, ils peuvent donc être exécutés simultanément. Il n'y a aucun souci que les tests doivent être exécutés dans un ordre spécifique pour mesurer correctement la solution.

Le principal cas d'utilisation de ces tests est qu'ils sont exécutés par le programmeur entre les modifications de son propre code source. Si vous effectuez le protocole de refactorisation rouge vert, un ROUGE inattendu indique toujours une erreur dans votre dernière modification; vous annulez cette modification, vérifiez que les tests sont VERTS et réessayez. Il n'y a pas beaucoup d'avantages à essayer d'investir dans une conception où chaque bogue possible est détecté par un seul test.

Bien sûr, une fusion introduit une faute, puis trouver que la faute n'est plus anodine. Vous pouvez suivre différentes étapes pour vous assurer que les défauts sont faciles à localiser. Voir

5
VoiceOfUnreason

En général (même si vous n'utilisez pas TDD), vous devez vous efforcer d'écrire des tests autant que possible tout en prétendant que vous ne savez pas comment il est mis en œuvre.

Si vous faites du TDD, cela devrait déjà être le cas. Vos tests sont une spécification exécutable du programme.

L'aspect du graphe d'appel sous les tests n'a pas d'importance, tant que les tests eux-mêmes sont raisonnables et bien entretenus.

Je pense que votre problème est votre compréhension du TDD.

À mon avis, votre problème est que vous "mélangez" vos personnages TDD. Vos personnages "test", "code" et "refactor" fonctionnent de manière totalement indépendante les uns des autres, idéalement. En particulier, vos personnes de codage et de refactoring n'ont aucune obligation envers les tests autres que de les faire/maintenir vertes.

Bien sûr, en principe, il serait préférable que tous les tests soient orthogonaux et indépendants les uns des autres. Mais ce n'est pas une préoccupation de vos deux autres personnages TDD, et ce n'est certainement pas une exigence stricte stricte ou même nécessairement réaliste de vos tests. Fondamentalement: ne jetez pas vos sentiments de bon sens sur la qualité du code pour essayer de répondre à une exigence que personne ne vous demande.

1
Tim Seguine