Das Selenium-Framework ist eine von ThoughtWorks entwickelte Bibliothek zur Automatisierung von Integrations- bzw. Oberflächentests für Webanwendungen. Dabei kann die Interaktion und (mit Abstrichen) das Design von Webanwendungen getestet werden.

Allgemeines zu Selenium

Selenium bietet mehrere Möglichkeiten zur Erstellung von Tests. So können beispielsweise über die Selenium-IDE Tests aufgezeichnet werden. Dabei muss der Tester lediglich im Aufzeichnungsmodus die gewünschten Interaktionen (Klicks, Texteingaben, Auswahlen, …) ausführen. Der automatisierbare Test stellt dann die Wiederholung der Interaktionen dar, wobei dieselben Ergebnisse erwartet werden. Ist eine Schaltfläche nicht verfügbar oder navigiert der Test zu einer unerwarteten Seite, schlägt der Test fehl.

Eine andere Möglichkeit ist das Implementieren von Tests in Code. Dabei werden Aufrufe mit der Testbefehl-API gegen die Webanwendung ausgeführt, die mit einem Test-Runner angesprochen wird.

Hintergrund

Die Möglichkeit des Aufzeichnens von Tests ist zwar initial sehr komfortabel, allerdings ist die Wartbarkeit schlecht, weswegen sich die queo-Fachteams gegen diese Variante entschieden haben.

Die Wahl fiel daher auf das Implementieren der Testfälle mit der Selenium-API. Allerdings wurde schnell deutlich, dass das Implementieren dieser Tests sehr aufwendig ist, und viel Schreibaufwand und Zeit in Anspruch nimmt. Es mussten daher Möglichkeiten geschaffen werden, diesen Aufwand zu reduzieren.

Der Lösungsansatz stammt aus dem Java-Team, welches die Zeit zum Implementieren von Selenium-Tests drastisch reduzieren konnte, indem ein eigenes Framework entwickelt wurde, welches auf dem Java-Selenium-Framework aufsetzt und das Implementieren der Tests vereinfacht. Das .NET-Fachteam stand jetzt vor der Frage, ob es sinnvoller ist, die eigenen Anwendungen mit Java-Selenium-Tests zu testen oder eine Portierung des Frameworks für .NET vorzunehmen. Aufgrund des vertretbaren Aufwands einer Portierung entschieden wir uns dafür, eine .NET-Version zu erstellen.

Ansatz

Die Idee hinter dem Test-Framework des Java-Teams ist die Erweiterung des bestehenden Frameworks mit dem Ziel, eine möglichst gute Umsetzbarkeit des Page-Object-Patterns zu ermöglichen, bei dem jede Seite der Webanwendung durch eine eigene Klasse abgebildet wird. Die Erweiterung besteht darin, dass auch die Interaktionselemente auf der Seite (z. B. Text-Boxen, Radio-Buttons, Checkboxen, Links, Buttons, …) als Eigenschaften der Klasse implementiert werden und die verschiedenen Elemente ebenfalls durch jeweils implementierte Klassen abgebildet werden, die alle von einer gemeinsamen Basis-Klasse erben.

Das Selenium.NET-Framework stellt bereits eine solche Basis-„Klasse“ bereit, jedoch nur als Interface (IWebElement) und dadurch nicht anpass- bzw. erweiterbar. Um eine Erweiterung zu ermöglichen, wird ein Delegation-Pattern eingesetzt, wobei eine eigene WebElement-Klasse das Interface IWebElement implementiert und die Implementierung der Interface-Methoden aus einer Weiterleitung an den Delegaten besteht. Der Delegat ist eine zwingend benötigte Eigenschaft, welche im Konstruktor gesetzt werden muss. Jetzt kann die WebElement-Klasse beliebig erweitert werden und weitere Hilfsmethoden implementieren oder zusätzliche Logging-Einträge erzeugen.

Aufbau des Frameworks

Die Page-Factory ist der Kern des queo-.net-Selenium-Frameworks und für die Erzeugung der Page-Objekte (Instanzen der einzelnen Seiten) verantwortlich. Dazu enthält die Page-Factory eine Instanz des Web-Drivers (IWebDriver), welcher bei Selenium für das Parsen des HTML-Codes der jeweils angezeigten Seite verwendet wird. Wie beschrieben, verfügen die Page-Objekte auch über Elemente für die Navigation (Links, Buttons, …). Diese Elemente enthalten jeweils eine Methode zum Navigieren, welche als Rückgabewert die Seite, zu der navigiert werden soll, liefert, was die Navigation übersichtlicher macht.

Jede Seite der Webanwendung muss mit einer eindeutigen Page-ID ausgestattet werden. Diese wird nach jeder Navigation von der Page-Factory ausgewertet, um sicherzustellen, dass die korrekte Seite aufgerufen wurde. Die Auswertung, ob die korrekte Seite aufgerufen wurde, berücksichtigt dabei zusätzlich evtl. auftretende Ladezeiten, indem bei einer unerwarteten Page-ID die Überprüfung nach einer gewissen Zeitspanne wiederholt wird.

