Exercice : Ajouter des tests unitaires à votre application

Effectué

Dans cette unité, nous allons ajouter des tests unitaires à la build automatisée que nous avons créée avec Microsoft Azure Pipelines. Des bogues de régression se sont glissés dans le code de votre équipe au point d’endommager la fonctionnalité de filtrage du leaderboard. Plus précisément, le mode de jeu incorrect s’affiche en permanence.

L’image suivante illustre le problème. Quand un utilisateur sélectionne « Milky Way » pour n’afficher que les scores issus de cette carte de jeu, il obtient des résultats provenant d’autres cartes de jeux telles qu’Andromeda.

A screenshot of the leaderboard showing incorrect results: Andromeda galaxy scores show in the Milky Way galaxy listing.

L’équipe souhaite intercepter l’erreur avant d’atteindre les testeurs. Les tests unitaires sont un excellent moyen de tester automatiquement l’existence de bogues de régression.

Ajouter des tests unitaires à ce moment du processus donnera à l’équipe une longueur d’avance au moment où elle apportera des améliorations à l’application web Space Game. L’application utilise une base de données de documents pour stocker les meilleurs scores et les profils des joueurs. Pour l’instant, elle utilise des données de test locales. Ils envisagent de connecter l’application à une base de données en production.

Beaucoup de frameworks de tests unitaires sont disponibles pour les applications C#. Nous allons utiliser NUnit du fait de sa popularité auprès de la communauté.

Voici le test unitaire avec lequel vous travaillez :

[TestCase("Milky Way")]
[TestCase("Andromeda")]
[TestCase("Pinwheel")]
[TestCase("NGC 1300")]
[TestCase("Messier 82")]
public void FetchOnlyRequestedGameRegion(string gameRegion)
{
    const int PAGE = 0; // take the first page of results
    const int MAX_RESULTS = 10; // sample up to 10 results

    // Form the query predicate.
    // This expression selects all scores for the provided game region.
    Expression<Func<Score, bool>> queryPredicate = score => (score.GameRegion == gameRegion);

    // Fetch the scores.
    Task<IEnumerable<Score>> scoresTask = _scoreRepository.GetItemsAsync(
        queryPredicate, // the predicate defined above
        score => 1, // we don't care about the order
        PAGE,
        MAX_RESULTS
    );
    IEnumerable<Score> scores = scoresTask.Result;

    // Verify that each score's game region matches the provided game region.
    Assert.That(scores, Is.All.Matches<Score>(score => score.GameRegion == gameRegion));
}

Vous pouvez filtrer le leaderboard en fonction de n’importe quelle combinaison de type de jeu et de carte de jeu.

Ce test interroge le leaderboard pour obtenir les meilleurs scores et vérifie que chaque résultat correspond à la carte de jeu fournie.

Dans une méthode de test NUnit, TestCase fournit des données inline à utiliser pour tester cette méthode. NUnit appelle ici la méthode de test unitaire FetchOnlyRequestedGameRegion comme suit :

FetchOnlyRequestedGameRegion("Milky Way");
FetchOnlyRequestedGameRegion("Andromeda");
FetchOnlyRequestedGameRegion("Pinwheel");
FetchOnlyRequestedGameRegion("NGC 1300");
FetchOnlyRequestedGameRegion("Messier 82");

Notez l’appel à la méthode Assert.That à la fin du test. Une assertion est une condition ou une instruction que vous déclarez comme étant vraie. Si la condition s’avère fausse, cela peut indiquer un bogue dans votre code. NUnit exécute chaque méthode de test en utilisant les données inline que vous spécifiez et enregistre le résultat en tant que test défaillant ou réussi.

De nombreux frameworks de test unitaire fournissent des méthodes de vérification proches du langage naturel. Ces méthodes facilitent la lecture des tests et vous aident à mapper les tests aux spécifications de l’application.

Considérez l’assertion établie dans cet exemple :

Assert.That(scores, Is.All.Matches<Score>(score => score.GameRegion == gameRegion));

Vous pouvez lire cette ligne comme suit :

Déclarer que la région de jeu de chaque score retourné correspond à la région de jeu fournie.

Voici le processus à suivre :

  1. Récupérez une branche du dépôt GitHub qui contient les tests unitaires.
  2. Exécutez les tests localement pour vérifier qu’ils réussissent.
  3. Ajoutez des tâches à la configuration du pipeline pour exécuter les tests et collecter les résultats.
  4. Poussez (push) la branche vers votre dépôt GitHub.
  5. Regardez votre projet Azure Pipelines générer automatiquement l’application et exécuter les tests.

Récupérer (fetch) la branche à partir de GitHub

Ici, vous allez récupérer la branche unit-tests à partir de GitHub et extraire ou basculer vers cette branche.

