Qu’est-ce que l’ATDD ?

ATDD ou «Acceptance Test-Driven Development», est une méthodologie de développement logiciel qui implique une collaboration étroite entre les développeurs, les testeurs et les clients pour définir des tests d’acceptation avant de commencer le développement. L’ATDD permet de mieux cibler les fonctionnalités à développer et fournit une documentation claire du comportement du logiciel pour les développeurs et les testeurs.

Avec l’ATDD, les tests d’acceptance vérifient si le logiciel respecte bien les spécifications rédigées par les métiers.

 

Différence entre ATDD et BDD

En première approche, les deux techniques peuvent être faciles à confondre, et sont même utilisées de manière interchangeable. En effet, dans les deux cas, testeurs, développeurs, et personnes ayant la connaissance métier sont amenées à collaborer en amont dans l’écriture des tests. Les deux approches privilégient également l’écriture de tests au format « Given… When… Then »

Given : Dans cette partie, nous mettons les données en entrée de notre fonctionnalité

When : Ici, nous mettrons l’action qui va changer le système, la plupart du temps l’appel à notre fonction.

Then : Cette dernière partie contient le résultat attendu en sortie.

Cependant, en ATDD les tests d’acceptation sont écrits avant le code source, et le code est ensuite écrit pour passer ces tests. Là où en BDD, «Behavior-Driven Development», le comportement du code est décrit avant le code source, et le code est ensuite écrit pour correspondre à ce comportement. ATDD et BDD sont similaires, mais ATDD est orienté vers les tests et BDD est orienté vers le comportement.

 

L’ATDD avec Cucumber

Afin d’illustrer les précédents propos, nous allons concevoir un exemple à l’aide de Cucumber, outil qui permet aux développeurs de spécifier le comportement d’un logiciel sous forme de scénarios ou test lisibles par tous, puis exécute ces scénarios pour vérifier que le logiciel fonctionne comme prévu.

Exemple métier :

Notre client est une boutique de tissus, qui vend aussi bien aux particuliers qu’aux professionnels, et veut mettre en place une tarification en gros sur son site web, selon le modèle suivant :

– Distinction tarif TTC (nous partirons sur une hypothèse de 20 % pour les besoins de cet article)/Hors Taxe pour les clients Particuliers/Professionnels respectivement.

– A partir de 50m d’un tissu donné acheté, une réduction de 5 % est appliquée sur le total

– A partir de 250m, la réduction passe à 15 %

Modélisation de départ

En tant que développeur, je démarre mon projet de manière classique. Je mets notamment en place mes classes de base, comme suit :

Customer et Fabric sont les entités 

CustomerType est une énumération représentant la nature « particulier » ou « professionnel » du client

FabricPriceComputation est la classe de calcul de base. Celle-ci a déjà une méthode de calcul mise en place :

public BigDecimal computePrice(Customer customer, Fabric fabric, Integer length) {
           return BigDecimal.ZERO;
}

Evidemment, cette méthode ne répond pas à notre besoin. Mais ceci nous suffit pour le moment, nous allons pouvoir nous concentrer sur la mise en place de Cucumber pour ce projet.

Pour l’utilisation de Cucumber (nous allons partir sur les dernières versions, Cucumber 7 et Junit 5, ce dernier étant nécessaire pour la mise en place des tests), nous allons devoir mettre en place les dépendances dans le POM parent, comme suit.

 

Nous allons également créer un module de test spécifiquement pour les tests ATDD contenant une classe TestRunner dans le répertoire src/test/java, ainsi qu’un répertoire contenant les features, c’est-à-dire les fichiers .feature décrivant nos scénarios de tests d’acceptance, dans src/test/resources/cucumber-features.

A ce stade, le projet doit avoir la structure suivante :

La classe TestRunner a la forme suivante :

 

import org.junit.platform.suite.api.SelectClasspathResource;
import org.junit.platform.suite.api.Suite;


@Suite
@SelectClasspathResource("cucumber-features")
public class TestRunner { }

