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

Cet article fait partie de l’ensemble des articles sur B.D.D. (Behavior Driven Development) et fait suite à celui sur la rédaction des scénarios à l’aide de 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.

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 installé, créez un nouveau Projet de Tests Unitaires (Unit Test Project).
A noter que ce type de projet existe aussi bien pour .NET Core et .NET Framework, ainsi que les packages NuGet dont nous allons parler.

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 va vous falloir intégrer les packages NuGet de SpecFlow afin que les scénarios puissent être exécutés.
Cette opération devra être répétée autant de fois que vous avez de projets avec des scénarios.
Pour cela, faîtes un clic-droit sur le projet puis choisissez 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.xUnit ou SpecFlow.NUnit.

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

La configuration de SpecFlow se fait à l’aide d’un fichier specflow.json
Vous trouverez plus d’informations ici : Configuration — documentation (specflow.org)

Vous pouvez notamment y configurer la langue par défaut de vos scénarios.

4. Intégrer les scénarios

4.1. Génération des steps

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 steps 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 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 libre de renommer les méthodes comme bon vous semble ainsi que les paramètres et 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 regex 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

Capture d'écran - 2 scénarios avec Gherkin et Visual Studio

Maintenant, essayons avec un deuxième. Voilà à quoi ressemble désormais notre fichier de scénarios :

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/

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

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.

Verified by MonsterInsights