Conception de tests unitaires pour un système avec état

20

Contexte

Le développement piloté par les tests a été popularisé après que j'ai déjà fini l'école et dans l'industrie. J'essaie de l'apprendre, mais certaines choses importantes m'échappent encore. Les partisans de TDD disent beaucoup de choses comme (ci-après dénommé "principe d'assertion unique" ou SAP ):

Depuis quelque temps, je réfléchis à la façon dont les tests TDD peuvent être aussi simples, aussi expressifs et aussi élégants que possible. Cet article explore un peu ce que c'est que de rendre les tests aussi simples et décomposés que possible: viser une seule assertion dans chaque test.

Source: http://www.artima.com/weblogs/viewpost.jsp?thread=35578

Ils disent aussi des choses comme ça (ci-après dénommé le "principe de la méthode privée" ou PMP ):

En règle générale, vous ne testez pas directement les méthodes privées. Puisqu'ils sont privés, considérez-les comme un détail d'implémentation. Personne ne va jamais appeler l'un d'eux et s'attendre à ce que cela fonctionne d'une manière particulière.

Vous devriez plutôt tester votre interface publique. Si les méthodes qui appellent vos méthodes privées fonctionnent comme prévu, vous supposez alors par extension que vos méthodes privées fonctionnent correctement.

Source: Comment testez-vous les méthodes privées?

Situation

J'essaie de tester un système de traitement de données avec état. Le système peut faire différentes choses pour le même élément de données, compte tenu de son état antérieur à la réception de ces données. Envisagez un test simple qui crée un état dans le système, puis teste le comportement que la méthode donnée est censée tester.

  • SAP suggère que je ne devrais pas tester la "procédure de création d'état", je devrais supposer que l'état correspond à ce que j'attends du code de génération, puis tester le changement d'état que j'essaie de tester

  • PMP suggère que je ne peux pas ignorer cette étape de "création d'état" et simplement tester les méthodes qui régissent cette fonctionnalité indépendamment.

Le résultat dans mon code actuel a été des tests gonflés, compliqués, longs et difficiles à écrire. Et si les transitions d'état changent, les tests doivent être modifiés ... ce serait bien avec de petits tests efficaces mais extrêmement longs et déroutants avec ces longs tests gonflés. Comment cela se fait-il normalement?

durron597
la source
2
Je ne pense pas que vous trouverez une solution élégante à cela. L'approche générale n'est pas de rendre le système dynamique pour commencer, ce qui ne vous aide pas à tester quelque chose qui est déjà construit. Le refactoriser pour qu'il soit apatride n'en vaut probablement pas la peine non plus.
Doval
@Doval: veuillez expliquer comment rendre quelque chose comme un téléphone (SIP UserAgent) non étatique. Le comportement attendu de cette unité est spécifié dans la RFC à l'aide d'un diagramme de transition d'état.
Bart van Ingen Schenau
Êtes-vous en train de copier / coller / éditer vos tests ou écrivez-vous des méthodes utilitaires pour partager la configuration / démontage / fonctionnalité commune? Alors que certains cas de test peuvent certainement devenir longs et gonflés, cela ne devrait pas être si fréquent. Dans un système avec état, je m'attendrais à une routine de configuration commune où l'état final est un paramètre et cette routine vous amène à l'état que vous souhaitez tester. De plus, à la fin de chaque test, j'aurais une méthode de démontage qui vous ramènerait à l'état de démarrage connu (si cela était nécessaire) afin que votre méthode de configuration fonctionne correctement au début du prochain test.
Dunk
Sur une tangente mais j'ajouterai également que les diagrammes d'état sont un outil de communication et non un décret d'implémentation même s'il est dans un RFC. Tant que vous répondez aux fonctionnalités décrites, vous répondez à la norme. J'ai eu quelques occasions où j'ai converti des implémentations de transition d'état vraiment compliquées (telles que définies dans les RFC) en fonctionnalités de traitement général très simples. Je me souviens d'un cas où je me suis débarrassé de quelques milliers de lignes de code une fois que j'ai réalisé que, à part quelques indicateurs, environ 5 états faisaient exactement la même chose une fois que vous renommiez les éléments communs "cachés".
Dunk

Réponses:

15

La perspective:

Faisons donc un pas en arrière et demandons ce que TDD essaie de nous aider. TDD essaie de nous aider à déterminer si notre code est correct ou non. Et par correct, je veux dire "le code répond-il aux exigences de l'entreprise?" Le point de vente est que nous savons que des changements seront nécessaires à l'avenir, et nous voulons nous assurer que notre code reste correct après avoir effectué ces changements.

