BDD 3/5 : Implémenter les scénarios en C# avec Specflow

Cet article fait partie de l’ensemble des articles sur la méthodologie B.D.D. (Behavior Driven Development) et fait suite à celui sur la rédaction des scénarios à l’aide du langage Gherkin. Désormais, nous allons aborder l’implémentation des scénarios que nous avons rédigés ensemble. Cette partie concernant avant tout les développeurs. Pour ce qui va suivre, je vais utiliser C# et Visual Studio ainsi qu’un nouvel outil appelé SpecFlow.

1. Intégrer SpecFlow dans Visual Studio

SpecFlow permet à la fois de rédiger tous nos scénarios dans Visual Studio mais aussi de les « transformer » en code,  de les implémenter.

SpecFlow existe en deux versions : SpecFlow (version gratuite) et SpecFlow+ (version payante des fonctionnalités supplémentaires) : https://specflow.org/getting-started/

L’installation du plugin SpecFlow dans Visual Studio vous permet d’écrire des fichiers .feature au sein de l’IDE.
Cette étape sera nécessaire qu’une seule fois.

Suivez le guide pour installer SpecFlow et l’intégrer à Visual Studio. Une fois que c’est fait, vous pouvez dès à présent créer un nouveau Projet de Tests Unitaires (Unit Test Project).

Capture d'écran - Création du projet de tests unitaires dans Visual Studio

Création du projet de tests unitaires dans Visual Studio

2. Intégrer SpecFlow dans le projet de tests

En plus de l’extension, il vous faudra intégrer les packages NuGet de SpecFlow afin que les scénarios puissent être exécutés lorsqu’ils auront été implémentés.
Cette opération devra être répétée autant de fois que vous avez de projets avec des scénarios.
Pour cela, il suffit de faire un clic-droit sur le projet puis de choisir l’option Gérer les packages NuGet (Manage NuGet Packages).

Capture d'écran - Gérer les packages NuGet dans Visual Studio

Gérer les packages NuGet dans Visual Studio

Cherchez le package SpecFlow.MsTest dans la liste.
MsTest est le moteur de tests unitaires fourni par Microsoft et Visual Studio.
NuGet va ajouter deux dépendances supplémentaires à votre projet : SpecFlow et SpecFlow.MsTest.

Si vous l’avez l’habitude d’utiliser un autre framework de tests unitaires, vous pouvez chercher SpecFlow.NUnit si vous utilisez NUnit ou SpecFlow.xUnit si vous utilisez xUnit.

Capture d'écran - NuGet Package Manager

Packages installés par NuGet

Une fois les packages NuGet installés, passons en revue rapidement la configuration de SpecFlow.

3. Configuration de SpecFlow

Cette dernière se passe dans le fichier App.config :

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    
<section name="specFlow" type="TechTalk.SpecFlow.Configuration.ConfigurationSectionHandler, TechTalk.SpecFlow" />
  </configSections>

  <specFlow>
    <!-- For additional details on SpecFlow configuration options see http://go.specflow.org/doc-config -->
    <unitTestProvider name="MsTest" />
  </specFlow>
</configuration>

Par défaut, les scénarios sont pris en charge s’ils sont rédigés en Anglais (Given-When-Then) et le moteur de tests unitaires utilisé est MsTest.

Si vous souhaitez rédiger vos scénarios en Français, ajoutez la ligne suivante dans la section :

<language feature="fr-fr" />

Tous les autres paramètres de configuration sont disponibles dans la documentation : https://specflow.org/documentation/Configuration/

4. Intégrer les scénarios

4.1. Générer les scénarios à l’aide de SpecFlow

Faîtes un clic-droit sur votre projet, puis cliquez sur Ajouter -> Nouvel élément (ou Add -> New Item).
Dans la liste, choisissez SpecFlow Feature File (French/français) ou SpecFlow Feature File (si vous rédigez vos scénarios en anglais). Nous pourrons appeler notre fichier ServirCafe.feature par exemple.

Reprenons la fonctionnalité de notre article précédent pour l’intégrer dans notre fichier ServirCafe.feature :

Fonctionnalité: Servir un café
En tant qu'utilisateur
Je veux consommer un café
dont le prix fixe est de 40 centimes

Scénario: Servir un café court sans sucre quand je fais l'appoint
Etant donné que j'ai inséré 0,40 euros
Quand je demande un "café court sans sucre"
Alors la machine me remplit un gobelet de "café court sans sucre"

A présent, faîtes un clic-droit sur votre scénario, choisissez l’option Generate Step Definitions, puis cliquez sur Generate.

Capture d'écran - Scénario rédigé avec SpecFlow

Générer l’implémentation des scénarios à l’aide de SpecFlow

Cliquez ensuite sur Generate

Cliquez ensuite sur Generate

Un nouveau fichier ServirUnCafeSteps.cs a été ajouté au projet. A l’intérieur on y trouve normalement une classe nommée ServirUnCafeSteps marquée d’un attribut [Binding] et contenant 3 méthodes publiques :

[Binding]
public class ServirUnCafeSteps