Elle contient une annotation @Suite, afin de pouvoir être détectée par Junit lors du lancement des tests, ainsi qu’une annotation @SelectClasspathResource(« cucumber-features ») afin de savoir où trouver les fichiers.

En coordination avec le client et l’équipe de test, nous écrivons maintenant une première feature en nous basant sur l’expression de besoin et sur la spécification, décrivant notre test dans le cas de l’achat par un particulier. Nous la nommerons test-individual.feature.

Feature: Facturation for individual
    An individual must pay their taxes


    Scenario: An individual buys satin
        Given satin costs 4 euros per meter
        When an individual buys 10 meters
        Then the cost should be 48.00 euros

Comme nous pouvons le voir, le scénario décrit suit la structure Gherkin, « Given…When…Then »

Après une première compilation, lançons les tests via la commande mvn test.

Les tests sont en erreur, cependant Cucumber nous propose un début de code afin de pouvoir les exécuter par la suite :

Il revient alors au développeur d’écrire une classe StepDefinitions qui contiendra cet exemple de code. Elle sera placée dans src/test/java du package cucumber, comme suit :

Relançons alors mvn test :

On note un changement du message d’erreur, notre classe de test est donc bien détectée. Ajoutons du code afin de pouvoir tester, ce qui implique notamment d’ajouter notre package Computation en dépendance du package cucumber.

public class StepDefinitions {


    private Fabric fabric;
    private Customer customer;
    private Integer length;

    private FabricPriceComputation computation= new FabricPriceComputation();


    @Given("satin costs {double} euros per meter")
    public void satin_costs_euros_per_meter(Double double1) {
        fabric = new Fabric();
        fabric.setName("satin");
        fabric.setPrice(BigDecimal.valueOf(double1));
    }
    @When("an individual buys {int} meters")
    public void an_individual_buys_meters(Integer int1) {
        customer =new Customer();
        customer.setCustomerType(CustomerType.INDIVIDUAL);
        length = int1;
    }


    


    @Then("the cost should be {double} euros")
    public void the_cost_should_be_euros(Double price) {
        Assertions.assertEquals(BigDecimal.valueOf(price).setScale(2), computation.computePrice(customer, fabric, length).setScale(2));
    }
}

En relançant mvn test, nous avons le résultat suivant

Ceci est normal, étant donné que le calcul du prix n’a pas encore été implémenté. Mettons donc uniquement le cas d’un client particulier en place pour le moment dans la méthode computePrice de la classe de calcul.

public BigDecimal computePrice(Customer customer, Fabric fabric, Integer length) {
        switch (customer.getCustomerType()) {
            case INDIVIDUAL:
                return fabric.getPrice().multiply(BigDecimal.valueOf(length)).multiply(BigDecimal.valueOf(1.20));
            case PROFESSIONAL:
                return null;
            default:
                return BigDecimal.ZERO;
        }
   } 

Mvn test nous indique que notre cas de test est maintenant OK lorsqu’on le lance :

Il nous faut donc maintenant gérer les cas relatifs aux professionnels. Nous retournons donc voir le client et les testeurs, afin d’écrire le cas de test par défaut, comme suit, dans un nouveau fichier, test-professional.feature.

Feature: Facturation for professional

    A professional doesn’t pay taxes

         Scenario: A professional buys satin

        Given satin costs 4 euros per meter

        When a professional buys 10 meters

        Then the cost should be 40.00 euros

Le test initial est KO, car nous devons ajouter une étape when dans la classe StepDefinitions.

 public class StepDefinitions {


    private Fabric fabric;
    private Customer customer;
    private Integer length;


    private FabricPriceComputation computation= new FabricPriceComputation();


    @Given("satin costs {double} euros per meter")
    public void satin_costs_euros_per_meter(Double double1) {
        fabric = new Fabric();
        fabric.setName("satin");
        fabric.setPrice(BigDecimal.valueOf(double1));
    }
    @When("an individual buys {int} meters")
    public void an_individual_buys_meters(Integer int1) {
        customer =new Customer();
        customer.setCustomerType(CustomerType.INDIVIDUAL);
        length = int1;
    }


    @When("a professional buys {int} meters")
    public void a_professional_buys_meters(Integer int1) {
        customer =new Customer();
        customer.setCustomerType(CustomerType.PROFESSIONAL);
        length = int1;
    }


    @Then("the cost should be {double} euros")
    public void the_cost_should_be_euros(Double price) {
        Assertions.assertEquals(BigDecimal.valueOf(price).setScale(2), computation.computePrice(customer, fabric, length).setScale(2));
    }
}

