Difficultés avec TDD et refactoring (Ou - Pourquoi est-ce plus douloureux qu'il ne devrait l'être?)


20

Je voulais apprendre à utiliser l'approche TDD et j'avais un projet sur lequel je voulais travailler depuis un moment. Ce n'était pas un grand projet, donc j'ai pensé que ce serait un bon candidat pour TDD. Cependant, j'ai l'impression que quelque chose a mal tourné. Laissez-moi vous donner un exemple:

À un niveau élevé, mon projet est un complément pour Microsoft OneNote qui me permettra de suivre et de gérer des projets plus facilement. Maintenant, je voulais aussi garder la logique métier pour cela aussi découplée que possible de OneNote au cas où je déciderais de créer mon propre stockage personnalisé et back-end un jour.

J'ai d'abord commencé avec un test d'acceptation de mots simples pour décrire ce que je voulais que ma première fonctionnalité fasse. Cela ressemble à ceci (en le simplifiant par souci de concision):

  1. L'utilisateur clique sur créer un projet
  2. Types d'utilisateurs dans le titre du projet
  3. Vérifiez que le projet est créé correctement

En sautant les choses sur l'interface utilisateur et une planification intermédiaire, j'arrive à mon premier test unitaire:

[TestMethod]
public void CreateProject_BasicParameters_ProjectIsValid()
{
    var testController = new Controller();
    Project newProject = testController(A.Dummy<String>());
    Assert.IsNotNull(newProject);
}

Jusqu'ici tout va bien. Rouge, vert, refactor, etc. Bon maintenant, il faut réellement enregistrer des trucs. Découpant quelques étapes ici, je termine avec cela.

[TestMethod]
public void CreateProject_BasicParameters_ProjectMatchesExpected()
{
    var fakeDataStore = A.Fake<IDataStore>();
    var testController = new Controller(fakeDataStore);
    String expectedTitle = fixture.Create<String>("Title");
    Project newProject = testController(expectedTitle);

    Assert.AreEqual(expectedTitle, newProject.Title);
}

Je me sens toujours bien à ce stade. Je n'ai pas encore de magasin de données concret, mais j'ai créé l'interface à quoi je m'attendais.

Je vais sauter quelques étapes ici parce que ce message est assez long, mais j'ai suivi des processus similaires et finalement j'arrive à ce test pour mon magasin de données:

[TestMethod]
public void SaveNewProject_BasicParameters_RequestsNewPage()
{
    /* snip init code */
    testDataStore.SaveNewProject(A.Dummy<IProject>());
    A.CallTo(() => oneNoteInterop.SavePage()).MustHaveHappened();
}

C'était bon jusqu'à ce que j'essaie de l'implémenter:

public String SaveNewProject(IProject project)
{
    Page projectPage = oneNoteInterop.CreatePage(...);
}

Et il y a le problème là où se trouve le "...". Je me rends compte à ce stade que CreatePage nécessite un ID de section. Je ne m'en étais pas rendu compte lorsque je pensais au niveau du contrôleur parce que je ne voulais que tester les bits pertinents pour le contrôleur. Cependant, tout en bas ici, je me rends compte maintenant que je dois demander à l'utilisateur un emplacement pour stocker le projet. Maintenant, je dois ajouter un ID d'emplacement à la banque de données, puis en ajouter un au projet, puis en ajouter un au contrôleur et l'ajouter à TOUS les tests qui sont déjà écrits pour toutes ces choses. Il est devenu fastidieux très rapidement et je ne peux pas m'empêcher de penser que j'aurais compris cela plus rapidement si j'avais esquissé le design à l'avance plutôt que de le laisser être conçu pendant le processus TDD.

Quelqu'un peut-il m'expliquer si j'ai fait quelque chose de mal dans ce processus? Existe-t-il de toute façon ce type de refactoring qui peut être évité? Ou est-ce courant? S'il est courant, existe-t-il des moyens de le rendre plus indolore?

Merci a tous!


Vous obtiendriez des commentaires très perspicaces si vous publiez ce sujet sur ce forum de discussion: groups.google.com/forum/#!forum/… qui est spécifiquement pour les sujets TDD.
Chuck Krutsinger

1
Si vous devez ajouter quelque chose à tous vos tests, il semble que vos tests soient mal écrits. Vous devez refactoriser vos tests et envisager d'utiliser un appareil sensible.
Dave Hillier du

Réponses:


19

Bien que TDD soit (à juste titre) présenté comme un moyen de concevoir et de développer votre logiciel, c'est toujours une bonne idée de penser à la conception et à l'architecture au préalable. IMO, "esquisser le design à l'avance" est un jeu équitable. Cependant, ce sera souvent à un niveau plus élevé que les décisions de conception que vous serez amené à travers TDD.

