Schreiben der UI-Tests

Abgeschlossen

In diesem Abschnitt unterstützen Sie Andy und Amita beim Schreiben von Selenium-Tests, mit denen das von Amita beschriebene Verhalten der Benutzeroberfläche überprüft wird.

Amita führt die Tests normalerweise in Chrome, Firefox und Microsoft Edge aus. Hier gehen Sie genauso vor. Der von Microsoft gehostete Agent ist so vorkonfiguriert, dass er mit jedem dieser Browser funktioniert.

Abrufen des Branchs aus GitHub

In diesem Abschnitt rufen Sie den selenium-Branch aus GitHub ab. Sie checken dann diesen Branch aus, d. h. Sie wechseln zu diesem Branch. Durch den Inhalt des Branchs können die Tests nachvollziehen, die von Andy und Amita geschrieben werden.

Dieser Branch enthält das Space Game-Projekt, mit dem Sie in den vorherigen Modulen gearbeitet haben. Er enthält auch eine Azure Pipelines-Konfiguration als Ausgangspunkt.

  1. Öffnen Sie in Visual Studio Code das integrierte Terminal.

  2. Um einen Branch namens selenium aus dem Microsoft-Repository abzurufen, navigieren Sie zu diesem Branch, und führen Sie die folgenden git fetch- und git checkout-Befehle aus:

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

    Tipp

    Wenn Sie Amitas manuellen Test in der vorherigen Lerneinheit mitverfolgt haben, haben Sie diese Befehle möglicherweise schon ausgeführt. Wenn Sie sie bereits in der vorherigen Lerneinheit ausgeführt haben, können Sie sie jetzt noch einmal ausführen.

    Denken Sie daran, dass sich upstream auf das Microsoft GitHub-Repository bezieht. Die Git-Konfiguration erkennt das Upstream-Remoterepository, da Sie diese Beziehung eingerichtet haben. Diese wurde eingerichtet, als Sie das Projekt aus dem Microsoft-Repository geforkt und lokal geklont haben.

    In Kürze werden Sie diesen Branch per Push in Ihr GitHub-Repository (als origin bezeichnet) übertragen.

  3. (Optional) Öffnen Sie in Visual Studio Code die Datei azure-pipelines.yml. Machen Sie sich mit der Anfangskonfiguration vertraut.

    Die Konfiguration weist Ähnlichkeit mit den Konfigurationen auf, die Sie in den vorherigen Modulen dieses Lernpfads erstellt haben. Damit wird nur die Releasekonfiguration der Anwendung erstellt. Aus Gründen der Übersichtlichkeit werden außerdem die Trigger, manuellen Genehmigungen und Tests ausgelassen, die Sie in vorherigen Modulen eingerichtet haben.

    Hinweis

    Bei einer robusteren Konfiguration könnten die Branches angegeben werden, die am Buildprozess beteiligt sind. Beim Pushen einer Änderung in einem Branch können immer Komponententests ausgeführt werden, um z. B. die Codequalität zu überprüfen. Sie können die Anwendung auch in einer Umgebung bereitstellen, in der umfassendere Tests durchgeführt werden. Diese Bereitstellung erfolgt jedoch nur dann, wenn ein Pull Request vorhanden ist, Sie über einen Release Candidate verfügen oder Code im Branch main zusammengeführt werden soll.

    Weitere Informationen finden Sie unter Implementieren eines Codeworkflows in Ihrer Buildpipeline mithilfe von Git und GitHub und Buildpipeline-Trigger.

Schreiben des Komponententestcodes

Amita möchte unbedingt lernen, wie sie Code schreiben kann, der den Webbrowser steuert.

Sie und Andy arbeiten zusammen, um die Selenium-Tests zu schreiben. Andy hat bereits ein leeres NUnit-Projekt eingerichtet. Sie verwenden dabei die Selenium-Dokumentation, einige Online-Lernprogramme und die Notizen, die Amita bei der manuellen Durchführung der Tests gemacht hat. Am Ende dieses Moduls finden Sie weitere nützliche Ressourcen.

