Dans quelle mesure les tests TDD doivent-ils être granulaires?

18

Pendant la formation TDD basée sur le cas d'un logiciel médical, nous mettons en œuvre l'histoire suivante: "Lorsque l'utilisateur appuie sur le bouton Enregistrer, le système doit ajouter un patient, ajouter un appareil et ajouter des enregistrements de données d'appareil".

L'implémentation finale ressemblera à ceci:

if (_importDialog.Show() == ImportDialogResult.SaveButtonIsPressed)
{
   AddPatient();
   AddDevice();
   AddDeviceDataRecords();
}

Nous avons deux façons de le mettre en œuvre:

  1. Trois tests où chacun vérifie une méthode (AddPatient, AddDevice, AddDeviceDataRecords) a été appelé
  2. Un test qui vérifie que les trois méthodes ont été appelées

Dans le premier cas, si quelque chose ne va pas dans la condition de la clause if, les trois tests échoueront. Mais dans le second cas, si le test échoue, nous ne savons pas exactement ce qui ne va pas. Quelle manière préférez-vous.

SiberianGuy
la source

Réponses:

8

Mais dans le second cas, si le test échoue, nous ne savons pas exactement ce qui ne va pas.

Je pense que cela dépendrait en grande partie de la qualité des messages d'erreur produits par le test. En général, il existe différentes façons de vérifier qu'une méthode a été appelée; Par exemple, si vous utilisez un objet factice, il vous donnera un message d'erreur précis décrivant la méthode attendue qui n'a pas été appelée pendant le test. Si vous vérifiez que la méthode a été appelée en détectant les effets de l'appel, c'est à vous de produire un message d'erreur descriptif.

En pratique, le choix entre les options 1 et 2 dépend également de la situation. Si je vois le code que vous montrez ci-dessus dans un projet hérité, je choisis l'approche pragmatique du cas # 2 juste pour vérifier que chacune des 3 méthodes est appelée correctement lorsque la condition est remplie. Si je développe ce morceau de code en ce moment, les 3 appels de méthode seraient très probablement ajoutés un par un, à des moments différents (peut-être des jours ou des mois les uns des autres), donc j'ajouterais un nouveau test unitaire séparé pour vérifier chaque appel.

Notez également que dans les deux cas, vous devriez également avoir des tests unitaires séparés pour vérifier que chacune des méthodes individuelles fait ce qu'elle est censée faire.

Péter Török
la source
Ne jugerez-vous pas raisonnable de combiner éventuellement ces trois tests en un seul?
SiberianGuy
@Idsa, peut être une décision raisonnable, bien que dans la pratique, je ne me préoccupe que rarement de ce type de refactoring. Là encore, je travaille avec du code hérité, où les priorités sont différentes: nous nous concentrons sur l'augmentation de la couverture des tests du code existant et le maintien de la quantité croissante de tests unitaires maintenable.
Péter Török
30

La granularité dans votre exemple semble être la différence entre les tests unitaires et les tests d'acceptation.

Un test unitaire teste une seule unité de fonctionnalité, avec le moins de dépendances possible. Dans votre cas, il pourrait y avoir 4 unités

  • AddPatient ajoute-t-il un patient (c'est-à-dire qu'il appelle les fonctions de base de données pertinentes)?
  • AddDevice ajoute-t-il un appareil?
  • AddDeviceDataRecords ajoute-t-il les enregistrements?
  • la fonction principale non modifiée de votre exemple appelle-t-elle AddPatient, AddDevice et AddDeviceFunctions

Les tests sont destinés aux développeurs , afin qu'ils aient l'assurance que leur code est techniquement correct

Les tests d'acceptation doivent tester la fonctionnalité combinée du point de vue de l'utilisateur. Ils doivent être modélisés le long des user stories, et être aussi élevés que possible. Ainsi, vous n'avez pas à vérifier si des fonctions sont appelées, mais si un avantage visible pour l'utilisateur est obtenu:

lorsque l'utilisateur entre les données, clique sur ok et ...

  • ... va à la liste des patients, il devrait voir un nouveau patient avec le prénom
  • ... va à la liste des appareils, il devrait voir un nouvel appareil
  • ... va aux détails du nouvel appareil, il devrait voir de nouveaux enregistrements de données

les tests d'acceptation sont pour les clients , ou pour construire une meilleure communication avec eux.

Pour répondre à votre question "que préférez-vous": quel est le plus gros problème pour vous en ce moment, bugs et régression (=> plus de non-tests) ou compréhension et formalisation de la situation dans son ensemble (=> plus de tests d'acceptation)

keppla
la source
13

Nous avons deux façons de le mettre en œuvre:

C'est faux.

Trois tests où chacun vérifie une méthode (AddPatient, AddDevice, AddDeviceDataRecords) a été appelé

Vous devez le faire pour être sûr que cela fonctionne.

Un test qui vérifie que les trois méthodes ont été appelées

Vous devez également le faire pour être sûr que l'API fonctionne.

La classe - en tant qu'unité - doit être entièrement testée. Chaque méthode.

Vous pouvez commencer par un test qui couvre les trois méthodes, mais il ne vous dit pas grand-chose.

si le test échoue, nous ne savons pas exactement ce qui ne va pas.

Correct. C'est pourquoi vous testez toutes les méthodes.

Vous devez tester l'interface publique. Étant donné que cette classe fait trois choses plus une (même si elles sont regroupées dans une seule méthode en raison de la user story), vous devez tester les quatre choses. Trois bas niveau et un bundle.

S.Lott
la source
2

Nous écrivons nos tests unitaires pour des phrases significatives de fonctionnalités qui souvent correspondent à une méthode (si vous avez bien écrit votre code), mais parfois deviennent plus grandes, englobant de nombreuses méthodes.

Par exemple, imaginez que l'ajout d'un patient à votre système nécessite que certains sous-programmes (fonctions enfants) soient appelés:

  1. VerifyPatientQualification
  2. EnsureDoctorExistence
  3. CheckInsuranceHistory
  4. EnsureEmptyBed

Nous pouvons également écrire un test unitaire pour chacune de ces fonctions.

Saeed Neamati
la source
2

Une règle de base simple que j'ai suivie est de nommer le test afin qu'il décrive précisément ce que fait le test. Si le nom du test devient trop complexe, c'est un signe que le test en fait peut-être trop. Ainsi, par exemple, nommer un test pour faire ce que vous proposez dans l'option 2 peut ressembler à PatientIsAddedDeviceIsAddedAndDeviceDataRecordsWhenSaved, ce qui est beaucoup plus complexe que trois tests distincts PatientIsAddedWhenSaved, DeviceIsAddedWhenSaved, DataRecordsWhenSaved. Je pense également que les leçons qui peuvent être tirées du BDD sont assez intéressantes où chaque test est vraiment représentatif d'une exigence unique qui pourrait être décrite dans un langage naturel.

jpierson
la source