J'apporte cette perspective parce que je pense qu'il est facile de se perdre dans les détails et de perdre de vue ce que nous essayons de réaliser.

Principes - SAP:

Bien que je ne sois pas un expert en TDD, je pense que vous manquez une partie de ce que le principe d'assertion unique (SAP) essaie d'enseigner. SAP peut être reformulé comme «tester une chose à la fois». Mais TOTAT ne déroule pas aussi facilement que SAP.

Tester une chose à la fois signifie que vous vous concentrez sur un cas; un chemin; une condition aux limites; un cas d'erreur; un que ce soit par test. Et l'idée directrice derrière cela est que vous devez savoir ce qui s'est cassé lorsque le scénario de test échoue, afin de pouvoir résoudre le problème plus rapidement. Si vous testez plusieurs conditions (c.-à-d. Plus d'une chose) dans un test et que le test échoue, alors vous avez beaucoup plus de travail sur vos mains. Vous devez d'abord identifier lequel des multiples cas a échoué, puis comprendre pourquoi ce cas a échoué.

Si vous testez une chose à la fois, votre champ de recherche est beaucoup plus petit et le défaut est identifié plus rapidement. Gardez à l'esprit que «tester une chose à la fois» ne vous empêche pas nécessairement de regarder plus d'une sortie de processus à la fois. Par exemple, lorsque je teste un "bon chemin connu", je peux m'attendre à voir une valeur résultante spécifique fooainsi qu'une autre valeur dans baret je peux le vérifier foo != bardans le cadre de mon test. La clé est de regrouper logiquement les contrôles de sortie en fonction du cas testé.

Principes - PMP:

De même, je pense que vous manquez un peu ce que le principe de méthode privée (PMP) doit nous enseigner. PMP nous encourage à traiter le système comme une boîte noire. Pour une entrée donnée, vous devriez obtenir une sortie donnée. Peu importe comment la boîte noire génère la sortie. Vous vous souciez seulement que vos sorties s'alignent avec vos entrées.

PMP est vraiment une bonne perspective pour regarder les aspects API de votre code. Il peut également vous aider à définir ce que vous devez tester. Identifiez vos points d'interface et vérifiez qu'ils respectent les termes de leurs contrats. Vous n'avez pas besoin de vous soucier de la façon dont les méthodes derrière l'interface (alias privées) font leur travail. Vous avez juste besoin de vérifier qu'ils ont fait ce qu'ils étaient censés faire.


TDD appliqué ( pour vous )

Votre situation présente donc un peu une ride au-delà d'une application ordinaire. Les méthodes de votre application sont dynamiques, leur sortie dépend donc non seulement de l'entrée, mais aussi de ce qui a été fait précédemment. Je suis sûr que je devrais <insert some lecture>ici que l'état soit horrible et bla bla bla, mais cela n'aide vraiment pas à résoudre votre problème.

Je vais supposer que vous avez une sorte de tableau de diagramme d'état qui montre les différents états potentiels et ce qui doit être fait pour déclencher une transition. Si vous ne le faites pas, vous en aurez besoin car cela aidera à exprimer les exigences commerciales de ce système.

Les tests: Tout d'abord, vous allez vous retrouver avec un ensemble de tests qui décident d'un changement d'état. Idéalement, vous aurez des tests qui exercent toute la gamme des changements d'état qui peuvent se produire, mais je peux voir quelques scénarios où vous n'aurez peut-être pas besoin d'aller aussi loin.

Ensuite, vous devez créer des tests pour valider le traitement des données. Certains de ces tests d'état seront réutilisés lors de la création des tests de traitement des données. Par exemple, supposons que vous ayez une méthode Foo()qui a différentes sorties en fonction des états Initet State1. Vous voudrez utiliser votre ChangeFooToState1test comme une étape de configuration afin de tester la sortie lorsque " Foo()est dedans State1".

Il y a certaines implications derrière cette approche que je veux mentionner. Spoiler, c'est là que j'exaspère les puristes

Tout d'abord, vous devez accepter que vous utilisiez quelque chose comme test dans une situation et une configuration dans une autre situation. D'une part, cela semble être une violation directe de SAP. Mais si vous définissez logiquement ChangeFooToState1deux objectifs, vous respectez toujours l'esprit de ce que SAP nous enseigne. Lorsque vous devez vous assurer que les Foo()états changent, vous l'utilisez ChangeFooToState1comme test. Et lorsque vous avez besoin de valider Foo()la sortie de "lorsque State1vous êtes", vous l'utilisez ChangeFooToState1comme configuration.

