Blog Arolla

BDD et SpecFlow pour des tests plus lisibles

Dans ce billet, je vais utiliser SpecFlow et la syntaxe Gherkin pour écrire des tests en langage naturel. Je souhaite montrer à quel point la mise en place en est facile, lorsque vous disposez de code déjà testable, et comment celle-ci vous aide à écrire de meilleurs tests en quelques minutes.

SpecFlow est à la fois un composant ou framework de test, et un plug-in pour Visual Studio, qui permet d’écrire des tests d’une manière naturelle, puis de les exécuter avec votre dérouleur de tests (test-runner) favori. Comme on peut le lire sur leur site (en anglais, traduit ici par mes soins) :

SpecFlow a pour but de combler le fossé de communication entre les experts du domaine et les développeurs, en liant des spécifications de comportements, lisibles par le métier, avec leur implémentation sous-jacente. Notre mission est de fournir une approche pragmatique et sans friction au développement guidé par les tests d’acceptance (ATDD) et au développement guidé par le comportement (BDD) pour les projets .NET d’aujourd’hui.

SpecFlow est construit sur la base de la syntaxe Gherkin, une grammaire conçue pour écrire des spécifications de comportement. Cette syntaxe a d’abord émergé de la communauté Ruby, et est liée à un projet nommé Cucumber. Pour plus de détails, vous pouvez consulter le site http://cukes.info :

Cucumber  est la réécriture par Aslak Hellesøy du “story-runner” de RSpec, qui a été originellement écrit par Dan North. (qui à nouveau était une réécriture de sa première implémentation - RBehave. RBehave était son portage en Ruby de JBehave)…

Gherkin est le DSL (Domain Specific Language) de Cucumber, et en tant que tel, il n’est pas lié à un langage ou à une technologie particulière. Pour cette raison il peut être utilisé en .NET en utilisant SpecFlow. Cette syntaxe n’a que très peu de mots-clés, mais se révèle très puissante. La meilleure manière d’expliquer son fonctionnement est de montrer un exemple de scénario de test :

Scenario: Filter on age
    Given I have written a query against the provider
    And I have added a Age >= 36 where clause
    Given The people finder service filters on Age >= 36
    When I execute the query
    Then The service parameter should be Age IsGreaterThan 36
    And The result count should be 3
    And The result should validate the servicePredicate

Arrivé à ce point du billet, vous aurez peut-être remarqué que je travaille à nouveau sur mon provider Linq… mais dans ce billet je ne parlerai que de test et de l’utilisation de SpecFlow, alors ne fuyez pas, même si vous n’arrivez pas à digérer les arbres d’expressions !

Ce que SpecFlow vous permet de faire, c’est d’écrire vos spécifications et cas de tests sous forme de texte brut, et de générer des classes de tests sur cette base. SpecFlow propose un certain nombre de fonctionnalités particulièrement intéressantes, qui permettent d’organiser les spécifications des tests et des exemples.

Afin d’utiliser SpecFlow dans votre projet, la manière la plus simple de procéder est d’ajouter une référence dans votre projet de test, en utilisant le gestionnaire de packages NuGet. Pour obtenir l’intégration à Visual Studio, vous devez installer également l’extension “SpecFlow integration for Visual Studio extension”.

Lorsqu’on développe en BDD, les tests sont écrits pour tester des fonctionnalités et des exemples précis de comportements. Pour chaque fonctionnalité de votre application (ou composant, ou classe), vous pouvez créer un fichier “feature”. Comme vous pouvez le voir dans l’exemple précédent, les tests vont être organisés autour du concept de “scénario”. Chaque fichier “feature” peut contenir plusieurs scénarios (ou scenarii pour les purs latinistes). Le lien entre les scénarios et l’implémentation concrète des tests va s’effectuer ligne à ligne : chaque ligne de scénario va correspondre au final à un appel de méthode.

  • le clauses “Given” et leurs “And” associés vont correspondre à des méthodes qui mettent en place le contexte des tests à effectuer,
  • la clause “When” va déclencher une action ou un évènement qui doit engendrer le comportement que l’on souhaite tester,
  • les clauses “Then” et ses “And” associés vont effectuer des assertions qui déterminent le résultat du test.