Sehen wir uns nun den Prozess an, den Andy und Amita zum Schreiben der Tests verwenden. Sie können die Schritte nachvollziehen, indem Sie HomePageTest.cs im Verzeichnis Tailspin.SpaceGame.Web.UITests in Visual Studio Code öffnen.

Definieren der HomePageTest-Klasse

Andy: Als erstes müssen wir unsere Testklasse definieren. Wir können eine von mehreren Benennungskonventionen verwenden. Nennen wir unsere Klasse HomePageTest. In dieser Klasse werden alle Homepage-Tests eingefügt.

Andy fügt den folgenden Code zu HomePageTest.cs hinzu:

public class HomePageTest
{
}

Andy: Wir müssen diese Klasse als public kennzeichnen, damit Sie für das NUnit-Framework verfügbar ist.

Hinzufügen der IWebDriver-Membervariablen

Andy: Als nächstes benötigen wir eine IWebDriver-Membervariable. IWebDriver ist die Programmierschnittstelle, die zum Starten eines Webbrowsers und für die Interaktion mit dem Inhalt der Webseite verwendet wird.

Amita: Ich habe von Schnittstellen in der Programmierung gehört. Kannst du mir mehr dazu sagen?

Andy: Du kannst dir eine Schnittstelle als Spezifikation oder Blaupause für das gewünschte Verhalten einer Komponente vorstellen. Eine Schnittstelle stellt die Methoden bzw. Verhaltensweisen der betreffenden Komponente zur Verfügung. Die Schnittstelle stellt jedoch keine darunterliegenden Details bereit. Du oder jemand anderes erstellt mindestens eine konkrete Klasse, die diese Schnittstelle implementiert. Selenium stellt die konkreten Klassen bereit, die wir benötigen.

Dieses Diagramm zeigt die IWebDriver-Schnittstelle und einige Klassen, die diese Schnittstelle implementieren:

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

Im Diagramm werden drei Methoden angezeigt, die IWebDriver bereitstellt: Navigate, FindElement und Close.

Die drei hier gezeigten Klassen, ChromeDriver, FirefoxDriver und EdgeDriver, implementieren jeweils IWebDriver und die zugehörigen Methoden. Es gibt auch andere Klassen, z. B. SafariDriver, die IWebDriver ebenfalls implementieren. Jede Treiberklasse kann den Webbrowser steuern, den sie darstellt.

Andy fügt der-Klasse wie folgt eine Membervariable namens driver zur HomePageTest-Klasse hinzu, ähnlich wie bei diesem Code:

public class HomePageTest
{
    private IWebDriver driver;
}

Definieren der Testfixtures

Andy: Wir möchten alle Tests für Chrome, Firefox und Microsoft Edge ausführen. In NUnit können wir Testfixtures verwenden, um alle Tests mehrmals auszuführen, jeweils einmal für jeden Browser, der getestet werden soll.

In NUnit wird das TestFixture-Attribut verwendet, um die Testfixtures zu definieren. Andy fügt die folgenden drei Testfixtures zur HomePageTest-Klasse hinzu:

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

Andy: Als Nächstes müssen wir einen Konstruktor für unsere Testklasse definieren. Der Konstruktor wird aufgerufen, wenn NUnit eine Instanz dieser Klasse erstellt. Als Argument übernimmt der Konstruktor die Zeichenfolge, die mit unseren Testfixtures verknüpft wurde. Der Code sollte wie folgt aussehen:

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

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

Andy: Wir fügen die Membervariable browser hinzu, damit wir den aktuellen Browsernamen im Setupcode verwenden können. Als nächstes schreiben wir den Setup-Code.

Definieren der Setup-Methode

Andy: Als Nächstes müssen wir die Membervariable IWebDriver einer Klasseninstanz zuweisen, die diese Schnittstelle für den Browser implementiert, in dem der Test durchgeführt wird. Die Klassen ChromeDriver, FirefoxDriver und EdgeDriver implementieren diese Schnittstelle für Chrome, Firefox bzw. Edge.

