Points de données

Le processus BDD avec SpecFlow

Julie Lerman

Télécharger l'exemple de code

Julie LermanVous savez désormais que j'aime inviter des développeurs à s'exprimer sur des sujets qui attirent ma curiosité lorsque je suis avec le groupe d'utilisateurs que je dirige à Vermont. Tout cela a donné lieu à des rubriques telles su Knockout.js et Breeze.js. Il y a d'autres rubriques, telles que La séparation des responsabilités dans les requêtes de commandes (CQRS) sur lesquelles je travaille depuis un moment. Mais récemment, Dennis Doire (architecte et testeur) a parlé de SpecFlow et Selenium, deux outils pour testeurs effectuant du développement piloté par comportement (BDD). À nouveau, j'ai ouvert grand les yeux et j'ai commencé à chercher des excuses pour utiliser ces outils. En réalité, c'était le BDD qui attirait mon attention. Même si je suis tournée vers les données, l'époque où je concevais des applications à partir de base de données est loin derrière moi, et je cherche désormais à me concentrer sur le domaine.

BDD est une variante du développement piloté par les tests (TDD) qui se concentre sur les récits utilisateurs et la création de logiques et de tests autour de ces récits. Au lieu de satisfaire une règle unique, vous devez satisfaire des ensembles d'activités. Cette approche est très holistique (et j'adore ça), cette perspective m'intéresse donc beaucoup. L'idée est qu'alors qu'un test d'unité classique peut garantir qu'un événement sur un objet client fonctionne correctement, BDD se concentre, de manière plus générale, sur le comportement que moi j'attends en tant qu'utilisateur lorsque j'utilise le système que vous créez pour moi. BDD est souvent utilisé pour définir les critères d'acceptation durant les discussions avec les clients. Par exemple, lorsque je suis devant l'ordinateur et que je remplis un formulaire New Customer, puis que j'appuie sur le bouton Enregistrer, le système doit enregistrer les informations sur le client, puis afficher un message indiquant que cette opération a été effectuée avec succès.

Ou alors lorsque j'active la partie Customer Management du logiciel, il devrait ouvrir automatiquement le dernier client sur lequel j'ai travaillé dans ma dernière session.

Vous pouvez constater d'après ces récits utilisateur que BDD peut être une technique orientée interface utilisateur pour concevoir les tests automatisés, mais beaucoup de ces scénarios sont rédigés avant la conception d'une interface utilisateur. Et grâce à des outils tels que Selenium (docs.seleniumhq.org) et WatiN (watin.org), il est possible d'automatiser des tests dans le navigateur. Mais BDD ne consiste pas seulement à décrire l'interaction utilisateur. Pour obtenir une vision plus large de BDD, consultez la discussion sur InfoQ parmi certaines références sur BDD, TDD et des spécifications, par exemple ici bit.ly/10jp6ve.

Je ne veux plus avoir à me soucier de cliquer sur les boutons, etc. et je veux redéfinir un peu les récits utilisateur. Je peux supprimer les éléments dépendants de l'interface utilisateur du récit et me concentrer sur la partie du processus qui n'est pas dépendante de l'écran. Et, bien entendu, les récits qui m'intéressent sont ceux qui font référence à l'accès aux données.

La création de la logique pour savoir si un comportement particulier est satisfait ou non peut s'avérer laborieuse. SpecFlow est l'un des outils figurant dans la présentation de Doire (specflow.org). Cet outil s'intègre à Visual Studio et vous permet de définir des récits utilisateur (appelés scénarios) à l'aide de simples règles. Il automatise ensuite une partie de la création et de l'exécution des méthodes (parfois avec des tests, parfois sans). L'objectif est de valider que les règles du récit sont satisfaites.

Je vais vous expliquer comment créer quelques comportements pour vous mettre en appétit, et si vous en voulez plus, vous trouverez des ressources à la fin de l'article.

Vous devez tout d'abord installer SpecFlow dans Visual Studio, vous pouvez le faire à partir de Visual Studio Extensions et Updates Manager. L'objectif du BDD étant de commencer le développement de votre projet en décrivant des comportements, le premier projet de votre solution est un projet test dans lequel vous allez décrire ces comportements. Le reste de la solution va ensuite découler de ce point.