Il est également vrai que lorsque les choses changent, vous devrez généralement mettre à jour les tests. Il n'y a aucun moyen d'éliminer complètement cela, mais vous pouvez faire certaines choses pour rendre vos tests moins cassants et minimiser la douleur.

  1. Autant que possible, gardez les détails de mise en œuvre hors de vos tests. Cela signifie uniquement tester par des méthodes publiques et, si possible, privilégier la vérification basée sur l'état à la vérification basée sur l'interaction . En d'autres termes, si vous testez le résultat de quelque chose plutôt que les étapes pour y arriver, vos tests devraient être moins fragiles.

  2. Minimisez la duplication dans votre code de test, tout comme vous le feriez dans le code de production. Ce message est une bonne référence. Dans votre exemple, il semble difficile d'ajouter la IDpropriété à votre constructeur, car vous avez appelé le constructeur directement dans plusieurs tests différents. Au lieu de cela, essayez d'extraire la création de l'objet dans une méthode ou de l'initialiser une fois pour chaque test dans une méthode d'initialisation de test.


J'ai lu les mérites de l'état basé sur l'interaction et je le comprends la plupart du temps. Cependant, je ne vois pas comment c'est possible dans tous les cas sans exposer EXPLICITEMENT les propriétés pour le test. Prenez mon exemple ci-dessus. Je ne sais pas comment vérifier que le magasin de données a bien été appelé sans utiliser une assertion pour "MustHaveBeenCalled". Quant au point 2, vous avez absolument raison. J'ai fini par le faire après toutes les modifications, mais je voulais simplement m'assurer que mon approche était généralement conforme aux pratiques TDD acceptées. Merci!
Landon

@Landon Il existe des cas où les tests d'interaction sont plus appropriés. Par exemple, vérifier qu'un appel a été effectué vers une base de données ou un service Web. Fondamentalement, chaque fois que vous devez isoler votre test, en particulier d'un service externe.
jhewlett

@Landon Je suis "un classique convaincu", donc je ne suis pas très expérimenté avec les tests basés sur les interactions ... Mais vous n'avez pas besoin de faire une affirmation pour "MustHaveBeenCalled". Si vous testez une insertion, vous pouvez utiliser une requête pour voir si elle a été insérée. PS: J'utilise des stubs pour des raisons de performances lors de tout tester sauf la couche de base de données.
Hbas

@jhewlett C'est aussi la conclusion à laquelle je suis arrivé. Merci!
Landon

@Hbas Aucune base de données à interroger. Je suis d'accord que ce serait la voie la plus simple à suivre si j'en avais un, mais j'ajoute cela à un bloc-notes OneNote. Le mieux que je puisse faire est d'ajouter une méthode Get à ma classe d'assistance interop pour essayer de tirer la page. JE POURRAIS écrire le test pour le faire, mais j'avais l'impression de tester deux choses à la fois: ai-je enregistré cela? et ma classe d'aide récupère-t-elle correctement les pages? Cependant, je suppose qu'à un moment donné, vos tests devront peut-être s'appuyer sur un autre code testé ailleurs. Merci!
Landon

10

... Je ne peux pas m'empêcher de penser que j'aurais attrapé cela plus rapidement si j'avais esquissé le design à l'avance plutôt que de le laisser être conçu pendant les processus TDD ...

Peut-être peut-être pas

D'une part, TDD fonctionnait très bien, vous offrant des tests automatisés au fur et à mesure que vous développiez des fonctionnalités et se cassant immédiatement lorsque vous deviez changer l'interface.

D'un autre côté, peut-être que si vous aviez commencé avec la fonctionnalité de haut niveau (SaveProject) au lieu d'une fonctionnalité de niveau inférieur (CreateProject), vous auriez remarqué des paramètres manquants plus tôt.

Là encore, peut-être que vous n'en auriez pas. C'est une expérience irremplaçable.

Mais si vous cherchez une leçon pour la prochaine fois: commencez par le haut. Et pensez au design autant que vous le souhaitez en premier.


0

https://frontendmasters.com/courses/angularjs-and-code-testability/ D'environ 2:22:00 à la fin (environ 1 heure). Désolé que la vidéo ne soit pas gratuite, mais je n'en ai pas trouvé une gratuite qui l'explique si bien.

L'une des meilleures présentations de l'écriture de code testable se trouve dans cette leçon. C'est une classe AngularJS, mais la partie test est tout autour du code java, principalement parce que ce dont il parle n'a rien à voir avec le langage, et tout à voir avec l'écriture d'un bon code testable en premier lieu.

La magie réside dans l'écriture de code testable, plutôt que dans l'écriture de tests de code. Il ne s'agit pas d'écrire du code qui prétend être un utilisateur.

Il passe également un certain temps à rédiger la spécification sous la forme d'affirmations de test.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.