La première méthode correspond à la ligne : Etant donné que j’ai inséré 0,40 euros

[Given(@"que j'ai inséré (.*) €")]
public void SoitQueJAiInsere(Decimal p0)
{
    ScenarioContext.Current.Pending();
}

La seconde méthode correspond à la ligne : Quand je demande un « café court sans sucre »

[When(@"je demande un ""(.*)""")]
public void QuandJeDemandeUn(string p0)
{
    ScenarioContext.Current.Pending();
}

La troisième méthode correspond à la ligne : Alors la machine me remplit un gobelet de « café court sans sucre »

[Then(@"la machine me remplit un gobelet de ""(.*)""")]
public void AlorsLaMachineMeRemplitUnGobeletDe(string p0)
{
    ScenarioContext.Current.Pending();
}

4.2. Vérifier que la génération des scénarios s’est bien passée

Avant de rentrer dans le détail de chaque méthode, vérifions que tout est en place.

Lancez les tests unitaires en cliquant sur Test – Run – All tests ou en utilisant le raccourci clavier CTRL + R et A.
A ce stade, vous devriez voir apparaître un test unitaire échoué avec une erreur indiquant : « One or more step definitions are not implemented yet« .

Capture d'écran - Echec du premier test Specflow

 

Dans ce cas, votre fichier de scénario (.feature) et correctement lié à votre classe de tests (.cs).
SpecFlow s’y retrouve en fait grâce aux attributs utilisés (Binding, Given, When, Then…) dans la classe de tests et grâce aux expressions régulières (« regex« ).

Grâce à ce mécanisme, vous êtes en mesure de transmettre des paramètres depuis votre scénario directement à votre code de test :

  • des nombres entiers ou à virgule
  • des chaînes de caractères à l’aide des guillemets
  • des tableaux de données (dans l’exemple un peu plus bas)

Ainsi, vous pouvez modifier vos jeux de tests et les exécuter pour vous assurer que le scénario est bien géré.

Pour info, vous êtes libres de renommer les méthodes comme bon vous semble ainsi que les paramètres y compris changer leur type mais surtout, ne touchez ni aux attributs ni aux expressions régulières, sauf si vous êtes suffisamment à l’aise avec SpecFlow et les expressions régulières pour le faire.

4.3. Implémenter les scénarios

Nous allons compléter nos 3 méthodes.

  • La première va nous permettre de récupérer le montant (ici 0.40 €) à insérer dans la machine
  • La seconde va exécuter l’action de demander un café court sans sucre avec l’appoint
  • La troisième va vérifier que la machine a bien servi un café court sans sucre
[Binding]
public class ServirUnCafeSteps
{
   private decimal amount;
   private string drinkName;

   [Given(@"que j'ai inséré (.*) €")]
   public void SoitQueJAiInsere(decimal amount)
   {
      this.amount = amount;
   }

   [When(@"je demande un ""(.*)""")]
   public void QuandJeDemandeUn(string drinkName)
   {
      this.drinkName = CoffeeManager.Fill(drinkName, amount);
   }

   [Then(@"la machine me remplit un gobelet de ""(.*)""")]
   public void AlorsLaMachineMeRemplitUnGobeletDe(string drinkName)
   {
      Assert.AreEqual(drinkName, this.drinkName);
   }
}

Notons que j’ai crée une nouvelle classe CoffeeManager pour l’occasion :

public static class CoffeeManager
{
   public static string Fill(string drinkName, decimal amount)
   {
      return drinkName;
   }
}

4.4. Vérifier l’implémentation de notre scénario

Lançons l’exécution de nos tests (CTRL+R puis A), et regardons le résultat. Tout fonctionne.

Capture d'écran - Le test est passé avec succès

Le test est passé avec succès

Nous avons implémenté notre premier scénario. Hourra !

5. Implémenter un second scénario

Maintenant, essayons avec un deuxième. Voilà à quoi ressemble désormais notre fichier de scénarios :
Capture d'écran - 2 scénarios avec Gherkin et Visual Studio

Dans le cas où le montant de 0.40€ n’est pas respecté, la méthode Fill() de la classe CoffeeManager renverra nul.
Répétez les étapes 4.1 à 4.4 pour implémenter le second scénario.
Vous devriez avoir quelque chose qui ressemble à ceci :

[Binding]
public class ServirUnCafeSteps
{
   private decimal amount;
   private string drinkName;

   [Given(@"que j'ai inséré (.*) €")]
   public void SoitQueJAiInsere(decimal amount)
   {
      this.amount = amount;
   }

   [When(@"je demande un ""(.*)""")]
   public void QuandJeDemandeUn(string drinkName)
   {
      this.drink = CoffeeManager.Fill(drinkName, amount);
   }

   [Then(@"la machine me remplit un gobelet de ""(.*)""")]
   public void AlorsLaMachineMeRemplitUnGobeletDe(string drinkName)
   {
      Assert.AreEqual(drinkName, this.drinkName);
   }

