Keine Angst vor Veränderung – Stabile automatisierte Oberflächentests trotz Redesign
Tester, die sich mit automatisierten Oberflächentests auseinandersetzen, kennen die Problematik: Oberflächen werden modernisiert und die dafür erstellten Tests lassen sich nicht mehr ausführen, da einzelne Elemente oder ganze Oberflächen neu angeordnet wurden. Nun stellt sich die Frage, ob automatisierte Oberflächentests überhaupt sinnvoll für sich stark wandelnde Oberflächen realisierbar sind? Unsere Antwort: Ja.
In diesem Artikel wird ein praxiserprobter Ansatz vorgestellt, bei dem eine prozessorientierte Abstraktionsebene zwischen Testfalllogik und UI eingeführt wird. Dadurch können Testfälle weitestgehend stabil gehalten werden, während durch Entwicklungsarbeiten an der Oberfläche nicht nur einzelne Steuerelemente, sondern auch ganze Formulare verändert werden.
Testen stellt einen zentralen Aspekt der Qualitätssicherung dar und sollte parallel zur Entwicklung durchgeführt werden. In der Praxis jedoch wird dies oftmals nicht befolgt. Vor allem die Oberflächenautomatisierung wird häufig erst gegen Ende der Entwicklung angegangen. Nun gut: Besser spät als nie! Doch wie geht man dann damit um, wenn der Startschuss zur Oberflächenautomatisierung eines bestehenden Systems fällt, für welches ein größerer Umbau der Oberfläche absehbar ist? Auf eine stabile Oberfläche zu warten und den Anfang der Testautomatisierung damit auf unbestimmte Zeit nach hinten zu verlegen, ist der Qualität nicht zuträglich. Es muss also die Herausforderung gemeistert werden, die Tests weitestgehend stabil halten zu können, auch wenn die Oberfläche stark umstrukturiert wird.
Dieses Problem galt es für ein bereits ausgeliefertes Enterprise Resource Planning (ERP) System auf Basis von Microsoft Dynamics AX 2012 zu lösen, für welches es zu diesem Zeitpunkt noch keinerlei Tests gab. Wie alle ERP-Systeme zeichnet sich Dynamics AX durch eine sehr hohe Komplexität bezüglich Architektur und Oberflächen aus. So ist z. B. die Oberfläche nicht durchgängig mit einer Technologie implementiert, sondern vermischt die Verwendung von WinForms und Win32. Dies erschwert das Auffinden von Elementen im Rahmen der Oberflächenautomatisierung erheblich. Kurz nach dem geplanten Beginn der Oberflächenautomatisierung sollten die Hauptmasken für alle Prozesse umgebaut werden. Der Benutzer sollte dadurch bei den ohnehin sehr komplexen Abläufen innerhalb Dynamics AX durch eine verbesserte, bis dahin nicht gegebene, prozessorientierte Oberfläche unterstützt werden.
Der Ansatz, um trotz des bevorstehenden Oberflächenumbaus mit der Testautomatisierung zu beginnen, basiert auf einem elementaren Prinzip der Softwaretechnik: der Abstraktion. Durch Einführung einer zweistufigen Abstraktion, werden die Tests von der konkreten Ausprägung der Oberfläche entkoppelt. Auf der ersten Stufe wird die Ansteuerbarkeit der Oberflächenelemente sichergestellt. Darauf aufbauend, stellt die zweite Stufe eine Abstraktion zum prozessorientierten Beschreiben von Tests zur Verfügung. Somit sind die Tests nicht von der konkreten Oberfläche abhängig und müssen bei einem Umbau der Oberfläche nicht mehr angepasst werden.
Noch ein kleiner Einschub zur Begrifflichkeit: Der Begriff "Oberflächenautomatisierung" bezeichnet im Folgenden das Programmieren automatisierter Tests für ein System über dessen Oberfläche. Die Oberfläche wird dabei über die Hierarchie der Oberflächenelemente angesteuert.
Wieso zwei Abstraktionsebenen?
Bevor das bereits erwähnte Abstraktionsmodell im Detail erklärt wird, empfiehlt es sich zunächst, nochmals einen Schritt zurück zu gehen: Wo genau liegt das Problem? Ist es nicht so, dass bei hinreichenden Änderungen an der Oberfläche des getesteten Systems, in jedem Fall die betroffenen Tests angepasst werden müssen? Nein: Zwar müssen Anpassungen durchgeführt werden, jedoch nicht zwangsläufig an den Tests. Genaugenommen sollten es nicht die Tests sein, die angepasst werden müssen.
Abstraktion ist das Mittel der Wahl, um die Tests von Änderungen an der Oberfläche zu entkoppeln. Konkret bedeutet dies, dass die Tests nicht mehr direkt auf Oberflächenelemente zugreifen, sondern die Zugriffe über eine Zwischenschicht realisiert werden. Diese Zwischenschicht stellt die für die Tests benötigten Funktionalitäten der Oberfläche in einem anprogrammierbaren Modell zur Verfügung. Damit können Änderungen von Elementen einer Maske zentral in diesem Modell nachgezogen werden. Alle darauf aufbauenden Tests müssen nicht angepasst werden. In der Fachliteratur wird diese Zwischenschicht als "Page Object Model" bezeichnet, in welchem jede Maske durch ein sogenanntes "Page Object" repräsentiert wird. Zu den wesentlichen Vorteilen dieses Modells gehören die Vermeidung von Redundanzen und eine daraus resultierende erhöhte Wartbarkeit.
Zur Veranschaulichung verwendet der gesamte Artikel ein durchgängiges Beispiel. Aufgrund der hohen Komplexität wird auf die Verwendung realer Dynamics AX-Masken verzichtet und stattdessen das folgende vereinfachte Beispiel betrachtet: Für das Anlegen eines neuen Antrags müssen zunächst die Antragsdaten (Application data) eingegeben werden. Im Anschluss muss gegebenenfalls die Telefonnummer des Kunden ergänzt werden. In der Ursprungsmaske musste dafür der Dialog mit den Kundendaten aufgerufen (vgl. Abb.1, links), bearbeitet und wieder geschlossen werden. Nach dem Umbau der Oberfläche, wird das Feld mit der Telefonnummer direkt unterhalb der Antragsdaten eingeblendet (vgl. Abb.1, rechts).
Abb.2 veranschaulicht die Hierarchie der Oberflächenelemente im Kontext des Beispiels für das Fenster New Application vor dem Umbau. Code-Snippet 1 zeigt ein Quellcodebeispiel um den Kundendialog über den Button Customer zu öffnen: Zunächst wird der Button Customer anhand seiner Id innerhalb des Elements ApplicationDataContainer auf dem Formular NewApplicationForm identifiziert. Über die Methode Click() wird dann der Button gedrückt, um den Kundendialog zu öffnen. Existiert dieser Code-Auszug innerhalb mehrerer Tests, muss bei Änderungen an der Oberfläche auch jeder dieser Tests angepasst werden.
Code-Snippet 1: Quellcodeauszug zur Oberflächenautomatisierung
var applicationDataContainer = NewApplicationForm.FindControl(ControlType.GroupBox, By.Id("ApplicationDataContainer")); var openCustomerButton = applicationDataContainer.FindControl(ControlType.Button, By.Id("CustomerButton")); openCustomerButton.Click();
Um die Tests von Änderungen innerhalb des Formulars zu entkoppeln, wird das Identifizieren der Elemente in ein Page Object (vgl. Code-Snippet 2) ausgelagert. Die Tests selbst suchen nicht mehr die Elemente, sondern greifen auf diese durch das Page Object zur Verfügung gestellte Funktionalität zu. Der Grad der Abstraktion ist dabei variierbar: Das Page Object kann die einzelnen Oberflächenelemente oder ganze Methoden mit Abläufen exponieren. Im Beispiel stellt das Page Object NewApplication den Tests eine Methode OpenCustomerDialog() zum Öffnen eines Kundendialogs zur Verfügung. Die Tests definieren nur, dass im aktuellen Oberflächenkontext der Kundendialog geöffnet werden soll, nicht mehr das "wie". Wenn bei einer Änderung der Oberfläche der Kundendialog nicht mehr über einen Button – sondern z. B. über ein LinkLabel – zu öffnen ist, muss nur noch das Page Object angepasst werden. Die Tests bleiben unverändert.
Code-Snippet 2: Quellcode des Page Objects für die Maske New Application
public class NewApplicationPage { #region Private Fields private WinWindow _newApplicationFormWindow; private WinGroup _applicationDataContainer; private WinButton _customerButton; private WinEdit _amountEdit; #endregion Private Fields #region Constructors public NewApplicationPage(WinWindow newApplicationFormWindow) { _newApplicationFormWindow = newApplicationFormWindow; } #endregion Constructors #region Private Properties private WinWindow NewApplicationFormWindow { get { return _newApplicationFormWindow; } } private WinPane ApplicationDataContainer { get { return _applicationDataContainer ?? (_applicationDataContainer = NewApplicationFormWindow.FindControl(ControlType.WinGroup, By.Id("ApplicationDataContainer"))); } } private WinButton CustomerButton { get { return _customerButton ?? (_customerButton = ApplicationDataContainer.FindControl(ControlType.Button, By.Id("CustomerButton"))); } } private WinEdit AmountEdit { get { return _amountEdit ?? (_amountEdit = ApplicationDataContainer.FindControl(ControlType.Edit, By.Id("AmountEdit"))); } } #endregion Private Properties #region Public Methods public CustomerDialog OpenCustomerDialog() { CustomerButton.Click(); } #endregion Public Methods }
Was passiert nach einem großen, wie zuvor beschriebenen, Umbau der Oberfläche? Wenn nicht nur Oberflächenelemente innerhalb einer Maske verändert werden, sondern auch die Struktur und das Zusammenspiel mehrerer Masken? In diesem Fall müssen die Tests wieder angepasst werden. Ein Page Object stellt den Inhalt einer Maske zur Verfügung. Durch das Ändern der Struktur der Masken, muss auch die Struktur der Page Objects angepasst werden. Somit werden die von den Tests verwendeten Funktionen nach dem Umbau ggf. durch andere oder gar neue Page Objects bereitgestellt.
In der Fortsetzung des Beispiels wird die Telefonnummer eines Kunden (welche bisher in der Kundendialogs-Maske platziert war (vgl. Abb.1)) direkt in die Maske New Application integriert. Dieser Umbau bildet den Prozess besser ab und vereinfacht die Userinteraktion. Gleichzeitig werden jedoch strukturelle Änderungen am Page Object Model notwendig, welche wiederum Anpassungen der Tests erfordern (vgl. Code-Snippet 3).
Code-Snippet 3: Quellcodeauszug zum Setzen der Telefonnummer eines Kunden vor und nach dem Umbau
// Vor dem Umbau var customerDialog = newApplicationPage.OpenCustomerDialog(); customerDialog.SetPhoneNumber("2716057"); customerDialog.Close(); // Nach dem Umbau newApplicationPage.SetPhoneNumber("2716057");
Das Ziel, die Tests nicht anpassen zu müssen, kann durch Verwendung des Page Object Models nicht erreicht werden. Um die letzten Meter auf dem Weg zu oberflächenunabhängigen Tests zu beschreiten, muss die Frage beantwortet werden, was bei einem größeren Umbau der Oberfläche überhaupt konstant bleibt: Der Prozess. Wie zuvor beschrieben, wurde die Oberfläche angepasst, um den Prozess besser abzubilden. Für die Tests ist nicht die Abstraktion von den einzelnen Oberflächenelementen einer Maske ausreichend, vielmehr wird eine Abstraktion bis zur Prozessbeschreibungsebene benötigt. Daher wird eine weitere Abstraktionsschicht eingeführt: Das Zweischichtenmodell ist geboren!
Das Zweischichtenmodell
Die Oberfläche wird weiterhin durch das Page Object Model abgebildet. Diese Abbildung wird im Folgenden als "technische Schicht" bezeichnet. Die technische Schicht sollte einen möglichst geringen Grad an Abstraktion haben und über das Page Object Model ausschließlich die Ansteuerung der Oberflächenelemente zur Verfügung stellen. Dadurch ist es nah an der technischen Umsetzung der Oberfläche und der Aufbau ist klar definiert. Der geringe Grad an Abstraktion hat auch zur Folge, dass die Erzeugung des technischen Modells teilweise automatisiert oder toolgestützt erfolgen kann.
Von der fachlichen Seite betrachtet, wird ein weiteres Modell aufgebaut, welches den Prozess abbildet. Diese Abbildung wird im Folgenden als "Prozessschicht" bezeichnet und liefert den größtmöglichen Grad an Abstraktion von der Oberfläche. Für Zugriffe auf die Oberfläche, verwendet die Prozessschicht die technische Schicht. Den Tests werden fachliche Funktionen zur Verfügung gestellt. Dadurch müssen die Tests auch bei umfangreicheren Oberflächenänderungen nicht mehr angepasst werden. Das so definierte Zweischichtenmodell ist in Abb.3 visualisiert.
Die Umsetzung des Beispiels gestaltet sich mit dem vorgestellten Zweischichtenmodell wie folgt: In der technischen Schicht werden für das New Application-Formular und den Kundendialog die beiden Page Objects NewApplicationPage sowie CustomerDialog erstellt. Im Vergleich zu dem in Code-Snippet 4 gezeigten Page Object wird der Grad der Abstraktion jedoch reduziert (vgl. Code-Snippet 4). Es werden keine komplexen Methoden wie OpenCustomerDialog() bereitgestellt, sondern nur die auf den Masken enthaltenen Oberflächenelemente. Die Prozessschicht hingegen stellt fachliche Methoden, wie SetPhoneNumber(), zur Verfügung und greift für deren Realisierung auf die technische Schicht zu.
Code-Snippet 4: Quellcodeauszug des Zweischichtenmodells vor dem Umbau der Oberfläche
// Page Object für das Formular 'New Application' (Technische Schicht) public class NewApplicationPage { // restliche Implementierung des Page Objects 'NewApplicationPage' public WinButton CustomerButton { get { return _customerButton ?? (_customerButton = ApplicationDataContainer.FindControl(ControlType.Button, By.Id("CustomerButton"))); } } } // Page Object für das Formular 'CustomerDialog' (Technische Schicht) public class CustomerDialog { // restliche Implementierung des Page Objects 'CustomerDialog' public WinEdit PhoneNumber { get { return _phoneNumber ?? (_phoneNumber = ApplicationDataContainer.FindControl(ControlType.Edit, By.Id("PhoneNumberEdit"))); } } public WinButton CloseButton { get { return _closeButton ?? (_closeButton = ApplicationDataContainer.FindControl(ControlType.Button, By.Id("CloseButton"))); } } } // Klasse zur Bereitstellung fachlicher Methoden (Prozessschicht) public class Process { // restliche Implementierung der 'Process'-Klasse public void SetPhoneNumber(string phoneNo) { newApplicationPage.CustomerButton.Click(); var customerDialog = new CustomerDialog(); customerDialog.PhoneNumber.SendKeys(phoneNo); customerDialog.CloseButton.Click(); } }
Nun stellt sich abermals die Frage: Was passiert nach einem großen, maskenübergreifenden Umbau der Oberfläche? In diesem Fall muss die technische Schicht angepasst werden, da sie die Oberfläche mittels Page Objects abbildet. Das gleiche gilt für die darauf aufbauenden fachlichen Methoden in der Prozessschicht, da sie auf die Page Objects der technischen Schicht zugreifen. Die Tests selbst jedoch bleiben von den Änderungen der Oberfläche unberührt (vgl. Abb.3). Das Ziel der Entkopplung von Oberfläche und Implementierung fachlicher Tests ist erreicht.
Für das Beispiel bedeutet dies: Im Zuge des Oberflächenumbaus wird die Telefonnummer eines Kunden direkt in die Maske New Application integriert. D. h. in der technischen Schicht werden die beiden Page Objects NewApplication Page und CustomerDialog angepasst und das Feld PhoneNr von CustomerDialog nach NewApplicationPage verschoben. Weiterhin wird der Button Customer aus dem Page Object NewApplication entfernt. In der Prozessschicht wird abschließend die Methode SetPhoneNumber() entsprechend angepasst (vgl. Code-Snippet 5).
Code-Snippet 5: Quellcodeauszug des Zweischichtenmodells nach dem Umbau der Oberfläche
// Klasse zur Bereitstellung fachlicher Methoden (Prozessschicht) public class Process { // restliche Implementierung der 'Process'-Klasse public void SetPhoneNumber(string phoneNo) { newApplicationPage.PhoneNumber.SendKeys(phoneNo); } }
Weitere Anknüpfpunkte
Die Prozessschicht ermöglicht es den Testern, ihre Tests in einer sehr fachlichen Sprache zu formulieren. Dadurch können Tester und Anforderungsanalysten eine gemeinsame Sprache aufbauen. Bei dem Ansatz Acceptance Test Driven Development (ATDD) [1] können die Anforderungen an ein System direkt in der gemeinsamen Sprache formuliert und als Tests verwendet werden. Ein Blick auf Frameworks, welche diesen Ansatz unterstützen (z. B. SpecFlow für C# [2]) lohnt sich.
In dem diesem Ansatz zugrundeliegenden Projekt wurde die Prozessschicht mit einer Fluent API ausgestattet. Hierbei ist der Rückgabewert einer Methode immer das Objekt, an welchem die Methode aufgerufen wurde. Somit können beliebige Methodenaufrufe an einem Objekt kombiniert werden (vgl. Code-Snippet 6). Dieses Vorgehen ermöglicht es, große Dateneingabemasken übersichtlich mit validen, testrelevanten Daten zu befüllen: Alle übergebenen Werte werden wie gewünscht gesetzt. Alle nicht übergebenen Werte werden mit validen, realistischen Standardwerten gefüllt. Dadurch dokumentiert ein Aufruf, welche Daten für den jeweiligen Test tatsächlich relevant sind.
Code-Snippet 6: Beispielhafte Aufrufe einer Fluent API
Customer.WithName("Alice").WithPhoneNumber("0151-12345678"); Customer.WithName("Max"); Customer.WithPhoneNumber("0151-87654321");
Fazit
Die Testoberflächenautomatisierung sieht sich häufig mit Änderungen der Oberfläche konfrontiert. Bei kleineren Änderungen können diese durch Verwendung des Page Object Models abgefangen werden. Bei einem tiefgreifenden Umbau der Oberfläche reicht dieses Modell jedoch nicht aus, um Tests stabil zu halten. In diesem Artikel wurde unter Zuhilfenahme einer zweiten, prozessorientierten Abstraktionsschicht gezeigt, wie die gewünschte Stabilität trotz eines solchen Umbaus erzielt werden kann.
Der initiale Aufwand, um das hier vorgestellte Zweischichtenmodell aufzubauen, darf nicht unterschätzt werden. Daher sollten vorab Aufwand und Nutzen kritisch gegenübergestellt werden. Der Nutzen wächst mit steigender Anzahl an erwarteten Überschneidungen verwendeter Oberflächenelemente durch verschiedene Tests sowie dem Umfang der Oberflächenänderungen, da Änderungen sich zwar auf die Abstraktionsebenen, jedoch nicht mehr auf die Tests auswirken. Ebenso sollte die stark erhöhte Wartbarkeit der Codebasis in die Aufwand-Nutzen-Analyse miteinbezogen werden. Wird eine große Anzahl an wartbaren und robusten Tests angestrebt, so übersteigt der Nutzen schnell den initialen Aufwand, der zum Aufbau des Modells notwendig ist.
Die erreichte Entkopplung der Tests von der Oberfläche führt dazu, dass sich ändernde Oberflächen kein Hindernis mehr für die Testautomatisierung darstellen. Somit steht nichts mehr im Wege, um mit der Testautomatisierung zu beginnen und kommenden Umbauten der Oberfläche gelassen gegenüberzustehen.
- Informatik Aktuell – Dr. Tobias Nestler, Dr. Mirko Seifert & Dr. Christian Wende: Einführung in Acceptance Test-Driven Development (ATDD)
- Spec Flow