Vous devez créer un projet à l'aide du modèle Unit Test Project. Votre projet doit comporter une référence à TechTalk.SpecFlow.dll que vous pouvez l'installer à l'aide de NuGet. Vous devez ensuite créer un dossier appelé Features dans ce projet.

Ma première fonctionnalité sera basée sur un récit utilisateur à propos de l'ajout d'un nouveau client, c'est pourquoi j'ai créé un sous-dossier appelé Add dans le dossier Features (voir la figure 1). Voici où je vais définir mon scénario et demander de l'aide à SpecFlow.

Test Project with Features and Add Sub-FoldersFigure 1 Projet test contenant des sous-dossiers Features et Add

SpecFlow suit un modèle spécifique qui utilise des mots-clés qui aident à décrire la fonctionnalité dont vous définissez le comportement. Ces mots-clés proviennent d'un langage appelé Gherkin (« cornichon », comme le condiment), nom provenant d'un outil appelé Cucumber (cukes.info). Parmi ces mots-clés, on peut citer Given, And, When et Then, et vous pouvez les utiliser pour créer un scénario. Voici un scénario simple qui est encapsulé dans une fonctionnalité : Ajout d'un nouveau client :

Given a user has entered information about a customer
When she completes entering more information
Then that customer should be stored in the system

Il pourrait être plus élaboré, par exemple :

Given a user has entered information about a customer
And she has provided a first name and a last name as required
When she completes entering more information
Then that customer should be stored in the system

C'est dans la dernière commande que je vais appliquer de la persistance des données. SpecFlow ne se soucie pas de la manière dont cela se produit. L'objectif est d'écrire des scénarios pour prouver que le résultat est correct et qu'il le reste. Le scénario va déterminer l'ensemble de tests qui vont vous aider à compléter la logique du domaine :

Given that you have used the proper keywords
When you trigger SpecFlow
Then a set of steps will be generated for you to populate with code
And a class file will be generated that will automate the execution of these steps on your behalf

Voyons comment cela fonctionne.

Cliquez avec le bouton droit sur le dossier Add pour ajouter un nouvel élément. Si vous avez installé SpecFlow, vous pouvez trouver trois éléments qui lui sont associés en faisant une recherche sur specflow. Sélectionnez l'élément SpecFlow Feature File et donnez-lui un nom. J'ai appelé le mien AddCustomer.feature.

Un fichier feature démarre avec un exemple (la fonctionnalité mathématique omniprésente). Notez que cette fonctionnalité est décrite en haut, et en bas, un scénario (qui représente un exemple clé de la fonctionnalité) est décrit à l'aide des mots-clés Given, And, When et Then. Le complément SpecFlow garantit que le texte applique un codage de couleur afin que vous puissiez facilement discerner les termes de l'étape de vos propres instructions.

Je vais remplacer la fonctionnalité et les étapes définies par les miennes :

Feature: Add Customer
Allow users to create and store new customers
As long as the new customers have a first and last name

Scenario: HappyPath
Given a user has entered information about a customer
And she has provided a first name and a last name as required
When she completes entering more information
Then that customer should be stored in the system