   [Then(@"la machine me demande d'ajouter de la monnaie")]
   public void AlorsLaMachineMeDemandeDAjouterDeLaMonnaie()
   {
      Assert.IsNull(this.drinkName);
   }
}

La classe CoffeeManager :

public static class CoffeeManager
{
   public static string Fill(string drinkName, decimal amount)
   {
      if (amount == 0.4m)
      {
         return drinkName;
      }

      return null;
   }
}

Nos deux scénarios fonctionnent.

Capture d'écran - Tests unitaires au vert

Les deux scénarios sont au vert

Vous avez compris le principe.

6. Faire une fonctionnalité complète

Ensuite vous pouvez compléter avec autant de scénarios que vous avez rédigé pour couvrir tous les cas comme par exemple, avec une autre boisson (thé ou café au lait) ou s’il y a trop de monnaie insérée.
Voici la fonctionnalité complète et son implémentation pour notre machine à café :

Capture d'écran - tous les scénarios

Tous les scénarios sont au vert

[Binding]
public class ServirUnCafeSteps
{
    private decimal amount;
    private DrinkInfo drink;

    [Given(@"j'ai inséré (.*) €")]
    public void SoitQueJAiInsere(decimal amount)
    {
        this.amount = amount;
    }
        
    [When(@"je demande un ""(.*)""")]
    public void QuandJeDemandeUn(string drinkName)
    {
        this.drink = CoffeeManager.Fill(drinkName, amount);
    }
        
    [Then(@"la machine me remplit un gobelet de ""(.*)""")]
    public void AlorsLaMachineMeRemplitUnGobeletDe(string drinkName)
    {
        Assert.AreEqual(drinkName, this.drink.Name);
    }

    [Then(@"la machine me rend (.*) € de monnaie")]
    public void AlorsLaMachineMeRendDeMonnaie(Decimal change)
    {
        Assert.AreEqual(change, this.drink.Change);
    }

    [Then(@"la machine me demande d'ajouter de la monnaie")]
    public void AlorsLaMachineMeDemandeDAjouterDeLaMonnaie()
    {
        Assert.IsNull(this.drink);
    }
}
public static class CoffeeManager
{
    private const decimal Price = 0.4m;

    public static DrinkInfo Fill(string drinkName, decimal amount)
    {
        DrinkInfo drinkInfo = null;

        if (amount >= Price)
        {
            drinkInfo = new DrinkInfo { Name = drinkName };

            if (amount > Price)
            {
                drinkInfo.Change = amount - Price;
            }
        }

        return drinkInfo;
    }
}
public class DrinkInfo
{
    public string Name { get; set; }
    public decimal Change { get; set; }
}

Notons que j’ai utilisé un objet DrinkInfo me permettant de récupérer le libellé de la boisson ainsi que le rendu monnaie.

7. Avec des tableaux

Comme je l’ai précisé là-haut, et je terminerai là-dessus, il est également possible d’utiliser des tableaux en tant que paramètre. Un tableau peut représenter contenant autant de lignes et de colonnes que vous le souhaitez.
Reprenons un autre exemple de scénario issu de l’article précédent :

Capture d'écran - Scénario utilisant les tableaux

Dans ce scénario, nous avons 2 tableaux

Qui va être représenté en code de tests de la façon suivante :

[Binding]
public class LoginSteps
{
    private User user;
    private string message;

    [Given(@"que l'utilisateur suivant")]
    public void SoitQueLUtilisateurSuivant(Table table)
    {
        ScenarioContext.Current.Pending();
    }
        
    [When(@"je tente de me connecter avec les coordonnées")]
    public void QuandJeTenteDeMeConnecterAvecLesCoordonnees(Table table)
    {
        ScenarioContext.Current.Pending();
    }
        
    [Then(@"un message m'indique ""(.*)""")]
    public void AlorsUnMessageMIndique(string message)
    {
        ScenarioContext.Current.Pending();
    }
}

Dans les méthodes correspondant aux instructions Given et When, le paramètre passé est de type Table.
Je peux :

  • Le transformer directement en un objet qui contient exactement les mêmes propriétés à l’aide de la méthode CreateInstance()
    table.CreateInstance<User>();
  • Le transformer en une liste d’objets à l’aide de CreateSet()
    table.CreateSet<User>();
  • Récupérer manuellement les valeurs une par une
    var email = table.Rows[0]["email"]; // Récupération de la case Email de la première ligne
    var motDePasse = table.Rows[0]["motDePasse"]; // Récupération de la case MotDePasse de la première ligne

Désormais, vous avez les premières armes pour aborder l’implémentation des scénarios au sein de l’environnement .NET.
Je vous réfère une nouvelle fois à la documentation de SpecFlow si vous souhaitez de plus amples informations : http://specflow.org/documentation/
Mon prochain article traitera de l’implémentation mais côté Javascript cette fois-ci.

Articles similaires:

Lien Permanent pour cet article : https://www.jbvigneron.fr/2018/07/05/bdd-implementer-scenarios-en-csharp-avec-specflow/

Laisser un commentaire

Your email address will not be published.

css.php