Écrire les tests de l’interface utilisateur

Effectué

Dans cette section, vous aidez Andy et Amita à écrire des tests Selenium qui vérifient les comportements de l’interface utilisateur décrits par Amita.

Amita exécute normalement des tests sur Chrome, Firefox et Microsoft Edge. Vous procédez de la même façon ici. L’agent hébergé par Microsoft que vous allez utiliser est préconfiguré pour fonctionner avec chacun de ces navigateurs.

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

Dans cette section, vous allez récupérer (fetch) la branche selenium à partir de GitHub. Vous effectuez ensuite un basculement vers cette branche. Le contenu de la branche vous aide à suivre les tests écrits par Andy et Amita.

Cette branche contient le projet de Space Game que vous avez utilisé dans les modules précédents. Elle contient également une configuration Azure Pipelines qui vous permet de démarrer.

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

  2. Pour télécharger une branche nommée selenium à partir du référentiel Microsoft, basculez vers cette branche, puis exécutez les commandes git fetch et git checkout suivantes :

    git fetch upstream selenium
    git checkout -B selenium upstream/selenium
    

    Conseil

    Si vous avez suivi le test manuel d’Amita dans l’unité précédente, vous avez peut-être déjà exécuté ces commandes. Si vous les avez déjà exécutés dans l’unité précédente, vous pouvez néanmoins les réexécuter maintenant.

    N’oubliez pas que l’expression en amont fait référence au référentiel Microsoft GitHub. La configuration Git de votre projet comprend le dépôt distant en amont, car vous avez configuré cette relation. Vous l’avez configurée quand vous avez dupliqué (fork) le projet à partir du référentiel Microsoft et l’avez cloné localement.

    Dans quelques instants, vous pousserez (push) cette branche vers votre dépôt GitHub appelé origin.

  3. Si vous le souhaitez, dans Visual Studio Code, ouvrez le fichier azure-pipelines.yml. Familiarisez-vous avec la configuration initiale.

    La configuration ressemble à celles que vous avez créées dans les modules précédents de ce parcours d’apprentissage. Elle repose uniquement sur la configuration Release de l’application. Par souci de concision, la configuration omet également les déclencheurs, les approbations manuelles et les tests que vous avez configurés dans les modules précédents.

    Notes

    Pour une configuration plus robuste, les branches qui participent au processus de génération pourraient être spécifiées. Par exemple, pour faciliter la vérification de la qualité du code, vous pouvez effectuer des tests unitaires chaque fois que vous poussez une modification dans une branche. Vous pouvez aussi déployer l’application sur un environnement qui effectue des tests plus exhaustifs. Toutefois, vous ne procédez à ce déploiement que lorsque vous avez une requête de tirage (pull request), lorsque vous disposez d'une version Release Candidate, ou lorsque vous fusionnez du code sur la base de type primaire.

    Pour plus d’informations, consultez Implémenter un workflow de code dans votre pipeline de build à l’aide de Git et GitHub et Déclencheurs de pipeline de build.

Écrire le code de test unitaire

Amita a hâte d’apprendre à écrire du code qui contrôle le navigateur web.

Elle va travailler avec Andy pour écrire les tests Selenium. Andy a déjà configuré un projet NUnit vide. Tout au long du processus, ils se réfèrent à la documentation Selenium, à quelques tutoriels en ligne et aux notes qu’ils ont prises quand Amita a exécuté les tests manuellement. À la fin de ce module, vous trouverez plus de ressources pour vous aider tout au long de ce processus.

Passons en revue le processus utilisé par Andy et Amita pour écrire leurs tests. Vous pouvez suivre la procédure en ouvrant HomePageTest.cs dans le répertoire Tailspin.SpaceGame.Web.UITests dans Visual Studio Code.

Définir la classe HomePageTest

Andy : La première tâche consiste à définir notre classe de test. Nous pouvons choisir de suivre l’une des conventions d’affectation de noms. Appelons notre classe HomePageTest. Dans cette classe, nous allons placer tous nos tests en rapport avec la page d’accueil.

Andy ajoute ce code à HomePageTest.cs :

public class HomePageTest
{
}

Andy : Nous devons marquer cette classe comme étant public afin qu’elle soit disponible pour l’infrastructure NUnit.

Ajouter la variable membre IWebDriver

