Le lab n°4 - La Démarche méthodologique BDD de A à Z

écrit par julien.royer publié le mardi 21 novembre 2017
Dernière article de notre série consacrée aux méthodologies de tests. Je vais essayer ici de résumer la méthodologie BDD que l'on à suivi pour la phase de calcul des résultats de vote pour notre projet R&D.

Étape 1

Le Product Owner (PO) commence par rédiger une User Story (communément appelée US) sur le modèle : En tant que [utilisateur] je veux [faire une action] pour [atteindre un objectif] Un exemple concret pour notre système de calcul de vote serait : En tant que client de l'API à la clôture d'un scrutin majoritaire Je veux calculer le résultat du scrutin Pour obtenir le vainqueur du vote  

Étape 2

Le Product Owner continue sur la rédaction des critères d'acceptation de l’User story. Chaque critère d’acceptation sera remplacé par un test d’acceptance dans notre algorithme de test. Pour notre user story par exemple les critères définit sont les suivants:
Cas nominal
[] Le scrutin doit être clôturé
[] On obtient pour chaque option (candidat) le pourcentage de choix "oui"
[] Si un candidat obtient > 50% (la majorité des voix), il est vainqueur
[] Si aucun candidat n'a plus de 50%, alors on garde les 2 candidats correspondants aux meilleurs pourcentages et il n'y a pas de vainqueur (le système créera un tour de plus)
[] Si on est sur le dernier tour d'un vote, on précise le vainqueur, qui est l'option (candidat) ayant le pourcentage d'acte (choix) "oui" le plus élevé
[] Il ne peut y avoir que 2 tours maximum
 

Étape 3

Nous rediscutons ensuite en équipe du contenu de l’user story, notamment des critères d'acceptation déjà définit. Si nécessaire, nous les complétons. Nous commençons à déterminer ensemble des cas d’utilisation voire des scénarii possibles et réfléchissons aux cas plus spécifiques. Dans notre cas, cela nous a permis d’ajouter d'autres critères d'acceptation particuliers sur la gestion de l’égalité et du vote blanc par exemple :  
Gestion de l'égalité
[] Si on a une égalité sur un dernier tour, on on ne peut pas déterminer de vainqueur.
[] Si on n’est pas sur le dernier tour, et que l’égalité est sur les 2e et 3e options, on affiche les résultats sans désignation de qualifiés/vainqueur. On proposera ensuite de créer un nouveau scrutin.

Gestion du vote blanc
[] Si le vote blanc arrive dans les qualifiés on ne peut pas déterminer de vainqueur
 

Étape 4