Pour faire correspondre les lignes d’un scénario et les méthodes de la classe de test, ces méthodes sont décorées d’attributs spécifiques à SpecFlow, qui prennent en argument le modèle de la ligne associée. Ce modèle de phrase peut inclure des groupes d’expressions permettant de capturer des valeurs, qui seront utilisées pour renseigner à l’exécution les paramètres de la méthode à appeler.

[Given(@"I have added a (.*) where clause")]
public void GivenIHaveAddedAWhereClause(string predicate)

Si nous nous penchons à nouveau sur l’exemple présenté précédemment, la meilleure façon de comprendre comment ce scénario est décliné en appel de méthodes est d’observer la trace générée en sortie de test :

Given I have written a query against the provider
-> done: FeaturesTest.GivenIHaveWrittenAQuery() (0,0s)
And I have added a Age >= 36 where clause
-> done: FeaturesTest.GivenIHaveAddedAWhereClause("Age >= 36") (0,0s)
Given The people finder service filters on Age >= 36
-> done: FeaturesTest.GivenThePeopleFinderServiceFiltersOn("Age >= 36") (0,0s)
When I execute the query
-> done: FeaturesTest.WhenIExecuteTheQuery() (0,1s)
Then The service parameter should be Age IsGreaterThan 36
-> done: FeaturesTest.ThenTheServiceParameter("Age IsGreaterThan 36") (0,0s)
And The result count should be 3
-> done: FeaturesTest.ThenTheResultCount(3) (0,0s)
And The result should validate the servicePredicate
-> done: FeaturesTest.ThenTheResultShouldValidatetheServicePredicate() (0,0s)

Nous pouvons clairement voir que chaque ligne du scénario a été utilisée pour appeler une méthode. Pour “accumuler” les conditions et pouvoir effectuer des assertions sur les résultats, nous utilisons une classe qui définit le contexte du test :

public class ServiceContext
{
    // Data context
    public IEnumerable<Person> AllPeople { get; set; }

    // Input
    public List<string> Query { get; set; }
    public List<string> ServiceLambdas { get; set; }

    // Service predicate, built upon the ServiceLambdas
    public Expression<Func<Person, bool>>
                            ServicePredicate {get; set;}

    // Output
    public List<Person> Results { get; set; }
    public SearchCriteria PassedCriteria { get; set; }
}

En utilisant cette classe de contexte, les méthodes correspondant aux clauses “Given” renseignent les propriétés utilisées en entrée du test, le “When” exécute l’action à tester (ici, l’évaluation de la requête Linq par le provider), et les “Then” vérifient les assertions sur les propriétés correspondant aux données attendues en sortie.

L’étape suivante est d’ajouter des cas de tests supplémentaires. Une façon simple de le réaliser pourrait être de copier-coller le scénario précédent et d’en changer les paramètres. Mais comme toujours, nous cherchons à réduire la duplication du code (DRY). Nous allons donc utiliser ici une fonctionnalité très pratique et mettre en œuvre un “Scenario Outline”, associé à des exemples :

Scenario Outline: Filter on a single criterion
    Given I have written a query against the provider
    And I have added a <predicate> where clause
    Given The people finder service filters on <servicePredicate>
    When I execute the query
    Then The service parameter should be <serviceParameter>
    And The result count should be <resultsCount>
    And The result should validate the servicePredicate
    Examples:
    | predicate                     | servicePredicate              | serviceParameter            | resultsCount |
    | Age >= 36                     | Age >= 36                     | Age IsGreaterThan 36        | 3            |
    | Age >= 36                     |                               | Age IsGreaterThan 36        | 6            |
    | FirstName.StartsWith("Scar")  | FirstName.StartsWith("Scar")  | FirstName StartsWith Scar   | 1            |
    | LastName == Alba              |                               | LastName Equals Alba        | 6            |