Andy : Nous avons ensuite besoin d’une variable membre IWebDriver. IWebDriver est l’interface de programmation que vous utilisez pour lancer un navigateur web et interagir avec le contenu de la page web.

Amita : J’ai entendu parler des interfaces en programmation. Peux-tu m’en dire plus ?

Andy : Imagine une interface comme une spécification ou un blueprint qui définit la façon dont un composant doit se comporter. Une interface fournit les méthodes, ou comportements, de ce composant. Mais l’interface ne fournit aucun des détails sous-jacents. Tu crées, ou d’autres personnes créent, une ou plusieurs classes concrètes qui implémentent cette interface. Selenium fournit les classes concrètes dont nous avons besoin.

Ce diagramme illustre l’interface IWebDriver et quelques-unes des classes qui implémentent cette interface :

Diagram of the IWebDriver interface, its methods, and concrete classes.

Le diagramme montre trois des méthodes fournies par IWebDriver : Navigate, FindElement et Close.

Les trois classes présentées ici, ChromeDriver, FirefoxDriver et EdgeDriver implémentent IWebDriver et ses méthodes. Il existe d’autres classes, telles que SafariDriver, qui implémentent également IWebDriver. Chaque classe de pilote peut contrôler le navigateur web qu’il représente.

Andy ajoute une variable membre nommée driver à la classe HomePageTest, comme dans ce code :

public class HomePageTest
{
    private IWebDriver driver;
}

Définir les fixtures de test

Andy : Nous souhaitons exécuter l’ensemble complet des tests sur Chrome, Firefox et Edge. Dans NUnit, nous pouvons utiliser des fixtures de test pour exécuter l’ensemble complet des tests plusieurs fois, une fois pour chaque navigateur que nous souhaitons tester.

Dans NUnit, vous utilisez l’attribut TestFixture pour définir vos fixtures de test. Andy ajoute ces trois fixtures de test à la classe HomePageTest :

[TestFixture("Chrome")]
[TestFixture("Firefox")]
[TestFixture("Edge")]
public class HomePageTest
{
    private IWebDriver driver;
}

Andy : Ensuite, nous devons définir un constructeur pour notre classe de test. Le constructeur est appelé quand NUnit crée une instance de cette classe. En tant qu’argument, le constructeur prend la chaîne que nous avons jointe à nos fixtures de test. Voici à quoi ressemble le test :

[TestFixture("Chrome")]
[TestFixture("Firefox")]
[TestFixture("Edge")]
public class HomePageTest
{
    private string browser;
    private IWebDriver driver;

    public HomePageTest(string browser)
    {
        this.browser = browser;
    }
}

Andy : Nous avons ajouté la variable membre browser pour pouvoir utiliser le nom du navigateur actuel dans notre code d’installation. Écrivons maintenant le code d’installation.

Définir la méthode d’installation

Andy : Nous devons ensuite affecter notre variable membre IWebDriver à une instance de classe qui implémente cette interface pour le navigateur sur lequel nous effectuons le test. Les classes ChromeDriver, FirefoxDriver et EdgeDriver implémentent respectivement cette interface pour Chrome, Firefox et Edge.

Créons une méthode, nommée Setup, qui définit la variable driver. Nous utilisons l’attribut OneTimeSetUp pour indiquer à NUnit d’exécuter cette méthode une fois par fixture de test.

[OneTimeSetUp]
public void Setup()
{
}

Dans la méthode Setup, nous pouvons utiliser une instruction switch pour affecter la variable membre driver à l’implémentation concrète appropriée, en fonction du nom du navigateur. Ajoutons maintenant ce code.

// Create the driver for the current browser.
switch(browser)
{
    case "Chrome":
    driver = new ChromeDriver(
        Environment.GetEnvironmentVariable("ChromeWebDriver")
    );
    break;
    case "Firefox":
    driver = new FirefoxDriver(
        Environment.GetEnvironmentVariable("GeckoWebDriver")
    );
    break;
    case "Edge":
    driver = new EdgeDriver(
        Environment.GetEnvironmentVariable("EdgeWebDriver"),
        new EdgeOptions
        {
            UseChromium = true
        }
    );
    break;
    default:
    throw new ArgumentException($"'{browser}': Unknown browser");
}

Le constructeur de chaque classe de pilote prend un chemin facultatif vers le logiciel du pilote dont Selenium a besoin pour contrôler le navigateur web. Plus tard, nous aborderons le rôle des variables d’environnement montrées ici.