Après l’ajout de cette étape When, nous avons un échec, comme précédemment avec nos clients particuliers.

On implémente donc juste le nécessaire pour faire fonctionner le cas de base pour un professionnel :

public BigDecimal computePrice(Customer customer, Fabric fabric, Integer length) {
        switch (customer.getCustomerType()) {
            case INDIVIDUAL:
                return fabric.getPrice().multiply(BigDecimal.valueOf(length)).multiply(BigDecimal.valueOf(1.20));
            case PROFESSIONAL:
                return fabric.getPrice().multiply(BigDecimal.valueOf(length));
            default:
                return BigDecimal.ZERO;
        }
    }

Le test est enfin OK.

Cependant n’oublions pas les ristournes accordées aux professionnels pour une commande suffisante.

Rajoutons deux scénarios correspondants à ces réductions dans le fichier feature test-professional.feature, pour lesquels nous réitérerons le processus d’implémentation. Ici, l’intervention du client et de l’équipe de test est particulièrement pertinente. En effet, sur l’application de la réduction à plus de 250m de tissu, une ambiguïté pourrait apparaître lors de l’écriture du code. Les échanges entre les 3 intervenants lors de l’écriture des cas de tests permettent d’écarter ce type de problèmes plus tôt dans la conception et ainsi d’obtenir une nouvelle itération avec le fonctionnement attendu plus rapidement.

Feature: Facturation for professional
A professional doesn’t pay taxes

Scenario: A professional buys satin
Given satin costs 4 euros per meter
When a professional buys 10 meters
Then the cost should be 40.00 euros

Scenario: A professional buys more than 50 meters of satin
Given satin costs 4 euros per meter
When a professional buys 100 meters
Then the cost should be 380.00 euros

Scenario: A professional buys more than 250 meters of satin
Given satin costs 4 euros per meter
When a professional buys 300 meters
Then the cost should be 1020.00 euros

Et voici la méthode finale obtenue :

public BigDecimal computePrice(Customer customer, Fabric fabric, Integer length) {
        switch (customer.getCustomerType()) {
            case INDIVIDUAL:
                return fabric.getPrice().multiply(BigDecimal.valueOf(length)).multiply(BigDecimal.valueOf(1.20));
            case PROFESSIONAL:
                if(length >= 50 && length < 250){
                    return fabric.getPrice().multiply(BigDecimal.valueOf(length)).multiply(BigDecimal.valueOf(0.95));
                } else if(length >= 250){
                    return fabric.getPrice().multiply(BigDecimal.valueOf(length)).multiply(BigDecimal.valueOf(0.85));
                }
                return fabric.getPrice().multiply(BigDecimal.valueOf(length));
            default:
                return BigDecimal.ZERO;
        }
    }

Avec 4 tests OK :

Ci-dessous la structure finale du code pour cette application exemple, pour rappel :

Conclusion

Grâce à l’implémentation de l’ATDD par Cucumber, les personnes ayant une connaissance métier plus poussée peuvent contribuer de manière plus aisée au développement, en préparant les fichiers features pour guider les développeurs dans leur écriture. Ceci est particulièrement intéressant sur des projets back-end purs type calculateurs, par exemple, où il est parfois moins évident de dérouler un cahier de tests.

Le code peut être trouvé en intégralité à l’adresse suivante : https://github.com/hmidassi/fitnesseDevoteam


Bibliographie : https://dannorth.net/introducing-bdd/

Partager
Faire suivre