Um die Page-Factory in den Testfällen zu nutzen, muss jede Klasse die Selenium-Tests enthält, von SeleniumBaseTest erben.

Diese Basis-Testklasse übernimmt die Konfiguration der Page-Factory (Web-Driver-Instanziierung, Warte-Intervalle, …) und ist für das initiale Aufrufen der Webanwendung zuständig. Die Konfiguration erfolgt entweder manuell (über den Konstruktor) oder per Spring.NET.

Einen Selenium-Test implementieren

Das Implementieren eines Selenium-Tests soll mit Hilfe eines trivialen CRUD-Beispiels beschrieben werden. Der Test soll dabei einen Nutzer mit den Eigenschaften Nutzername und Geburtstag anlegen, anzeigen, ändern und wieder löschen.

Dazu wird eine Domain-Entität „User“ implementiert.

 public class User {
      public DateTime Birthday { get; set; }
      public string UserName { get; set; }

      public User(string userName, DateTime birthday, IPageFactory pageFactory)
        : base(pageFactory) {
            UserName = userName;
            Birthday = birthday;
      }
 }
 

Anschließend müssen die verwendeten Ansichten (Erstellen und Bearbeiten, Übersicht bzw. Liste) als Page-Klassen implementiert werden.

Die Erstellen-Seite enthält eine Text-Box (id=UserName) zum Eintragen des Nutzernamens, sowie einen Date-Picker (id=Birthday) zum Auswählen des Geburtages. Mit dem Button (id=btn_submit) kann das Formular abgeschickt werden und der Nutzer wird erstellt.

 public class UserCreateView : PageBase {
         public UserCreateView(IPageFactory pageFactory)
                 : base(pageFactory) {
         }

         [FindsBy(How = How.Id, Using = "Birthday")]
         public DatePicker BirthdayDatePicker { get; set; }

         [FindsBy(How = How.Id, Using = "UserName")]
         public TextBox UserNameInput { get; set; }

         [FindsBy(How = How.Id, Using = "btn_submit")]
         public NavigationButton<UserListPage> SubmitButton { get; set; }

         [FindsBy(How = How.Id, Using = "btn_cancel")]
         public NavigationButton<UserListPage> CancelButton { get; set; }

         public override string PageId {
             get { return "NoArea_User_Create"; }
         }

         public static UserCreateView GoToThisPage(IPageFactory pageFactory) {
             UserListPage userListPage = UserListPage.GoToThisPage(pageFactory);
             return userListPage.CreateButton.NavigateTo();
         }
 }
 

 

Die Bearbeiten-Seite enthält dieselben Elemente wie die Erstellen-Seite. Lediglich die Page-ID ist eine andere.

 

 public class UserEditView : PageBase {
         public UserEditView(IPageFactory pageFactory)
                 : base(pageFactory) {
         }

         [FindsBy(How = How.Id, Using = "Birthday")]
         public DatePicker BirthdayDatePicker { get; set; }

         [FindsBy(How = How.Id, Using = "UserName")]
         public TextBox UserNameInput { get; set; }

         [FindsBy(How = How.Id, Using = "btn_submit")]
         public NavigationButton<UserListPage> SubmitButton { get; set; }

         [FindsBy(How = How.Id, Using = "btn_cancel")]
         public NavigationButton<UserListPage> CancelButton { get; set; }

         public override string PageId {
             get { return "NoArea_User_Edit"; }
         }

         public static UserEditView GoToThisPage(string userName, IPageFactory pageFactory) {
             UserListPage userListPage = UserListPage.GoToThisPage(pageFactory);
             return userListPage.UsersTable.GetRowByUserName(userName).EditButton.NavigateTo();
         }
     }
 

 

Die Nutzerübersicht enthält eine Tabelle (id=tbl_users), in welcher der Name und der Geburtstag eines jeden Nutzer angezeigt werden, sowie einen Button (id=btn_add) zum Hinzufügen eines neuen Nutzers.

 public class UserListPage : PageBase {

         [FindsBy(How = How.Id, Using = "tbl_users")]
         public UsersTable UsersTable { get; set; }

         [FindsBy(How = How.Id, Using = "btn_add")]
         public NavigationButton<UserCreateView> CreateButton { get; set; }
         
         public UserListPage(IPageFactory pageFactory)
                 : base(pageFactory) {
         }

         public override string PageId {
             get { return "NoArea_User_Index"; }
         }

         public static UserListPage GoToThisPage(IPageFactory pageFactory) {
             // Click Menu and navigate to page
         }
 }
 