Dans cet exemple, le constructeur EdgeDriver requiert également des options supplémentaires pour spécifier que nous souhaitons utiliser la version Chromium d’Edge.

Définir les méthodes d’assistance

Andy : Je sais que nous devrons répéter deux actions au cours des tests :

  • La recherche d’éléments sur la page, tels que les liens sur lesquels nous cliquons, et les fenêtres modales auxquelles nous nous attendons
  • Le clics sur les éléments de la page, tels que les liens qui révèlent les fenêtres modales, et le bouton qui ferme chaque fenêtre modale

Écrivons deux méthodes d’assistance, une pour chaque action. Nous allons commencer par la méthode qui permet de rechercher un élément sur la page.

Écrire la méthode d’assistance FindElement

Quand vous localisez un élément sur la page, c’est généralement en réponse à un autre événement, tel que le chargement de la page ou des informations saisies par l’utilisateur. Selenium fournit la classe WebDriverWait, ce qui vous autorise à attendre qu’une condition soit vraie. Si la condition n’est pas vraie dans la période donnée, WebDriverWait lève une exception ou une erreur. Nous pouvons utiliser la classe WebDriverWait pour attendre l’affichage d’un élément donné et être prêts à recevoir une entrée utilisateur.

Pour localiser un élément dans la page, utilisez la classe By. La classe By fournit des méthodes qui vous permettent de rechercher un élément par son nom, par son nom de classe CSS, par l’étiquette HTML, ou dans notre cas, par son attribut id.

Andy et Amita codent la méthode d’assistance FindElement. Elle se présente comme ce code :

private IWebElement FindElement(By locator, IWebElement parent = null, int timeoutSeconds = 10)
{
    // WebDriverWait enables us to wait for the specified condition to be true
    // within a given time period.
    return new WebDriverWait(driver, TimeSpan.FromSeconds(timeoutSeconds))
        .Until(c => {
            IWebElement element = null;
            // If a parent was provided, find its child element.
            if (parent != null)
            {
                element = parent.FindElement(locator);
            }
            // Otherwise, locate the element from the root of the DOM.
            else
            {
                element = driver.FindElement(locator);
            }
            // Return true after the element is displayed and is able to receive user input.
            return (element != null && element.Displayed && element.Enabled) ? element : null;
        });
}

Écrire la méthode d’assistance ClickElement

Andy : Nous allons maintenant écrire une méthode d’assistance qui clique sur les liens. Selenium offre plusieurs moyens d’écrire cette méthode. L’une d’entre elles est l’interface IJavaScriptExecutor. Celle-ci nous permet de cliquer sur des liens programmatiquement à l’aide de JavaScript. Cette approche fonctionne bien, car elle peut cliquer sur des liens sans les faire défiler d’abord dans l’affichage.

ChromeDriver, FirefoxDriver et EdgeDriver implémentent IJavaScriptExecutor. Nous devons effectuer un cast du pilote vers cette interface, puis appeler ExecuteScript pour exécuter la méthode JavaScript click() sur l’objet HTML sous-jacent.

Andy et Amita codent la méthode d’assistance ClickElement. Elle se présente comme ce code :

private void ClickElement(IWebElement element)
{
    // We expect the driver to implement IJavaScriptExecutor.
    // IJavaScriptExecutor enables us to execute JavaScript code during the tests.
    IJavaScriptExecutor js = driver as IJavaScriptExecutor;

    // Through JavaScript, run the click() method on the underlying HTML object.
    js.ExecuteScript("arguments[0].click();", element);
}

Amita : J’aime bien l’idée d’ajouter ces méthodes d’assistance. Elles semblent être suffisamment générales pour être utilisées dans pratiquement n’importe quel test. Nous pouvons ajouter des méthodes d’assistance supplémentaires plus tard si nous en avons besoin.

Définir la méthode de test

Andy : Nous sommes maintenant prêts à définir la méthode de test. En nous basant sur les tests manuels que nous avons exécutés précédemment, appelons cette méthode ClickLinkById_ShouldDisplayModalById. Une bonne pratique consiste à attribuer des noms descriptifs aux méthodes de test qui définissent précisément ce que le test réalise. Ici, nous voulons cliquer sur un lien, défini par son attribut id. Ensuite, nous souhaitons vérifier que la fenêtre modale appropriée s’affiche, en utilisant également son attribut id.