Cette branche contient le projet Space Game que vous avez utilisé dans les modules précédents ainsi qu’une configuration Azure Pipelines de départ.

  1. Dans Visual Studio Code, ouvrez le terminal intégré.

  2. Exécutez les commandes git suivantes pour récupérer une branche nommée unit-tests auprès du dépôt Microsoft, puis passez à cette branche.

    git fetch upstream unit-tests
    git checkout -B unit-tests upstream/unit-tests
    

    Le format de cette commande vous permet d’obtenir le code de démarrage auprès du dépôt GitHub de Microsoft appelé upstream. Dans quelques instants, vous allez pousser cette branche vers votre dépôt GitHub appelé origin.

  3. Éventuellement, ouvrez le fichier azure-pipelines.yml dans Visual Studio Code,et familiarisez-vous avec la configuration initiale. La configuration ressemble à la configuration de base que vous avez créée dans le module Créer un pipeline de build avec Azure Pipelines. Elle repose uniquement sur la configuration Release de l’application.

Exécuter les tests localement

Il est judicieux d’exécuter tous les tests localement avant d’envoyer des tests au pipeline. C’est ce que vous allez faire ici.

  1. Dans Visual Studio Code, ouvrez le terminal intégré.

  2. Exécutez dotnet build pour générer chaque projet dans la solution.

    dotnet build --configuration Release
    
  3. Exécutez la commande dotnet test suivante pour exécuter les tests unitaires :

    dotnet test --configuration Release --no-build
    

    L’indicateur --no-build spécifie de ne pas générer le projet avant de l’exécuter. Vous n’avez pas besoin de générer le projet, car vous l’avez fait à l’étape précédente.

    Vous constaterez la réussite de chacun des cinq tests.

    Starting test execution, please wait...
    A total of 1 test files matched the specified pattern.
    
    Passed!  - Failed:     0, Passed:     5, Skipped:     0, Total:     5, Duration: 57 ms
    

    Dans cet exemple, l’exécution des tests prend environ une seconde.

    Notez qu’il y avait cinq tests au total. Bien que nous avons défini simplement une seule méthode de test, FetchOnlyRequestedGameRegion, ce test est exécuté cinq fois, à raison d’une fois par carte de jeu spécifiée dans les données TestCase inline.

  4. Exécutez les tests une deuxième fois. Cette fois-ci, spécifiez l’option --logger pour écrire les résultats dans un fichier journal.

    dotnet test Tailspin.SpaceGame.Web.Tests --configuration Release --no-build --logger trx
    

    Vous voyez d’après la sortie qu’un fichier TRX est créé dans le répertoire TestResults.

    Un fichier TRX est un document XML qui contient les résultats d’une série de tests. C’est un format courant pour les résultats de tests, car Visual Studio et les autres outils peuvent vous aider à visualiser les résultats.

    Plus tard, vous verrez comment Azure Pipelines peut vous aider à visualiser et à suivre vos résultats de test au fil du pipeline.

    Remarque

    Les fichiers TRX ne sont pas destinés à être inclus dans le contrôle de code source. Un fichier .gitignore permet de spécifier les fichiers temporaires et autres fichiers que Git doit ignorer. Le fichier .gitignore du projet est déjà configuré pour ignorer le contenu du répertoire TestResults.

  5. Éventuellement, dans Visual Studio Code, ouvrez DocumentDBRepository_GetItemsAsyncShould.cs à partir du dossier Tailspin.SpaceGame.Web.Tests et examinez le code de test. Même si vous n’êtes pas particulièrement intéressé par la génération d’applications .NET, le code de test peut s’avérer utile, car il ressemble au code que vous pouvez voir dans d’autres frameworks de tests unitaires.

Ajouter des tâches à la configuration du pipeline