Die Pages kennen die Domain-Klassen nicht, um eine bessere Skalierbarkeit zu gewährleisten.

Die Domain-Klasse enthält Methoden, um sich zu erstellen, zu ändern und zu löschen. Die Methoden werden so implementiert, dass sie sich quasi „durch die Anwendung klicken“, um das gewünschte Ergebnis zu erreichen. Die Methode für das Erstellen wird als statische Methode implementiert, da es bei komplexeren Entitäten sehr (zeit-)aufwendig sein kann, diese zu erstellen. Hat der Tester Zugriff auf die Datenbank gegen welche die Testanwendung ausgeführt wird, ist es sinnvoll die benötigten Daten per Skript in die Datenbank zu schreiben und die Objekte direkt über den Konstruktor zu instanziieren.

 public class User : DomainObjectBase {
         public User(string userName, DateTime birthday, IPageFactory pageFactory)
                 : base(pageFactory) {
             UserName = userName;
             Birthday = birthday;
         }

         public DateTime Birthday { get; set; }

         public string UserName { get; set; }

         public static DomainObjectAndPage<User, UserListPage> Create(string userName, DateTime birthday, IPageFactory pageFactory) {
             UserCreateView createView = UserCreateView.GoToThisPage(pageFactory);
             createView.UserNameInput.EnterText(userName);
             createView.BirthdayDatePicker.SetDate(birthday);

             UserListPage listAfterCreate = createView.SubmitButton.NavigateTo();
             User user = new User(userName, birthday, pageFactory);

             return new DomainObjectAndPage<User, UserListPage>(user, listAfterCreate);
         }

         public UserListPage Update(string newUserName, DateTime newBirthday) {
             UserEditView editView = UserEditView.GoToThisPage(UserName, PageFactory);
             editView.UserNameInput.EnterText(newUserName);
             editView.BirthdayDatePicker.SetDate(newBirthday);

             UserListPage listAfterUpdate = editView.SubmitButton.NavigateTo();

             UserName = newUserName;
             Birthday = newBirthday;

             return listAfterUpdate;
         }

         public UserListPage Delete() {
             return UserListPage.GoToThisPage(PageFactory).GetRowByUserName(UserName).DeleteButton.ClickAndAcceptAlert();
         }
 }
 

Der Testfall selbst ruft dann lediglich die Methoden auf, wodurch der Code des eigentlichen Testfalls sehr knapp gehalten wird.

   
 public class UserTests : SeleniumTestBase {

         [TestMethod]
         public void UserCrudTest() {

             /* create user */              
             string userName = CreateUniqueName("user_");
             DateTime birthday = new DateTime(2000,01,01);
             DomainObjectAndPage<User, UserListPage> createResult = User.Create(userName, birthday, PageFactory);
             User user = createResult.DomainObject;
             UserListPage listAfterCreate = createResult.Page;
             listAfterCreate.UsersTable.AssertExactlyOneRow(user.UserName, user.Birthday);

             /* reload and update user */
             string newUserName = CreateUniqueName("new user_");
             DateTime newBirthday = new DateTime(2002, 02, 02);
             UserListPage listAfterUpdate = user.Update(newUserName, newBirthday);
             listAfterUpdate.UsersTable.AssertExactlyOneRow(user.UserName, user.Birthday);

             /* delete user */
             UserListPage listAfterDelete = user.Delete();
             listAfterDelete.UsersTable.AssertNoRow(user.UserName);
         }
 }
 

Fazit

Das Testframework ermöglicht ein schnelleres Implementieren von Oberflächen-Tests. Des Weiteren wird die Lesbarkeit der Tests erhöht und auch die Anpassbarkeit und Wiederverwendbarkeit des Testcodes ist gegeben. Das Beheben von in Tests aufgedeckten Fehlern wird durch das erweiterte Logging und mit Hilfe von Screenshots erleichtert. Für den Kunden ergeben sich daraus eine weitere Qualitätssteigerung der Anwendungen aufgrund einer höheren Testabdeckung bzw. eine Kostensenkung mit der etablierten Teststruktur.

Da die Testfälle ähnlich implementiert werden wie Standard-Unit-Tests (Test-Klasse und Test-Methode), können Sie auch auf dem Integrations-Server (z. B. Hudson) ausgeführt werden und sind somit in den Qualitäts-Sicherungsprozess eingebunden. Durch die Kapselung des Web-Drivers ist es sogar möglich, mehrere Testläufe durchzuführen, die jeweils einen anders konfigurierten Browser verwenden, egal ob Internet Explorer, Firefox, Safari oder Chrome.

 

 

Diskutieren Sie mit!

Es steht Ihnen frei einen Kommentar zu hinterlassen. Sie finden Informationen zur Verwendung Ihrer Daten in unserer Datenschutzerklärung.

Alle mit einem markierten Felder sind Pflichtfelder und müssen ausgefüllt werden