(Merci à David Starr pour le nom du scénario ! Je l'ai volé dans sa vidéo Pluralsight.)

Que faire si les données requises ne sont pas fournies ? Je vais créer un autre scénario dans cette fonctionnalité pour gérer cette possibilité :

Scenario: Missing Required Data
Given a user has entered information about a customer
And she has not provided the first name and last name
When she completes entering more information
Then that user will be notified about the missing data
And the customer will not be stored into the system

Ça devrait suffire pour l'instant.

Du récit utilisateur au code

Vous avez vu pour l'instant l'élément Feature et le codage couleur fournis par SpecFlow. Notez qu'il y a un fichier codebehind associé au fichier feature qui contient des tests vides créés à partir des fonctionnalités. Chacun de ces tests va exécuter les étapes de votre scénario, mais vous n'avez pas besoin de les créer. Il existe plusieurs moyens de procéder. Vous pouvez exécuter les tests et SpecFlow renvoie la liste du code pour la classe Steps dans la sortie du test afin de la copier et de la coller. Vous pouvez aussi utiliser un outil dans le menu contextuel du fichier feature. Je vais détailler la seconde option :

  1. Cliquez avec le bouton droit de la souris dans la fenêtre de l'éditeur de texte du fichier feature. Dans le menu contextuel, vous voyez une section consacrée aux tâches SpecFlow.
  2. Cliquez sur Generate Step Definitions. Une fenêtre s'affiche pour vérifier les étapes à créer.
  3. Cliquez sur le bouton Copy methods to clipboard et utilisez les valeurs par défaut.
  4. Dans le dossier AddCustomer de votre projet, créez un fichier de classe intitulé Steps.cs.
  5. Ouvrez ce fichier, puis collez le contenu du Presse-papiers dans la définition de classe.
  6. Ajoutez une référence d'espace de nom en haut du fichier à l'aide de TechTalk.SpecFlow.
  7. Ajoutez une annotation de liaison à la classe.

Cette nouvelle classe est répertoriée dans la figure 2.

Figure 2 Le fichier Steps.cs

[Binding]
public class Steps
{
  [Given(@"a user has entered information about a customer")]
  public void GivenAUserHasEnteredInformationAboutACustomer()
  {
    ScenarioContext.Current.Pending();
  }
  [Given(@"she has provided a first name and a last name as required")]
  public void GivenSheHasProvidedAFirstNameAndALastNameAsRequired
 ()
  {
    ScenarioContext.Current.Pending();
  }
    [When(@"she completes entering more information")]
  public void WhenSheCompletesEnteringMoreInformation()
  {
    ScenarioContext.Current.Pending();
  }
  [Then(@"that customer should be stored in the system")]
  public void ThenThatCustomerShouldBeStoredInTheSystem()
  {
    ScenarioContext.Current.Pending();
  }
  [Given(@"she has not provided both the firstname and lastname")]
  public void GivenSheHasNotProvidedBothTheFirstnameAndLastname()
  {
    ScenarioContext.Current.Pending();
  }
  [Then(@"that user will get a message")]
  public void ThenThatUserWillGetAMessage()
  {
    ScenarioContext.Current.Pending();
  }
  [Then(@"the customer will not be stored into the system")]
  public void ThenTheCustomerWillNotBeStoredIntoTheSystem()
  {
    ScenarioContext.Current.Pending();
  }
}

Si vous observez les deux scénarios que j'ai créés, vous remarquez que même s'il y a des chevauchements dans ce qui est défini (par exemple « un utilisateur a entré des informations sur un client »), les méthodes générées ne créent pas d'étapes en double. Il faut également noter que SpecFlow va exploiter les constantes dans les attributs de méthode. Les noms de méthode à proprement parler sont sans intérêt.

À ce stade, vous pouvez laisser SpecFlow exécuter les tests qui appelleront ces méthodes. Alors que SpecFlow prend en charge plusieurs infrastructures de test unitaire, j'utilise MSTest, c'est pourquoi si vous cherchez cette solution dans Visual Studio, vous pouvez constater que le fichier codebehind de Feature définit une TestMethod pour chaque scénario. Chaque TestMethod exécute la combinaison correcte des méthodes détaillées avec une TestMethod qui s'exécute pour le scénario HappyPath.

Si je l'exécutais maintenant, en cliquant avec le bouton droit sur le fichier Feature et en choisissant « Exécuter les scénarios SpecFlow », le test ne serait pas concluant et je recevrai le message suivant : « One or more step definitions are not implemented yet. » En effet, chaque méthode du fichier Steps appellent toujours Scenario.Current.Pending.

Il est donc temps d'étoffer ces méthodes. D'après mes scénarios, j'ai besoin d'un type Customer avec certaines données obligatoires. Grâce à d'autres documentations, je sais que le nom et le prénom sont obligatoires, j'ai donc besoin de ces deux propriétés dans le type Customer. J'ai également besoin d'un mécanisme pour stocker ce client, ainsi qu'un emplacement de stockage. Pour mes tests, peu importe où et comment il est stocké, pourvu qu'il le soit, je vais donc utiliser un référentiel qui sera chargé de rassembler les données et de les stocker.

Je vais commencer par ajouter les variables _customer et _repository à la classe Steps :

private Customer _customer;
private Repository _repository;

Puis je vais remplacer une classe Customer :

public class Customer
{
  public int Id { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
}

Cela est suffisant pour que je puisse ajouter du code aux méthodes détaillées. La figure 3 illustre la logique ajoutée aux étapes indiquées dans HappyPath. J'ai créé un nouveau client, je dois ensuite fournir le nom et le prénom. Il n'y a pas grand-chose à faire pour détailler l'étape WhenSheCompletesEnteringMoreInformation.

Figure 3 Quelques-unes des méthodes SpecFlow

[Given(@"a user has entered information about a customer")]
public void GivenAUserHasEnteredInformationAboutACustomer()
{
  _newCustomer = new Customer();
}
[Given(@"she has provided a first name and a last name as required")]
public void GivenSheHasProvidedTheRequiredData()
{
  _newCustomer.FirstName = "Julie";
  _newCustomer.LastName = "Lerman";
}
[When(@"she completes entering more information")]
public void WhenSheCompletesEnteringMoreInformation()
{
}

La dernière étape est la plus intéressante. C'est là que non seulement je stocke le client, mais aussi que je prouve qu'il a effectivement été stocké. J'ai besoin d'une méthode Add dans mon référentiel pour stocker le client, d'une méthode Save pour l'envoyer à la base de données, puis d'une méthode pour savoir s'il est en effet possible de trouver ce client dans le référentiel. Je vais donc ajouter une méthode Add, une méthode Save et une méthode FindById dans mon référentiel ainsi :

public class CustomerRepository
{
  public void Add(Customer customer)
    { throw new NotImplementedException();  }
  public int Save()
    { throw new NotImplementedException();  }
  public Customer FindById(int id)
    { throw new NotImplementedException();  }
}

Je peux désormais ajouter une logique à l'étape finale qui sera appelée par mon scénario HappyPath. Je vais ajouter le client dans le référentiel et tester pour savoir s'il est possible de le trouver dans le référentiel. C'est là que finalement j'utilise une assertion pour déterminer si mon scénario a réussi. Si un client est trouvé (c'est-à-dire, IsNotNull), le test est réussi. Il s'agit d'un modèle très courant pour vérifier que les données ont été stockées. Toutefois, d'après mon expérience avec Entity Framework, je constate qu'il y a un problème qui ne sera pas détecté lors du test. Je vais commencer par le code suivant afin de vous montrer le problème de façon plus percutante et non vous montrer la manière correcte de commencer :

[Then(@"that customer should be stored in the system")]
public void ThenThatCustomerShouldBeStoredInTheSystem()
{
  _repository = new CustomerRepository();
  _repository.Add(_newCustomer);
  _repository.Save();
  Assert.IsNotNull(_repository.FindById(_newCustomer.Id));
}

Lorsque j'exécute à nouveau le test HappyPath, il échoue. Dans la figure 4, vous pouvez constater que le résultat du test montre comment le scénario SpecFlow fonctionne jusqu'ici. Mais vous devez faire attention à la raison pour laquelle le test a échoué. Ce n'est pas parce que FindById n'a pas trouvé le client, c'est parce que mes méthodes de référentiel ne sont pas encore implémentées.

Figure 4 Résultat d'un test qui a échoué indiquant le statut de chaque étape

Test Name:  HappyPath
Test Outcome:               Failed
Result Message:             
Test method UnitTestProject1.UserStories.Add.AddCustomerFeature.HappyPath threw exception:
System.NotImplementedException: The method or operation is not implemented.
Result StandardOutput:     
Given a user has entered information about a customer
-> done: Steps.GivenAUserHasEnteredInformationAboutACustomer() (0.0s)
And she has provided a first name and a last name as required
-> done: Steps. GivenSheHasProvidedAFirstNameAndALastNameAsRequired() (0.0s)
When she completes entering more information
-> done: Steps.WhenSheCompletesEnteringMoreInformation() (0.0s)
Then that customer should be stored in the system
-> error: The method or operation is not implemented.

Ma prochaine étape consiste à fournir une logique dans mon référentiel. Enfin, je vais utiliser ce référentiel pour interagir avec la base de données et comme je suis fan d'Entity Framework, je vais utiliser une entité Framework DbContext dans mon référentiel. Je vais commencer par créer une classe DbContext qui expose un Customers DbSet :

public class CustomerContext:DbContext
{
  public DbSet<Customer> Customers { get; set; }
}

Je peux ensuite refactoriser mon CustomerRepository pour utiliser le CustomerContext à des fins de persistance. Pour cette démonstration, je travaille directement dans le contexte plutôt que de me préoccuper des abstractions. Voici le CustomerRepository mis à jour :

public  class CustomerRepository
{
  private CustomerContext _context = new CustomerContext();
  public void Add(Customer customer
  {    _context.Customers.Add(customer);  }
  public int Save()
  {    return _context.SaveChanges();  }
  public Customer FindById(int id)
  {    return _context.Customers.Find(id);  }
}

À présent lorsque j'exécute à nouveau le test HappyPath, il est valide et toutes mes étapes sont marquées comme terminées. Mais je ne suis toujours pas satisfaite.

S'assurer que ces tests d'intégration comprennent le comportement d'EF

Pourquoi ne suis-je pas satisfaite quand mes tests sont validés et que je vois un cercle vert ? Car je sais que le test ne prouve pas vraiment que le client a été stocké.

Dans la méthode ThenThatCustomerShouldBeStoredInTheSystem, commentez l'appel à Save et exécutez à nouveau le test. Il est encore validé. Et je n'ai même pas enregistré le client dans la base de données ! Vous ne trouvez pas qu'il y a quelque chose qui cloche ? C'est que l'on appelle un « faux positif ».

Le problème c'est que la méthode DbSet Find que j'utilise dans mon référentiel est une méthode spéciale dans Entity Framework qui vérifie d'abord les objets en mémoire qui font l'objet d'un suivi par le contexte avant de passer dans la base de données. Lorsque j'ai appelé Add, j'ai rendu CustomerContext conscient de cette instance client. L'appel à Customers.Find a découvert cette instance et ainsi évité un passage inutile par la base de données. En fait, l'ID du client est toujours 0 car il n'a pas encore été stocké.

Comme j'utilise Entity Framework (et vous devez tenir compte du comportement de n'importe quelle infrastructure de mappage objet-relationnel (ORM) que vous utilisez), j'ai une manière plus simple pour tester et voir si le client est vraiment dans la base de données. Lorsque l'instruction EF SaveChanges insère le client dans la base de données, il retire le nouvel ID généré par la base de données du client et l'applique à l'instance qui est insérée. Par conséquent, si le nouvel ID du client n'est plus 0, je sais que mon client est vraiment dans la base de données. Je n'ai pas à interroger à nouveau la base de données.

Je vais repasser sur l'étape de vérification pour cette méthode en conséquence. Voici la méthode qui effectuera un test correct, j'en ai la certitude :

[Then(@"that customer should be stored in the system")]
  public void ThenThatCustomerShouldBeStoredInTheSystem()
  {
    _repository = new CustomerRepository();
    _repository.Add(_newCustomer);
    _repository.Save();
    Assert.IsNotNull(_newCustomer.Id>0);
  }

Le test est validé, et je sais qu'il l'est pour les bonnes raisons. Il n'est pas rare de définir un test d'échec, par exemple, à l'aide d'Assert.IsNull(FindById(customer.Id) pour s'assurer que le test n'est pas validé pour une mauvaise raison. Mais dans ce cas, le problème ne serait pas non plus apparu tant que je n'ai pas supprimé l'appel à Save. Si vous ne faites pas confiance au fonctionnement d'EF, il serait judicieux de créer également des tests d'intégration spécifiques, non liés aux récits utilisateur, pour vous assurer que vos référentiels se comportent comme vous le souhaitez.

Test de comportement ou test d'intégration ?

Au cours de mon apprentissage sur l'utilisation de ce premier scénario SpecFlow, je me suis trouvée face à une difficulté. Mon scénario indique que le client doit être stocké dans « le système ».

Le problème c'est que je n'étais pas sûre de la définition du mot système. D'après mon expérience, je sais que la base de données ou au moins un mécanisme de persistance est une partie très importante du système.

L'utilisateur ne se soucie pas des référentiels et des bases de données, seule son application l'intéresse. Mais il ne sera pas très content s'il ouvre une nouvelle session dans son application et ne parvient pas à retrouver le client car ce dernier n'a jamais été stocké dans la base de données (car je ne pensais pas que _repository.Save était nécessaire pour ce scénario).

J'ai contacté un autre Dennis (Dennis Doomen), l'auteur de Fluent Assertions et grand utilisateur de BDD, TDD, etc. dans des systèmes pour grandes entreprises. Il a confirmé qu'en tant que développeur, je devrais certainement appliquer mes connaissances aux étapes et aux tests même si cela signifie aller au-delà de ce que veut l'utilisateur qui a défini le scénario original. L'utilisateur fait part de ses connaissances et j'ajoute les miennes mais sans toutefois lui imposer mon point de vue technique. On continue à se comprendre et à bien communiquer.

Continuer à approfondir BDD et SpecFlow

Je suis presque sûre que s'il n'y avait pas eu tous les outils que j'ai créés pour prendre en charge BDD, mon expérience n'aurait pas été si facile. Même si je suis une passionnée de données, je fais très attention à collaborer avec mes clients, à comprendre leur entreprise et à m'assurer qu'ils sont satisfaits du logiciel que j'aide à créer pour eux. C'est la raison pour laquelle la conception pilotée par domaine et la conception pilotée par comportement m'interpellent autant. Je pense que plusieurs développeurs ressentent la même chose (même s'ils ne le clament pas haut et fort) et peuvent également être inspirés par ces techniques.

Outre les amis qui m'ont aidée à parvenir ici, voici certaines ressources qui m'ont été utiles. L'article du magazine MSDN, « Le processus BDD avec SpecFlow et WatiN » qui se trouve à cette adresse msdn.microsoft.com/magazine/gg490346, a été très utile. J'ai également regardé un excellent module du cours de David Starr intitulé Test First Development sur Pluralsight.com. (J'ai en réalité regardé ce module plusieurs fois.) J'ai trouvé que l'article de Wikipedia sur BDD (bit.ly/LCgkxf) était intéressant. En effet, il présente une vision globale de l'historique de BDD et les situations dans lesquelles il s'intègre dans d'autres pratiques. J'attends avec impatience le livre « BDD and Cucumber », dont Paul Rayner est le co-auteur (il m'a également aidé dans cet article) .

Julie Lerman est une Microsoft MVP, mentor et conseillère .NET et habite dans les collines du Vermont. Elle participe régulièrement à des groupes d'utilisateurs et à des conférences dans le monde entier, où elle partage son savoir-faire dans le domaine de l'accès aux données et d'autres sujets liés à Microsoft .NET. Son blog est accessible à l'adresse thedatafarm.com/blog. Elle est en outre l'auteur de « Programming Entity Framework » (2010), ainsi que d'une édition Code First (2011) et d'une édition DbContext (2012), publiées chez O’Reilly Media. Vous pouvez la suivre sur Twitter à l'adresse twitter.com/julielerman.

Merci aux experts techniques suivants d'avoir relu cet article : Dennis Doomen (Aviva Solutions) et Paul Rayner (Virtual Genius)
Dennis Doomen est consultant en chef chez Aviva Solutions (Pays-Bas), il est occasionnellement conférencier, coach Agile, auteur du livre Coding Guidelines for C# 3.0, 4.0 and 5.0, the Fluent Assertions framework and the Silverlight Cookbook. Il crée actuellement des solutions d'entreprise basées sur .NET, Event Sourcing et CQRS. Il adore le développement et l'architecture Agile, la programmation Xtreme et la conception pilotée par domaine. Vous pouvez le joindre sur Twitter : @ddoomen.