Aufteilen von Hilfsmethoden bei Software-Tests – Praxisbeispiel UI-Tests

04. Januar 2014 Softwaretest von Eric Kubenka

Große Softwareprojekte benötigen zahlreiche Tests. Und so wächst mit dem Projekt auch die Anzahl der Testfälle sowie die dazugehörigen Codezeilenanzahl schnell an und erreicht einen unüberschaubaren Wert.

Neben den eigentlichen Testfällen stellen Hilfsmethoden, wie beispielsweise das Suchen von Elementen, das Vorbereiten von Testdaten und so weiter und so fort einen enormen Anteil der Codebasis der Tests dar. Oft beginnt man einfach damit die Hilfsmethoden irgendwie einzuordnen und das Ganze verliert dann mit zunehmender Zeit und stetig wachsender Projektgröße die Übersicht.

Daher gibt es auch zum Aufteilen von Test-Hilfsmethoden entsprechende Pattern an die man sich halten kann. Das Single Responsible Pattern – jede Klasse trägt exakt eine Verantwortung – aus der „normalen Softwareentwicklung“ kann man meiner Meinung nach einwandfrei auch zum Strukturieren von Hilfsklassen verwenden.

Vorteile

Das Aufteilen von Hilfsmethoden in entsprechende Klassen dient erstens zur Steigerung der Übersichtlichkeit und zweitens trägt es dazu bei, dass Dopplungen vermieden werden. Die Wartung der Tests wird vereinfacht und die Code-Basis kann – geschickt ausgelagert – in jedem weiteren Projekt ebenfalls verwendet werden. Die Aussagekraft der Tests wird durch geschickte Kapselungsmethoden gesteigert und auch die Produktivität nimmt zu, wenn die Testprojekte gut strukturiert sind und jede Hilfsmethode einen festen Platz hat.

Gerrard Meszaros, Agile Software Development Consultant, stellt in seinem Buch "xUnit Test Patterns: Refactoring Test Code" einen meiner Meinung nach gelungenen Ansatz bereit, welchen ich gern in meinen Projekten verwende.

Dabei teilen sich die Hilfsmethoden seiner Ansicht nach so gut wie immer in folgende fünf Bestandteile auf:

  • Creation Methods
  • Finder Methods
  • Encapsulation Methods
  • Verification / Assert Methods
  • Cleanup Methods

Auf alle möchte ich kurz eingehen und anschließend ein kleines UI-Tests-Szenario unter Berücksichtigung dieser Aufteilung aufzeigen.

 

Creation Methods

Wie der Name schon sagt, dienen diese Hilfsmethoden zum Generieren und Bereitstellen von Daten. Darunter fällt beispielweise auch das Instanziieren der entsprechenden Testdaten, beziehungsweise der Abhängigkeiten für das Testobjekt. Auch das Instanziieren des Testobjekts selbst kann in diesen Bereich fallen.

 

Finder Methods

Auch hier gibt der Name bereits Aufschluss über die entsprechende Aktivität. Diese Methoden dienen zum Suchen, Ermitteln und Bereitstellen von Ressourcen. Gerade bei UI-Tests sind das zahlreiche Methoden, wie beispielsweise das Suchen von Elementen nach Id, Name, Inhalt oder sonstigen Sucheigenschaften.

 

Encapsulation Methods

Die Kapselungsmethoden dienen zum Zusammenfassen von Aktionen. Ein geeigentes Beispiel sind hier immer wieder auftretende Prozesse, die mehrere Codezeilen benötigen um ausgeführt zu werden. Beispielsweise muss ein Benutzer einer Anwendung vor jedem Test eingeloggt werden, ehe er interne Funktionen nutzen kann. Also wird das Suchen und Füllen der Textfelder für den Login, sowie das Abschicken des Formulars zusammengefasst ausgelagert.

 

Verification / Assert Methods

Darunter fallen Methoden zum Validieren von Objekteigenschaften, sowie Custom-Assert-Methoden.

 

Cleanup Methods

Um die Testumgebung sauber zurückzulassen, ist es oft von Nöten dass Überbleibsel des Tests noch beseitigt werden. All solche Teardown Methoden werden in dieser Kategorie zusammengefasst.

 

Praxisbeispiel UI-Test

Folgendes Beispiel ist auch wie immer auf Github bereitgestellt. Zu Beginn möchte ich das Szenario kurz beschreiben. Ein Anwender soll bei der zu testenden Anwendung angemeldet werden und anschließend wird verifiziert, dass die Home-Site mit einer Willkommensmeldung angezeigt wird.