Wir erstellen eine Methode namens Setup, mit der die Variable driver festgelegt wird. Wir verwenden das OneTimeSetUp-Attribut, um NUnit anzuweisen, diese Methode einmal pro Testfixture auszuführen.

[OneTimeSetUp]
public void Setup()
{
}

Mit derSetup-Methode können wir eine switch-Anweisung verwenden, um die driver-Membervariable abhängig vom jeweiligen Browsernamen der entsprechenden konkreten Implementierung zuzuweisen. Jetzt fügen wir den betreffenden Code hinzu.

// 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");
}

Der Konstruktor für jede Treiberklasse übernimmt einen optionalen Pfad zur Treibersoftware, die Selenium zum Steuern des Webbrowsers benötigt. Die Rolle der hier gezeigten Umgebungsvariablen werden wir später erörtern.

In diesem Beispiel sind für den EdgeDriver-Konstruktor weitere Optionen erforderlich, um anzugeben, dass die Chromium-Version von Microsoft Edge verwendet werden soll.

Definieren der Hilfsmethoden

Andy: Ich weiß, dass wir in den Tests zwei Aktionen wiederholen müssen:

  • Suchen von Elementen auf der Seite, z. B. den Links, auf die wir klicken, und den modalen Fenstern, die angezeigt werden sollen
  • Klicken auf Elemente auf der Seite, z. B. auf die Links zum Öffnen der modalen Fenster und die Schaltfläche zum Schließen der einzelnen modalen Fenster

Wir schreiben für beide Aktionen jeweils eine Hilfsmethode. Wir beginnen mit der Methode, die ein Element auf der Seite sucht.

Schreiben der FindElement-Hilfsmethode

Wenn Sie ein Element auf der Seite suchen, erfolgt dies in der Regel als Reaktion auf ein anderes Ereignis, z. B. das Laden der Seite oder die Eingabe von Informationen durch den Benutzer. Selenium stellt die WebDriverWait-Klasse bereit, sodass Sie warten können, bis eine Bedingung erfüllt ist. Wenn die Bedingung innerhalb des angegebenen Zeitraums nicht zutrifft, löst WebDriverWait eine Ausnahme oder einen Fehler aus. Wir können die WebDriverWait-Klasse verwenden, um zu warten, bis ein bestimmtes Element angezeigt wird und Benutzereingaben möglich sind.

Um ein Element auf der Seite zu suchen, wird die By-Klasse verwendet. DieBy-Klasse bietet Methoden, mit denen Sie ein Element anhand seines Namens, seines CSS-Klassennamens, seines HTML-Tags oder in diesem Fall anhand seines id-Attributs suchen können.

Andy und Amita codieren die FindElement-Hilfsmethode. Sie sieht wie dieser Code aus:

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;
        });
}

Schreiben der ClickElement-Hilfsmethode

Andy: Als Nächstes schreiben wir eine Hilfsmethode, die auf Links klickt. Selenium stellt mehrere Möglichkeiten zur Verfügung, um die Methode zu schreiben. Eine davon ist die IJavaScriptExecutor-Schnittstelle. Damit können wir mithilfe von JavaScript programmgesteuert auf Links klicken. Dieses Verfahren funktioniert gut, weil ein Klicken auf Links möglich ist, ohne diese zuerst in die Ansicht zu scrollen.

ChromeDriver, FirefoxDriver und EdgeDriver implementieren jeweils IJavaScriptExecutor. Wir müssen den Treiber in diese Schnittstelle umwandeln und dann ExecuteScript aufrufen, um die JavaScript-Methode click() für das zugrunde liegende HTML-Objekt auszuführen.