Andy ajoute un code de démarrage pour la méthode de test :

public void ClickLinkById_ShouldDisplayModalById(string linkId, string modalId)
{
}

Andy : Avant d’ajouter davantage de code, nous allons définir comment ce test doit réagir.

Amita : Je peux m’occuper de cette partie. Nous souhaitons :

  1. Localiser le lien par son attribut id, puis cliquer sur le lien.
  2. Localiser la fenêtre modale qui en résulte.
  3. Fermer la fenêtre modale.
  4. Vérifier que la fenêtre modale s’est bien affichée.

Andy : Parfait. Nous devrons également gérer d’autres éléments. Par exemple, nous devons ignorer le test si le pilote n’a pas pu être chargé et nous devons fermer la fenêtre modale uniquement si elle s’est affichée correctement.

Après s’être resservi un café, Andy et Amita ajoutent du code à leur méthode de test. Ils utilisent les méthodes d’assistance qu’ils ont écrites pour localiser les éléments de page et cliquer sur les liens et les boutons. Voici le résultat :

public void ClickLinkById_ShouldDisplayModalById(string linkId, string modalId)
{
    // Skip the test if the driver could not be loaded.
    // This happens when the underlying browser is not installed.
    if (driver == null)
    {
        Assert.Ignore();
        return;
    }

    // Locate the link by its ID and then click the link.
    ClickElement(FindElement(By.Id(linkId)));

    // Locate the resulting modal.
    IWebElement modal = FindElement(By.Id(modalId));

    // Record whether the modal was successfully displayed.
    bool modalWasDisplayed = (modal != null && modal.Displayed);

    // Close the modal if it was displayed.
    if (modalWasDisplayed)
    {
        // Click the close button that's part of the modal.
        ClickElement(FindElement(By.ClassName("close"), modal));

        // Wait for the modal to close and for the main page to again be clickable.
        FindElement(By.TagName("body"));
    }

    // Assert that the modal was displayed successfully.
    // If it wasn't, this test will be recorded as failed.
    Assert.That(modalWasDisplayed, Is.True);
}

Amita : Le codage a l’air parfait pour l’instant. Mais comment connecter ce test aux attributs id collectés précédemment ?

Andy : Très bonne question. C’est de cela dont nous allons nous occuper maintenant.

Définir des données de cas de test

Andy : Dans NUnit, il est possible de fournir des données aux tests de plusieurs façons. Ici, nous utilisons l’attribut TestCase. Cet attribut prend des arguments qu’il transmet ultérieurement à la méthode de test quand elle s’exécute. Nous pouvons avoir plusieurs attributs TestCase qui testent chacun une fonctionnalité différente de notre application. Chaque attribut TestCase produit un cas de test inclus dans le rapport qui s’affiche à la fin d’une exécution de pipeline.

Andy ajoute ces attributs TestCase à la méthode de test. Ces attributs décrivent le bouton Télécharger le jeu, l’un des écrans de jeu et le joueur en tête du classement. Chaque attribut spécifie deux attributs id : un pour le lien sur lequel cliquer et un pour la fenêtre modale correspondante.

// Download game
[TestCase("download-btn", "pretend-modal")]
// Screen image
[TestCase("screen-01", "screen-modal")]
// // Top player on the leaderboard
[TestCase("profile-1", "profile-modal-1")]
public void ClickLinkById_ShouldDisplayModalById(string linkId, string modalId)
{

...

Andy : Pour chaque attribut TestCase, le premier paramètre est l’attribut id du lien sur lequel cliquer. Le deuxième paramètre est l’attribut id de la fenêtre modale à laquelle nous nous attendons. Comme tu peux le voir, ces paramètres correspondent aux deux arguments de chaîne dans notre méthode de test.

Amita : Oui, effectivement. Avec un peu de pratique, je pense que je vais pouvoir ajouter mes propres tests. Quand pouvons-nous voir ces tests s’exécuter dans notre pipeline ?

Andy : Avant d’envoyer (push) les modifications dans le pipeline, vérifions tout d’abord que le code se compile et s’exécute en local. Nous allons valider et envoyer (push) les modifications à GitHub et observer leur progression dans le pipeline uniquement après avoir vérifié que tout fonctionne. Exécutons maintenant les tests localement.