Alles nacheinander geschrieben sieht dieser Testfall so aus. Der Vorteil hier ist, dass noch nicht viel von Nöten ist um diesen simplen Test auszuführen.

[TestMethod]
    public void LoginUserAndShowHomeOld()
    {
        // Entsprechenden Testdatensatz generieren
		var user = new User("eric", "password");

        // Login-Elemente suchen udn füllen
        var usrTxtBox = new UITestControl();
        var pwTxtBox = new UITestControl();
        var btn = new UITestControl();

        // searchproperties hinzufügen und element suchen
        usrTxtBox.SearchProperties.Add("AutomationId", "username",PropertyExpressionOperator.EqualTo);
        usrTxtBox = usrTxtBox.FindMatchingControls()[0];

        // searchproperties hinzufügen und element suchen
        pwTxtBox.SearchProperties.Add("AutomationId", "password", PropertyExpressionOperator.EqualTo);
        pwTxtBox = pwTxtBox.FindMatchingControls()[0];

        // searchproperties hinzufügen und element suchen
        btn.SearchProperties.Add("AutomationId", "loginbtn", PropertyExpressionOperator.EqualTo);
        btn = btn.FindMatchingControls()[0];


        // Setze entsprechende Werte 
        usrTxtBox.SetProperty("Text", user.Name);
        pwTxtBox.SetProperty("Text", user.Password);

        // LoginButton Clicken
        Mouse.Click(btn);

		// Die Willkommen-Message suchen und entsprechend verifizieren
        var welcomemsg = new UITestControl();
        welcomemsg.SearchProperties.Add("AutomationId", "welcomemsg", PropertyExpressionOperator.EqualTo);

        StringAssert.Contains(welcomemsg.GetProperty("Text").ToString(), "Willkommen, eric");
	}

Schnell wird deutlich, wie viel Methoden sich schon in diesem kleinen Test doppeln und daher Hilfsmethoden notwendig werden. Korrekt ausgelagert sieht der übrige Code dann wie folgt aus:

[TestMethod]
        public void LoginUserAndShowHome()
        {
            // Entsprechenden Testdatensatz generieren
            var user = DataGenerator.CreateUser("eric", "password");

            // Zusammengefasste Action Aufrufen, da die Handlungen immer identisch sind
            Authentication.PerformLogin(user);

            // Die Willkommen-Message suchen und entsprechend verifizieren
            var welcomemsg = ElementFinder.FindElementById("welcomemsg");
            StringAssert.Contains(welcomemsg.GetProperty("Text").ToString(), "Willkommen, eric");
        }

Erstens ist der Test nun deutlich lesbarer gestaltet. Es sind bereits Basisstrukturen für die weiteren Testfälle vorhanden, da beispielsweise beim Test mit falschen Logindaten der Loginprozess der gleiche bleibt, und nur das Überprüfen der Fehlermeldung eine andere ist. Weiterhin wurde die Wartbarkeit deutlich erhöht. Das Beispiel steht, wie bereits erwähnt, auf Github bereit und da kann nachgeschaut werden wie ich die Methoden in die einzelnen Klassen aufgeteilt habe. Die Struktur möchte ich hier nur noch kurz aufzeigen.

Solution Aufbau

 

Aber Vorsicht!

Zu vernachlässigen ist aber nicht, dass diese Planung der Aufteilung nicht zu einem Overhead führen soll. Das heißt, es soll nicht bereits im Vorfeld entschieden werden, wie das ganze Aufzuteilen ist, und welche Hilfsmethoden benötigt werden. Es sollen also auch wirklich nur die Methoden implementiert werden, die wirklich benötigt werden – Ansonsten wird wertvolle Zeit in unnötige Implementierung gesteckt. Das sinnvollste Vorgehen ist hier für:

  1. Schreiben des Testfalls
  2. Code-Dopplungen der gleichen Methode / Testklasse zusammenfassen
  3. Auslagern der Methoden in die entsprechenden Klassen (Kategorien)
    1. Falls noch nicht vorhanden zuvor die entsprechende Struktur anlegen
  4. Bei großen Projekten und zahlreichen Methoden: Auslagern in eigene Bibliothek

 

Fazit

Auch bei den Hilfsmethoden gibt es diverse Ansätze diese korrekt aufzuteilen und so dafür zu sorgen, dass eine gute Struktur vorliegt und so die Wartung vereinfacht wird. Weiterhin kann bei richtiger Umsetzung die entstandene Hilfsmethoden-Bibliothek sofort für das nächste Testprojekt verwendet werden.

 

Zurück