Andy und Amita codieren die ClickElement-Hilfsmethode. Sie sieht wie dieser Code aus:

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: Ich finde das Konzept dieser Hilfsmethoden gut. Sie scheinen allgemein genug zu sein, dass sie in fast jedem Test verwendet werden können. Wir können später weitere Hilfsmethoden hinzufügen, wenn diese benötigt werden.

Definieren der Testmethode

Andy: Jetzt können wir die Testmethode definieren. Basierend auf den manuellen Tests, die zuvor ausgeführt wurden, nennen wir diese Methode ClickLinkById_ShouldDisplayModalById. Testmethoden sollten anschauliche Namen aufweisen, die genau definieren, was der Test macht. Hier klicken wir auf einen Link, der durch sein id-Attribut definiert wird. Danach möchten wir ebenfalls anhand des jeweiligen id-Attributs überprüfen, ob das richtige modale Fenster angezeigt wird.

Andy fügt den Startercode für die Testmethode hinzu:

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

Andy: Bevor wir weiteren Code hinzufügen, sollten wir definieren, was dieser Test tun soll.

Amita: Ich kann diesen Teil übernehmen. Wir möchten Folgendes:

  1. Suchen des Links anhand seines id-Attributs und anschließendes Klicken auf den Link.
  2. Suchen des resultierenden modalen Fensters.
  3. Schließen des modalen Fensters.
  4. Überprüfen, ob das modale Fenster erfolgreich angezeigt wurde.

Andy: Prima. Wir müssen jetzt noch einige andere Dinge erledigen. Wir müssen den Test beispielsweise ignorieren, wenn der Treiber nicht geladen werden konnte, und wir dürfen das modale Fenster nur schließen, wenn das modale Fenster erfolgreich angezeigt wurde.

Nachdem Andy und Amita ihre Kaffeebecher wieder gefüllt haben, ergänzen sie den Code der Testmethode. Sie verwenden die erstellten Hilfsmethoden zum Suchen von Seitenelementen sowie zum Klicken auf Links und Schaltflächen. Das Ergebnis lautet wie folgt:

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: Die Codierung sieht bisher recht gut aus. Aber wie verknüpfen wir diesen Test mit den id-Attributen, die wir zuvor gesammelt haben?

Andy: Gute Frage. Darum kümmern wir uns als nächstes.

Definieren der Testfalldaten

Andy: In NUnit gibt es mehrere Möglichkeiten, um Daten für die Tests bereitzustellen. Hier verwenden wir das TestCase-Attribut. Dieses Attribut übernimmt Argumente, die später bei der Ausführung an die Testmethode zurückgegeben werden. Es können mehrere TestCase-Attribute vorhanden sein, die jeweils eine andere Funktion der App testen. Jedes TestCase-Attribut erzeugt einen Testfall, der im Bericht enthalten ist, der am Ende einer Pipeline angezeigt wird.

Andy fügt diese TestCase-Attribute der Testmethode hinzu. Diese Attribute beschreiben die Schaltfläche Download game, einen Spielbildschirm und den Topspieler in der Bestenliste. Jedes Attribut umfasst zwei id-Attribute: eins für den anzuklickenden Link und eins für das entsprechende modale Fenster.

// 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: Für jedes TestCase-Attribut ist der erste Parameter das id-Attribut für den anzuklickenden Link. Der zweite Parameter ist das id-Attribut für das modale Fenster, das angezeigt werden soll. Du siehst, dass diese Parameter den beiden Stringargumenten in unserer Testmethode entsprechen.

Amita: Ja, ich sehe das. Ich denke, dass ich mit etwas mehr Übung meine eigenen Tests hinzufügen kann. Wann können diese Tests in unserer Pipeline ausgeführt werden?

Andy: Bevor wir Änderungen in die Pipeline pushen, müssen wir zunächst überprüfen, ob der Code kompiliert und lokal ausgeführt wird. Wir committen und pushen die Änderungen an GitHub übertragen. Sie durchlaufen die Pipeline erst dann, nachdem wir überprüft haben, ob alles funktioniert. Wir führen die Tests jetzt lokal aus.