Une fois le “Scenario Outline” préparé, nous pouvons lister tous les cas à tester. Ajouter un cas de test correspondant au même scénario ne demande alors plus que l’ajout d’une unique ligne d’exemple.

Si vous avez suivi attentivement ce billet jusqu’ici, vous aurez peut-être remarqué que mon scénario fait des assertions sur le nombre de résultats, mais que je n’ai défini nulle part de données de test. En fait, il y a une clause “Given” cachée, qui est utilisée pour tous les scénarios du fichier “feature” ! Pour mettre en place un tel comportement, il suffit d’utiliser le mot-clé “Background” :

Background:
    Given The people are
    | FirstName | LastName    | Age | Id                                     |
    | Scarlett  | Johansson   | 27  | {8c319634-935d-4681-adcc-02d5347fe6c4} |
    | Jessica   | Alba        | 31  | {32a84597-8c3d-44bc-a1a5-6538188e9d25} |
    | Penelope  | Cruz        | 38  | {5aa0eb59-3961-472f-b829-7d54ac8eeeef} |
    | Reese     | Witherspoon | 36  | {77fcd741-3839-4692-925f-a3a0eb19cf42} |
    | Charlize  | Theron      | 36  | {e37290f6-d376-44e2-944d-d0af13c1a75c} |
    | Mouloud   | Achour      | 31  | {18af541a-a5dc-41d2-af47-479b1c06e216} |

Un dernier point : bien que l’interprétation des expressions lambdas fournies en arguments dans ce scénario soit un sujet indépendant qui pourrait être discuté plus avant, paramétrer les données de test utilisées dans la dernière clause “Given” présentée est d’une simplicité enfantine, et je n’ai pas pu résister à reproduire le code ici :

[Given(@"The people are")]
public void GivenThePeopleAre(Table people)
{
    this.context.AllPeople = people.CreateSet<Person>();
}

Grâce aux méthodes utilitaires incluses dans SpecFlow, la création du jeu de données de test est complètement automatisée ! En conclusion, je vous recommande très fortement d’essayer SpecFlow, et je pense vraiment que vous ne le regretterez pas.

Tout le code source de ce billet ainsi que celui des billets précédents de la série Linq Provider, est désormais disponible sur mon GitHub : https://github.com/pirrmann/LinqToService. N’hésitez surtout pas à brancher / contribuer sur ce code, et je serais vraiment ravi de recevoir vos avis éventuels !

Site Web | Plus de publications

Contributeur enthousiaste

5 comments for “BDD et SpecFlow pour des tests plus lisibles

  1. @guillaume_agile
    5 octobre 2012 at 17 h 00 min

    juste bravo pour cet article qui sort enfin des œillères du BDD pour les tests d’acceptance.
    puis je le citer dans ma présentation BDD que je donnerai à Agile Grenoble?
    @guillaume_agile

  2. 8 octobre 2012 at 16 h 06 min

    Merci pour ce retour ! Aucun problème pour citer ce billet lors d”une présentation, le contenu de ce blog est justement là pour être partagé !

  3. Didi
    12 décembre 2014 at 22 h 06 min

    Bonjour,
    est-possible d’utiliser les features en français? Si oui comment ça marche. merci

  4. Nicolas Montaudouin
    12 avril 2018 at 9 h 14 min

    Bonjour,

    En retard surement mais oui c’est possible d’écrire en français.
    Il faut juste modifier un peu le fichier de config en y indiquant la culture.

  5. Nicolas Montaudouin
    12 avril 2018 at 9 h 15 min

    Dans la balise specFlow
    language feature=”fr-FR” et bindingCulture name=”fr-FR”