Ici, vous allez configurer le pipeline de build pour qu’il exécute vos tests unitaires et collecte les résultats.

  1. Dans Visual Studio Code, modifiez azure-pipelines.yml comme suit :

    trigger:
    - '*'
    
    pool:
      vmImage: 'ubuntu-20.04'
      demands:
      - npm
    
    variables:
      buildConfiguration: 'Release'
      wwwrootDir: 'Tailspin.SpaceGame.Web/wwwroot'
      dotnetSdkVersion: '6.x'
    
    steps:
    - task: UseDotNet@2
      displayName: 'Use .NET SDK $(dotnetSdkVersion)'
      inputs:
        version: '$(dotnetSdkVersion)'
    
    - task: Npm@1
      displayName: 'Run npm install'
      inputs:
        verbose: false
    
    - script: './node_modules/.bin/node-sass $(wwwrootDir) --output $(wwwrootDir)'
      displayName: 'Compile Sass assets'
    
    - task: gulp@1
      displayName: 'Run gulp tasks'
    
    - script: 'echo "$(Build.DefinitionName), $(Build.BuildId), $(Build.BuildNumber)" > buildinfo.txt'
      displayName: 'Write build info'
      workingDirectory: $(wwwrootDir)
    
    - task: DotNetCoreCLI@2
      displayName: 'Restore project dependencies'
      inputs:
        command: 'restore'
        projects: '**/*.csproj'
    
    - task: DotNetCoreCLI@2
      displayName: 'Build the project - $(buildConfiguration)'
      inputs:
        command: 'build'
        arguments: '--no-restore --configuration $(buildConfiguration)'
        projects: '**/*.csproj'
    
    - task: DotNetCoreCLI@2
      displayName: 'Run unit tests - $(buildConfiguration)'
      inputs:
        command: 'test'
        arguments: '--no-build --configuration $(buildConfiguration)'
        publishTestResults: true
        projects: '**/*.Tests.csproj'
    
    - task: DotNetCoreCLI@2
      displayName: 'Publish the project - $(buildConfiguration)'
      inputs:
        command: 'publish'
        projects: '**/*.csproj'
        publishWebProjects: false
        arguments: '--no-build --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/$(buildConfiguration)'
        zipAfterPublish: true
    
    - task: PublishBuildArtifacts@1
      displayName: 'Publish Artifact: drop'
      condition: succeeded()
    

    Cette version introduit cette tâche de build DotNetCoreCLI@2.

    - task: DotNetCoreCLI@2
      displayName: 'Run unit tests - $(buildConfiguration)'
      inputs:
        command: 'test'
        arguments: '--no-build --configuration $(buildConfiguration)'
        publishTestResults: true
        projects: '**/*.Tests.csproj'
    

    Cette tâche de build exécute la commande dotnet test.

    Notez que cette tâche ne spécifie pas l’argument --logger trx que vous avez utilisé quand vous avez exécuté les tests manuellement. L’argument publishTestResults l’ajoute pour vous. Cet argument indique au pipeline de générer le fichier TRX dans un répertoire temporaire, accessible via la variable intégrée $(Agent.TempDirectory). Il publie également les résultats de la tâche dans le pipeline.

    L'argument projects spécifie tous les projets C# qui correspondent à "**/*.Tests.csproj". La partie "**" correspond à tous les répertoires, et la partie "*.Tests.csproj" correspond à tous les projets dont le nom de fichier se termine par ".Tests.csproj". La branche unit-tests contient un seul projet de test unitaire, Tailspin.SpaceGame.Web.Tests.csproj. En spécifiant un modèle, vous pouvez exécuter des projets de test supplémentaires sans avoir à modifier votre configuration de build.

Pousser (push) la branche vers GitHub

Ici, vous allez envoyer (push) vos modifications vers GitHub et regarder le pipeline s’exécuter. Rappelez-vous que vous êtes actuellement dans la branche unit-tests.

  1. Dans le terminal intégré, ajoutez azure-pipelines.yml à l’index, commitez les modifications, puis poussez la branche vers GitHub.

    git add azure-pipelines.yml
    git commit -m "Run and publish unit tests"
    git push origin unit-tests
    

Regarder Azure Pipelines exécuter les tests

Ici, vous voyez les tests s’exécuter dans le pipeline, puis vous visualisez les résultats à partir de Microsoft Azure Test Plans. Azure Test Plans fournit tous les outils dont vous avez besoin pour tester correctement vos applications. Vous pouvez créer et exécuter des plans de test manuels, générer des tests automatisés et collecter un feedback auprès des parties prenantes.

  1. Dans Azure Pipelines, suivez la build à travers chacune des étapes.

    Vous pouvez constater que la tâche Run unit tests - Release exécute les tests unitaires comme vous l’avez fait manuellement à partir de la ligne de commande.

    A screenshot of Azure Pipelines showing console output from running unit tests.

  2. Revenez au récapitulatif du pipeline.

  3. Passez à l’onglet Tests.

    Un récapitulatif de la série de tests apparaît. Tous les cinq tests ont réussi.

    A screenshot of Azure Pipelines showing the Tests tab with 5 total tests run and 100 percent passing.

  4. Dans Azure DevOps, sélectionnez Plans de test, puis sélectionnez Exécutions.

    A screenshot of Azure DevOps navigation menu with Test Plans section and Runs tab highlighted.

    Vous voyez les séries de tests les plus récentes, y compris celle que vous venez d’exécuter.

  5. Double-cliquez sur la série de tests la plus récente.

    Un récapitulatif des résultats apparaît.

    A screenshot of Azure DevOps test run results summary showing 5 passed tests.

    Dans cet exemple, tous les cinq tests ont réussi. Si un test échoue, vous pouvez accéder à la tâche de build pour obtenir des détails supplémentaires.

    Vous pouvez également télécharger le fichier TRX afin de l’examiner par le biais de Visual Studio ou d’un autre outil de visualisation.

Bien que vous n’ayez ajouté un seul test, il constitue un bon point de départ et résout le problème immédiat. L’équipe dispose désormais d’un emplacement pour ajouter d’autres tests et les exécuter à mesure qu’elle améliore son processus.

Fusionner votre branche dans main

Dans un scénario réel, si vous êtes satisfait des résultats, vous pouvez fusionner la branche unit-tests vers main, mais être plus concis, nous allons ignorer ce processus pour l’instant.