L’équipe (client et devs) traduit ensuite (dans un fichier spécifique que l’on appelle « feature ») les premiers cas d’utilisation. Nous utilisons la méthode de spécification par l’exemple. Pour cela, on utilise le langage naturel Gherkin en passant par un outil de test automatisé SpecFlow Concrètement, nous définissons un cas de test via la syntaxe particulière suivante : Given (Etant donné) [un contexte], When (Lorsque) [l'utilisateur effectue des actions], Then (Alors) [on constate une conséquence]. Un exemple simple de scénario écrit pour le scrutin majoritaire :
Feature: ScrutinMajoritaire
	En tant que client de l'API à la clôture d'un scrutin majoritaire
	Je souhaite calculer le résultat du scrutin
	Pour obtenir le vainqueur du vote

   Scenario: Scrutin majoritaire un électeur et un vainqueur
	Given les options suivantes
	| Nom        |
	| candidat 1 |
	| candidat 2 |
	And le vote d'un electeur est "candidat 1"
	When je clôture le scrutin majoritaire
	Then "candidat 1" est désigné comme vainqueur
	And le résultat est valide
	And j'obtiens le résultat suivant
	| Option     | Nombre de vote | pourcentage |
	| candidat 1 | 1              | 100         |
	| candidat 2 | 0              | 0           |
 

Étape 5

Avec SpecFlow, nous serons en mesure de transposer ce premier scénario de l'anglais vers le langage C#. Les développeurs vont générer automatiquement à partir du fichier « feature » les tests d’acceptances associés à leurs scénarios. Pour générer le fichier de steps associé à vos scénarii : Clic droit « Generate Steps Definitions » : une nouvelle classe « ScrutinMajoritaireSteps.cs » est générée.

Étape 6

Dans la classe « ScrutinMajoritaireSteps.cs », nous complètons le squelette des tests : nous ajoutons les éléments de contexte pour le Given, le calcul du test pour le When et enfin le résultat attendu pour le Then. Pour le scrutin majoritaire notre « steps » se traduit par :
    [Binding]
    public class ScrutinMajoritaireSteps
    {
        public ResultatModel Resultat
        {
            get => ScenarioContext.Current["resultat"] as ResultatModel;
            set => ScenarioContext.Current["resultat"] = value;
        }

        [Given(@"le vote d'un electeur est ""(.*)""")]
        public void GivenLeVoteDUnElecteurEst(string vote)
        {
            var options = SharedSteps.GetOptionsParNom();
            options[vote].Suffrages.Add(new Model.Business.SuffrageModel { Valeur = 1 });
        }

        [When(@"je clôture le scrutin majoritaire")]
        public void WhenJeClotureLeScrutinMajoritaire()
        {
            List<ChoixModel> choixPossibles = new List<ChoixModel>
                {
                    new ChoixModel
                    {
                        Id = 1,
                         Nom = "Choisi",
                         Valeur = 1
                    },
                    new ChoixModel
                    {
                        Id = 2,
                         Nom = "Non choisi",
                         Valeur = 0
                    }
                };
            var scrutin = new ScrutinModel
            {
                Options = SharedSteps.GetOptionsParNom().Values.ToList(),
                ChoixPossibles = choixPossibles
            };

            var calculateur = new CalculateurScrutinMajoritaire();
            Resultat = calculateur.CalculerResultat(scrutin);
        }

        [Then(@"""(.*)"" est désigné comme vainqueur")]
        public void ThenEstDesigneCommeVainqueur(string vainqueur)
        {
            Check.That(Resultat.ResultatsIndividuels.Single(ri => ri.IsVainqueur).Option.Nom).IsEqualTo(vainqueur);
        }

        [Then(@"j'obtiens le résultat suivant")]
        public void ThenJObtiensLeResultatSuivant(Table table)
        {
            foreach (var row in table.Rows)
            {
                var option = row["Option"];
                var nombreDeVote = int.Parse(row["Nombre de vote"]);
                var pourcentage = decimal.Parse(row["pourcentage"]);

                var resultatOption = Resultat.ResultatsIndividuels.Single(ri => ri.Option.Nom == option);
                var score = resultatOption.Scores.FirstOrDefault(s => s.Choix.Valeur == 1);

                Check.That(score?.Pourcentage ?? 0m).IsEqualTo(pourcentage);
                Check.That(score?.Votes ?? 0).IsEqualTo(nombreDeVote);
            }
        }

        [Then(@"il n'y a pas de vainqueur")]
        public void ThenIlNYAPasDeVainqueur()
        {
            Check.That(Resultat.ResultatsIndividuels.Any(ri => ri.IsVainqueur)).IsFalse();
        }

        [Then(@"le résultat est valide")]
        public void ThenLeResultatEstValide()
        {
            Check.That(Resultat.IsResultatValide).IsTrue();
        }

        [Then(@"le résultat n'est pas valide")]
        public void ThenLeResultatNEstPasValide()
        {
            Check.That(Resultat.IsResultatValide).IsFalse();
        }
    }
Remarque : on commence souvent par écrire le  « Given » voire le « Then » pour terminer par le « When » qui constitue ici le cœur de notre cas : le calcul du vote. On peut d’ailleurs imaginer faire un « mock » du « Calculateur » pour se concentrer dans un premier temps sur l'implémentation de notre architecture de tests.  

Étape 7

Il ne nous reste plus qu'a compiler pour générer automatiquement nos scénarii et les voir apparaître dans la liste de nos tests unitaires. A la compilation, une nouvelle classe ScrutinMajoritaire.feature.cs est ajoutée au projet. Nous ne la touchons pas s'il s'agit d'une classe auto-générée par SpecFlow.  

Étape 8

Nous complétons ces tests fonctionnels par des tests unitaires. Ces tests sont interdépendants de la base de données et utilise des objets métiers pour fonctionner. C’est aussi ici que l’on retrouve des tests plus techniques (test de nullité, conditions spécifiques à remplir…) On obtient une classe « CalculateurScrutinMajoritaireShould » contenant les tests unitaires suivants :
public class CalculateurScrutinMajoritaireShould
    {
        private CalculateurScrutinMajoritaire _calculateur;

        [TestInitialize]
        public void Init()
        {
            _calculateur = new CalculateurScrutinMajoritaire();
        }

        [TestMethod]
        public void ThrowExceptionNullScrutin()
        {
            Check.ThatCode(() => _calculateur.CalculerResultat(null))
                .Throws<VoteInException>()
                .WithMessage("Impossible de calculer les résultats : le scrutin est null");
        }

        [TestMethod]
        public void ReturnOrdereredResults()
        {
            var scrutinBusiness = new ScrutinModel
            {
                DateCloture = new DateTime(2017, 11, 07),
                Options = new List<OptionsModel>
                {
                    new OptionsModel
                    {
                        Id = 23,
                        Nom = "Candidat 1",
                        Suffrages = new List<SuffrageModel>
                        {
                            new SuffrageModel
                            {
                                Valeur = 1
                            },
                            new SuffrageModel
                            {
                                Valeur = 1
                            },
                            new SuffrageModel
                            {
                                Valeur = 1
                            }
                        }
                    },
                    new OptionsModel
                    {
                        Id = 12,
                        Nom = "Candidat 2",
                        Suffrages = new List<SuffrageModel>
                        {
                            new SuffrageModel
                            {
                                Valeur = 1
                            }
                        }
                    },
                    new OptionsModel
                    {
                        Id = 45,
                        Nom = "Candidat 3",
                        Suffrages = new List<SuffrageModel>
                        {
                            new SuffrageModel
                            {
                                Valeur = 1
                            },
                            new SuffrageModel
                            {
                                Valeur = 1
                            }
                        }
                    }
                }
            };

            var resultat = _calculateur.CalculerResultat(scrutinBusiness) as ResultatScrutinMajoritaireModel;

            Check.That(resultat.ResultatsIndividuels).HasSize(3);

            var firstOption = resultat.ResultatsIndividuels.First();
            Check.That(firstOption.Option.Id).IsEqualTo(23);
            Check.That(firstOption.Pourcentage).IsEqualTo(50);

            var secondOption = resultat.ResultatsIndividuels.ElementAt(1);
            Check.That(secondOption.Option.Id).IsEqualTo(45);
            Check.That(secondOption.Pourcentage).IsEqualTo(100 / 3m);

            var thirdOption = resultat.ResultatsIndividuels.Last();
            Check.That(thirdOption.Option.Id).IsEqualTo(12);
            Check.That(thirdOption.Pourcentage).IsEqualTo(100 / 6m);
        }

        [TestMethod]
        public void IdentifyWinnerOnLatestTurn()
        {
            var scrutinBusiness = new ScrutinModel
            {
                DateCloture = new DateTime(2017, 11, 07),
                IsDernierTour = true,
                Options = new List<OptionsModel>
                {
                    new OptionsModel
                    {
                        Id = 23,
                        Nom = "Candidat 1",
                        Suffrages = new List<SuffrageModel>
                        {
                            new SuffrageModel
                            {
                                Valeur = 1
                            }
                        }
                    },
                    new OptionsModel
                    {
                        Id = 12,
                        Nom = "Candidat 2",
                        Suffrages = new List<SuffrageModel>
                        {
                            new SuffrageModel
                            {
                                Valeur = 1
                            },
                            new SuffrageModel
                            {
                                Valeur = 1
                            }
                        }
                    }
                }
            };

            var resultat = _calculateur.CalculerResultat(scrutinBusiness) as ResultatScrutinMajoritaireModel;

            var gagant = resultat.ResultatsIndividuels.First();
            var perdant = resultat.ResultatsIndividuels.Last();

            Check.That(gagant.Option.Id).IsEqualTo(12);
            Check.That(gagant.Option).IsEqualTo(resultat.Vainqueur);
            Check.That(perdant.Option).Not.IsEqualTo(resultat.Vainqueur);
        }

        [TestMethod]
        public void EndedVoteInCaseOfEquality()
        {
            var scrutinBusiness = new ScrutinModel
            {
                DateCloture = new DateTime(2017, 11, 07),
                Options = new List<OptionsModel>
                {
                    new OptionsModel
                    {
                        Id = 23,
                        Nom = "Candidat 1",
                        Suffrages = new List<SuffrageModel>
                        {
                            new SuffrageModel
                            {
                                Valeur = 1
                            }
                        }
                    },
                    new OptionsModel
                    {
                        Id = 12,
                        Nom = "Candidat 2",
                        Suffrages = new List<SuffrageModel>
                        {
                            new SuffrageModel
                            {
                                Valeur = 1
                            }
                        }
                    }
                }
            };

            var resultat = _calculateur.CalculerResultat(scrutinBusiness);

            Check.That(resultat.IsResultatValide).IsFalse();
        }

        [TestMethod]
        public void EndedVoteWhenBlankVoteWins()
        {
            var scrutinBusiness = new ScrutinModel
            {
                DateCloture = new DateTime(2017, 11, 07),
                IsDernierTour = true,
                IsVoteBlancPrisEnCompte = true,
                Options = new List<OptionsModel>
                {
                    new OptionsModel
                    {
                        Id = 23,
                        Nom = "Vote blanc",
                        IsVoteBlanc = true,
                        Suffrages = new List<SuffrageModel>
                        {
                            new SuffrageModel
                            {
                                Valeur = 1
                            },
                            new SuffrageModel
                            {
                                Valeur = 1
                            }
                        }
                    },
                    new OptionsModel
                    {
                        Id = 12,
                        Nom = "Candidat 2",
                        Suffrages = new List<SuffrageModel>
                        {
                            new SuffrageModel
                            {
                                Valeur = 1
                            }
                        }
                    }
                }
            };

            var resultat = _calculateur.CalculerResultat(scrutinBusiness);

            Check.That(resultat.IsResultatValide).IsFalse();
        }

        [TestMethod]
        public void IdentifyWinnerWhenBlankVoteIsQualifiedAmongTwoCandidates()
        {
            var scrutinBusiness = new ScrutinModel
            {
                DateCloture = new DateTime(2017, 11, 07),
                IsDernierTour = false,
                IsVoteBlancPrisEnCompte = true,
                Options = new List<OptionsModel>
                {
                    new OptionsModel
                    {
                        Id = 23,
                        Nom = "Vote blanc",
                        IsVoteBlanc = true,
                        Suffrages = new List<SuffrageModel>
                        {
                            new SuffrageModel
                            {
                                Valeur = 1
                            }
                        }
                    },
                    new OptionsModel
                    {
                        Id = 12,
                        Nom = "Candidat 2",
                        Suffrages = new List<SuffrageModel>
                        {
                            new SuffrageModel
                            {
                                Valeur = 1
                            },
                            new SuffrageModel
                            {
                                Valeur = 1
                            }
                        }
                    }
                }
            };

            var resultat = _calculateur.CalculerResultat(scrutinBusiness);

            Check.That(resultat.IsResultatValide).IsTrue();
            Check.That(resultat.Vainqueur.Id).IsEqualTo(12);
        }

        [TestMethod]
        public void EndedVoteWhenBlankVoteIsQualified()
        {
            var scrutinBusiness = new ScrutinModel
            {
                DateCloture = new DateTime(2017, 11, 07),
                IsDernierTour = false,
                IsVoteBlancPrisEnCompte = true,
                Options = new List<OptionsModel>
                {
                    new OptionsModel
                    {
                        Id = 23,
                        Nom = "Vote blanc",
                        IsVoteBlanc = true,
                        Suffrages = new List<SuffrageModel>
                        {
                            new SuffrageModel
                            {
                                Valeur = 1
                            },
                            new SuffrageModel
                            {
                                Valeur = 1
                            }
                        }
                    },
                    new OptionsModel
                    {
                        Id = 65,
                        Nom = "Candidat 1",
                        Suffrages = new List<SuffrageModel>
                        {
                            new SuffrageModel
                            {
                                Valeur = 1
                            }
                        }
                    },
                    new OptionsModel
                    {
                        Id = 12,
                        Nom = "Candidat 2",
                        Suffrages = new List<SuffrageModel>
                        {
                            new SuffrageModel
                            {
                                Valeur = 1
                            },
                            new SuffrageModel
                            {
                                Valeur = 1
                            },
                            new SuffrageModel
                            {
                                Valeur = 1
                            }
                        }
                    }
                }
            };

            var resultat = _calculateur.CalculerResultat(scrutinBusiness);

            Check.That(resultat.IsResultatValide).IsFalse();
        }
    }
 

Étape 9

Nous terminons par écrire l'implémentation du code qui calcule le résultat de notre vote Dans notre cas, l’implémentation finale du calcul de résultat dans la couche Business est la suivante :
 public class CalculateurScrutinMajoritaire : ICalculateur
    {
        private const int ValeurChoisi = 1;

        public IResultatModel CalculerResultat(ScrutinModel scrutin)
        {
            if (scrutin == null)
            {
                throw new VoteInException("Impossible de calculer les résultats : le scrutin est null");
            }
            decimal nbVoteTotal = scrutin.Options.SelectMany(o => o.Suffrages).Count(s => s.Valeur == ValeurChoisi);

            var resultat = new ResultatScrutinMajoritaireModel();

            resultat.ResultatsIndividuels = scrutin.Options
                .Select(o => CreateResultatIndividuel(nbVoteTotal, o))
                .OrderByDescending(ri => ri.Votes)
                .ToList();

            resultat.IsResultatValide = IsResultatValide(resultat, scrutin);

            var first = resultat.ResultatsIndividuels.FirstOrDefault();
            if (resultat.IsResultatValide && first != null && (first.Pourcentage > 50m || scrutin.IsDernierTour))
            {
                resultat.Vainqueur = first.Option;
            }
            return resultat;
        }

        private static ResultatIndividuelScrutinMajoritaireModel CreateResultatIndividuel(decimal nbVoteTotal, OptionsModel option)
        {
            var nbVote = option.Suffrages.Where(s => s.Valeur == ValeurChoisi).Count();
            return new ResultatIndividuelScrutinMajoritaireModel
            {
                Option = option,
                Votes = nbVote,
                Pourcentage = 100 * nbVote / nbVoteTotal
            };
        }

        private static bool IsResultatValide(ResultatScrutinMajoritaireModel resultat, ScrutinModel scrutin)
        {
            var first = resultat.ResultatsIndividuels.FirstOrDefault();
            var second = resultat.ResultatsIndividuels.ElementAtOrDefault(1);
            var third = resultat.ResultatsIndividuels.ElementAtOrDefault(2);

            if (first == null || first.Option.IsVoteBlanc)
            {
                return false;
            }
            if ((resultat.ResultatsIndividuels.Count == 2 || scrutin.IsDernierTour) && first.Votes == second.Votes)
            {
                return false;
            }

            bool isPremierTourSansVainqueur = !scrutin.IsDernierTour && first.Pourcentage <= 50m;
            if (isPremierTourSansVainqueur && (second?.Votes == third?.Votes || second.Option.IsVoteBlanc))
            {
                return false;
            }
            return true;
        }
    }