Le deuxième élément est que d'un point de vue pratique, vous n'allez pas vouloir de tests unitaires entièrement randomisés pour votre système. Vous devez exécuter tous les tests de changement d'état avant d'exécuter les tests de validation de sortie. SAP est en quelque sorte le principe directeur derrière cette commande. Pour indiquer ce qui devrait être évident - vous ne pouvez pas utiliser quelque chose comme configuration s'il échoue comme test.

Mettre ensemble:

À l'aide de votre diagramme d'état, vous générez des tests pour couvrir les transitions. Encore une fois, en utilisant votre diagramme, vous générez des tests pour couvrir tous les cas de traitement des données d'entrée / sortie pilotés par l'état.

Si vous suivez cette approche, les bloated, complicated, long, and difficult to writetests devraient être un peu plus faciles à gérer. En général, ils devraient finir plus petits et être plus concis (c'est-à-dire moins compliqués). Vous devriez noter que les tests sont également plus découplés ou modulaires.

Maintenant, je ne dis pas que le processus sera complètement indolore car écrire de bons tests demande un certain effort. Et certains d'entre eux seront toujours difficiles car vous mappez un deuxième paramètre (état) sur un certain nombre de vos cas. Et en passant, il devrait être un peu plus clair pourquoi un système sans état est plus facile à construire pour les tests. Mais si vous adaptez cette approche à votre application, vous devriez constater que vous êtes en mesure de prouver que votre application fonctionne correctement.


la source
11

Vous résumeriez généralement les détails de la configuration dans des fonctions afin de ne pas avoir à vous répéter. De cette façon, vous ne devez le modifier à un endroit du test que si la fonctionnalité change.

Cependant, vous ne voudriez normalement pas décrire même vos fonctions de configuration comme gonflées, compliquées ou longues. C'est un signe que votre interface doit être refactorisée, car s'il est difficile à utiliser pour vos tests, il est difficile pour votre vrai code d'utiliser également.

C'est souvent un signe d'en mettre trop dans une classe. Si vous avez des exigences avec état, vous avez besoin d'une classe qui gère l'état et rien d'autre. Les classes qui le prennent en charge doivent être apatrides. Pour votre exemple SIP, l'analyse d'un paquet doit être complètement sans état. Vous pouvez avoir une classe qui analyse un paquet puis appelle quelque chose comme sipStateController.receiveInvite()pour gérer les transitions d'état, qui elle-même appelle d'autres classes sans état pour faire des choses comme sonner le téléphone.

Cela rend la configuration des tests unitaires pour la classe de machine d'état une simple question de quelques appels de méthode. Si votre configuration pour les tests unitaires de machine d'état nécessite la création de paquets, vous en avez trop mis dans cette classe. De même, la classe de votre analyseur de paquets doit être relativement simple à créer pour le code de configuration, en utilisant une maquette pour la classe de machine d'état.

En d'autres termes, vous ne pouvez pas éviter complètement l'état, mais vous pouvez le minimiser et l'isoler.

Karl Bielefeldt
la source
Pour mémoire, l'exemple SIP était le mien, pas celui du PO. Et certaines machines d'état peuvent avoir besoin de plus de quelques appels de méthode pour les mettre dans le bon état pour un certain test.
Bart van Ingen Schenau
+1 pour "vous ne pouvez pas éviter complètement l'état, mais vous pouvez le minimiser et l'isoler." Je n'étais pas d'accord. L'État est un mal nécessaire dans le logiciel.
Brandon
0

L'idée centrale de TDD est qu'en écrivant d'abord des tests, vous vous retrouvez avec un système qui est, au moins, facile à tester. J'espère que cela fonctionne, est maintenable, bien documenté et ainsi de suite, mais sinon, au moins c'est toujours facile à tester.

Donc, si vous TDD et que vous vous retrouvez avec un système difficile à tester, quelque chose a mal tourné. Peut-être que certaines choses qui sont privées devraient être publiques, car vous en avez besoin pour les tests. Peut-être que vous ne travaillez pas au bon niveau d'abstraction; quelque chose d'aussi simple qu'une liste est dynamique à un niveau, mais une valeur à un autre. Ou peut-être donnez-vous trop d'importance aux conseils qui ne s'appliquent pas à votre contexte, ou votre problème est juste difficile. Ou bien sûr, votre design est peut-être tout simplement mauvais.

Quelle que soit la cause, vous n'allez probablement pas revenir en arrière et réécrire votre système pour le rendre plus testable avec un code de test simple. Le meilleur plan est donc probablement d'utiliser des techniques de test légèrement plus sophistiquées, comme:

Soru
la source