() translation by (you can also view the original English article)
Vor einigen Jahren war ich sehr skeptisch gegenüber automatisierten UI-Tests und diese Skepsis entstand aus einigen fehlgeschlagenen Versuchen. Ich schrieb einige automatisierte UI-Tests für Desktop- oder Webanwendungen und riss sie einige Wochen später aus der Codebasis heraus, weil die Kosten für deren Wartung zu hoch waren. Daher dachte ich, dass UI-Tests schwierig sind und dass es, obwohl es viele Vorteile bietet, am besten ist, es auf ein Minimum zu beschränken und nur die komplexesten Workflows in einem System durch UI-Tests zu testen und den Rest Unit-Tests zu überlassen. Ich erinnere mich, dass ich meinem Team von Mike Cohns Testpyramide erzählt habe und dass in einem typischen System über 70% der Tests Unit-Tests, etwa 5% UI-Tests und die restlichen Integrationstests sein sollten.
Daher dachte ich, dass UI-Tests schwierig sind und dass es zwar viele Vorteile bietet, es aber am besten ist, es auf ein Minimum zu beschränken...
Ich habe mich geirrt! Sicher, UI-Tests können schwierig sein. Das ordnungsgemäße Schreiben von UI-Tests dauert einige Zeit. Sie sind viel langsamer und spröder als Unit-Tests, da sie Klassen- und Prozessgrenzen überschreiten, den Browser treffen, sich ständig ändernde UI-Elemente (z. B. HTML, JavaScript) betreffen, die Datenbank, das Dateisystem und möglicherweise Netzwerkdienste betreffen. Wenn eines dieser beweglichen Teile nicht gut spielt, haben Sie einen fehlerhaften Test. Das ist aber auch das Schöne an UI-Tests: Sie testen Ihr System durchgängig. Kein anderer Test bietet Ihnen so viel oder so gründliche Abdeckung. Automatisierte UI-Tests können, wenn sie richtig durchgeführt werden, die besten Elemente in Ihrer Regressionssuite sein.
In den letzten Projekten haben meine UI-Tests über 80% meiner Tests gebildet! Ich sollte auch erwähnen, dass es sich bei diesen Projekten hauptsächlich um CRUD-Anwendungen mit wenig Geschäftslogik handelt, und seien wir ehrlich - die überwiegende Mehrheit der Softwareprojekte fällt in diese Kategorie. Die Geschäftslogik sollte noch auf Einheit getestet werden. Der Rest der Anwendung kann jedoch durch UI-Automatisierung gründlich getestet werden.
UI-Test ist falsch gelaufen
Ich möchte auf das eingehen, was ich falsch gemacht habe, was auch für Entwickler und Tester, die mit der Automatisierung der Benutzeroberfläche beginnen, sehr typisch zu sein scheint.
Was geht also schief und warum? Viele Teams starten die Automatisierung der Benutzeroberfläche mit Bildschirmrekordern. Wenn Sie eine Webautomatisierung mit Selen durchführen, haben Sie höchstwahrscheinlich Selenium IDE verwendet. Von der Selenium IDE-Homepage:
Die Selenium-IDE (Integrated Development Environment) ist das Tool, mit dem Sie Ihre Selenium-Testfälle entwickeln.
Dies ist tatsächlich einer der Gründe, warum das Testen der Benutzeroberfläche zu einer schrecklichen Erfahrung wird: Sie laden einen Bildschirmrekorder herunter und starten ihn, navigieren zu Ihrer Website und klicken, klicken, tippen, tippen, tippen, tippen, tippen, tippen, tippen, tippen und behaupten. Dann spielen Sie die Aufnahme erneut ab und es funktioniert. Süss!! Sie exportieren die Aktionen also als Testskript, fügen sie in Ihren Code ein, wickeln sie in einen Test ein und führen den Test aus. Der Browser wird lebendig, bevor Ihre Augen und Ihre Tests reibungslos funktionieren. Sie werden sehr aufgeregt, teilen Ihre Erkenntnisse mit Ihren Kollegen und zeigen sie Ihrem Chef. Sie werden sehr aufgeregt und sagen: "Automatisieren Sie alles"
Eine Woche später haben Sie 10 automatisierte UI-Tests und alles scheint großartig zu sein. Anschließend werden Sie vom Unternehmen aufgefordert, den Benutzernamen durch die E-Mail-Adresse zu ersetzen, da dies bei den Benutzern zu Verwirrung geführt hat. Dann führen Sie wie jeder andere großartige Programmierer Ihre UI-Testsuite aus, nur um festzustellen, dass 90% Ihrer Tests fehlerhaft sind, da Sie den Benutzer für jeden Test mit dem Benutzernamen anmelden und der Feldname geändert hat und Sie zwei Stunden benötigen, um alle zu ersetzen die Verweise auf den username
in Ihren Tests per email
und um die Tests wieder grün zu bekommen. Das Gleiche passiert immer wieder und irgendwann verbringen Sie Stunden am Tag damit, fehlerhafte Tests zu reparieren: Tests, die nicht fehlerhaft waren, weil etwas mit Ihrem Code schief gelaufen ist; aber weil Sie einen Feldnamen in Ihrer Datenbank / Ihrem Modell geändert oder Ihre Seite leicht umstrukturiert haben. Einige Wochen später beenden Sie die Ausführung Ihrer Tests aufgrund dieser enormen Wartungskosten und kommen zu dem Schluss, dass das Testen der Benutzeroberfläche zum Kotzen ist.
Sie sollten NICHT Selenium IDE oder einen anderen Bildschirmrekorder verwenden, um Ihre Testfälle zu entwickeln. Das heißt, es ist nicht der Bildschirmrekorder selbst, der zu einer spröden Testsuite führt. Es ist der Code, den sie generieren, der inhärente Wartbarkeitsprobleme aufweist. Viele Entwickler haben immer noch eine spröde UI-Testsuite, auch ohne Bildschirmrekorder, nur weil ihre Tests dieselben Attribute aufweisen.
Alle Tests in diesem Artikel wurden gegen die Mvc Music Store-Website geschrieben. Die Website hat einige Probleme, die das Testen der Benutzeroberfläche ziemlich schwierig machen. Deshalb habe ich den Code portiert und die Probleme behoben. Den tatsächlichen Code, gegen den ich diese Tests schreibe, finden Sie im GitHub-Repo für diesen Artikel hier
Wie sieht ein Sprödtest aus? Es sieht ungefähr so aus:
1 |
class BrittleTest |
2 |
{
|
3 |
[Test] |
4 |
public void Can_buy_an_Album_when_registered() |
5 |
{
|
6 |
var driver = Host.Instance.Application.Browser; |
7 |
driver.Navigate().GoToUrl(driver.Url); |
8 |
driver.FindElement(By.LinkText("Admin")).Click(); |
9 |
driver.FindElement(By.LinkText("Register")).Click(); |
10 |
driver.FindElement(By.Id("UserName")).Clear(); |
11 |
driver.FindElement(By.Id("UserName")).SendKeys("HJSimpson"); |
12 |
driver.FindElement(By.Id("Password")).Clear(); |
13 |
driver.FindElement(By.Id("Password")).SendKeys("!2345Qwert"); |
14 |
driver.FindElement(By.Id("ConfirmPassword")).Clear(); |
15 |
driver.FindElement(By.Id("ConfirmPassword")).SendKeys("!2345Qwert"); |
16 |
driver.FindElement(By.CssSelector("input[type=\"submit\"]")).Click(); |
17 |
driver.FindElement(By.LinkText("Disco")).Click(); |
18 |
driver.FindElement(By.CssSelector("img[alt=\"Le Freak\"]")).Click(); |
19 |
driver.FindElement(By.LinkText("Add to cart")).Click(); |
20 |
driver.FindElement(By.LinkText("Checkout >>")).Click(); |
21 |
driver.FindElement(By.Id("FirstName")).Clear(); |
22 |
driver.FindElement(By.Id("FirstName")).SendKeys("Homer"); |
23 |
driver.FindElement(By.Id("LastName")).Clear(); |
24 |
driver.FindElement(By.Id("LastName")).SendKeys("Simpson"); |
25 |
driver.FindElement(By.Id("Address")).Clear(); |
26 |
driver.FindElement(By.Id("Address")).SendKeys("742 Evergreen Terrace"); |
27 |
driver.FindElement(By.Id("City")).Clear(); |
28 |
driver.FindElement(By.Id("City")).SendKeys("Springfield"); |
29 |
driver.FindElement(By.Id("State")).Clear(); |
30 |
driver.FindElement(By.Id("State")).SendKeys("Kentucky"); |
31 |
driver.FindElement(By.Id("PostalCode")).Clear(); |
32 |
driver.FindElement(By.Id("PostalCode")).SendKeys("123456"); |
33 |
driver.FindElement(By.Id("Country")).Clear(); |
34 |
driver.FindElement(By.Id("Country")).SendKeys("United States"); |
35 |
driver.FindElement(By.Id("Phone")).Clear(); |
36 |
driver.FindElement(By.Id("Phone")).SendKeys("2341231241"); |
37 |
driver.FindElement(By.Id("Email")).Clear(); |
38 |
driver.FindElement(By.Id("Email")).SendKeys("chunkylover53@aol.com"); |
39 |
driver.FindElement(By.Id("PromoCode")).Clear(); |
40 |
driver.FindElement(By.Id("PromoCode")).SendKeys("FREE"); |
41 |
driver.FindElement(By.CssSelector("input[type=\"submit\"]")).Click(); |
42 |
|
43 |
Assert.IsTrue(driver.PageSource.Contains("Checkout Complete")); |
44 |
}
|
45 |
}
|
Die BrittleTest
-Klasse finden Sie hier.
Host ist eine statische Klasse mit einer einzigen statischen Eigenschaft: Instance
die bei der Instanziierung IIS Express auf der zu testenden Website startet und Firefox WebDriver an die Browserinstanz bindet. Wenn der Test abgeschlossen ist, werden der Browser und IIS Express automatisch geschlossen.
Dieser Test startet einen Webbrowser, ruft die Startseite der Mvc Music Store-Website auf, registriert einen neuen Benutzer, navigiert zu einem Album, fügt es dem Warenkorb hinzu und checkt aus.
Man könnte argumentieren, dass dieser Test zu viel macht und deshalb spröde ist; Aber die Größe dieses Tests ist nicht der Grund, warum er spröde ist - es ist so geschrieben, dass es ein Albtraum ist, ihn aufrechtzuerhalten.
Es gibt verschiedene Denkansätze zum Testen der Benutzeroberfläche und wie viel jeder Test abdecken sollte. Einige glauben, dass dieser Test zu viel bewirkt, und andere denken, ein Test sollte ein reales Szenario von Ende zu Ende abdecken und dies als perfekten Test betrachten (abgesehen von der Wartbarkeit).
Was ist also falsch an diesem Test?
- Dies ist ein Verfahrenscode. Eines der Hauptprobleme dieses Codierungsstils ist die Lesbarkeit oder das Fehlen derselben. Wenn Sie den Test ändern möchten oder wenn er unterbrochen wird, weil sich eine der beteiligten Seiten geändert hat, fällt es Ihnen schwer, herauszufinden, was geändert werden muss, und eine Grenze zwischen den Funktionsabschnitten zu ziehen. weil es alles ein großer Haufen Code ist, bei dem wir den 'Treiber' dazu bringen, ein Element auf der Seite zu finden und etwas damit zu tun. Keine Modularität.
- Dieser eine Test an sich hat möglicherweise nicht viel Duplizierung, aber ein paar weitere Tests wie diesen, und Sie haben eine Menge duplizierter Selektoren und Logik, um mit Webseiten aus verschiedenen Tests zu interagieren. Beispielsweise wird die Auswahl
By.Id("UserName")
in allen Tests, für die eine Registrierung erforderlich ist, und dupliziertdriver.FindElement(By.Id("UserName")).Clear()
unddriver.FindElement (By.Id("UserName")).SendKeys("
") werden überall dort dupliziert, wo Sie mit dem Textfeld Benutzername interagieren möchten. Dann gibt es das gesamte Anmeldeformular, das Checkout-Formular usw., das in allen Tests wiederholt wird, die mit ihnen interagieren müssen! Doppelter Code führt zu Albträumen bei der Wartbarkeit. - Überall gibt es viele magische Zeichenfolgen, was wiederum ein Problem der Wartbarkeit darstellt.
Testcode ist Code!
Es gibt auch Muster, mit denen Sie wartbarere UI-Tests schreiben können.
Ähnlich wie bei Ihrem eigentlichen Code müssen Sie Ihre Tests beibehalten. Geben Sie ihnen also die gleiche Behandlung.
Was lässt uns an Tests glauben, dass wir bei ihnen auf Qualität verzichten können? Wenn überhaupt, ist eine schlechte Testsuite meiner Meinung nach viel schwieriger zu warten als schlechter Code. Ich habe jahrelang schlechte Arbeitscodes in der Produktion gehabt, die nie kaputt gegangen sind und die ich nie anfassen musste. Sicher, es war hässlich und schwer zu lesen und zu warten, aber es funktionierte und es musste nicht geändert werden, sodass die tatsächlichen Wartungskosten Null waren. Bei schlechten Tests ist die Situation jedoch nicht ganz dieselbe: Weil schlechte Tests brechen und es schwierig sein wird, sie zu reparieren. Ich kann nicht zählen, wie oft Entwickler Tests vermieden haben, weil sie das Schreiben von Tests für eine enorme Zeitverschwendung halten, da die Wartung zu lange dauert.
Testcode ist Code: Wenden Sie SRP auf Ihren Code an? Dann sollten Sie es auch auf Ihre Tests anwenden. Ist Ihr Code DRY? Dann trocknen Sie auch Ihre Tests aus. Wenn Sie keine guten Tests schreiben (UI oder andere), verschwenden Sie viel Zeit damit, sie zu warten.
Es gibt auch Muster, mit denen Sie wartbarere UI-Tests schreiben können. Diese Muster sind plattformunabhängig: Ich habe dieselben Ideen und Muster verwendet, um UI-Tests für WPF-Anwendungen und Webanwendungen zu schreiben, die in ASP.Net und Ruby on Rails geschrieben wurden. Unabhängig von Ihrem Technologie-Stack sollten Sie in der Lage sein, Ihre UI-Tests mit ein paar einfachen Schritten wesentlich wartbarer zu machen.
Einführung in das Seitenobjektmuster
Viele der oben genannten Probleme beruhen auf dem prozeduralen Charakter des Testskripts, und die Lösung ist einfach: Objektorientierung.
Seitenobjekt ist ein Muster, mit dem die Objektorientierung auf UI-Tests angewendet wird. Aus dem Selenium-Wiki:
In der Benutzeroberfläche Ihrer Web-App gibt es Bereiche, mit denen Ihre Tests interagieren. Ein Seitenobjekt modelliert diese einfach als Objekte innerhalb des Testcodes. Dies reduziert die Menge an dupliziertem Code und bedeutet, dass bei Änderungen an der Benutzeroberfläche der Fix nur an einer Stelle angewendet werden muss.
Die Idee ist, dass Sie für jede Seite in Ihrer Anwendung / Website ein Seitenobjekt erstellen möchten. Seitenobjekte sind im Grunde das UI-Automatisierungsäquivalent Ihrer Webseiten.
Ich habe die Logik und Interaktionen aus dem BrittleTest in einige Seitenobjekte umgestaltet und einen neuen Test erstellt, der sie verwendet, anstatt den Webtreiber direkt zu treffen. Den neuen Test finden Sie hier. Der Code wird hier als Referenz kopiert:
1 |
public class TestWithPageObject |
2 |
{
|
3 |
[Test] |
4 |
public void Can_buy_an_Album_when_registered() |
5 |
{
|
6 |
var registerPage = HomePage.Initiate() |
7 |
.GoToAdminForAnonymousUser() |
8 |
.GoToRegisterPage(); |
9 |
|
10 |
registerPage.Username = "HJSimpson"; |
11 |
registerPage.Email = "chunkylover53@aol.com"; |
12 |
registerPage.Password = "!2345Qwert"; |
13 |
registerPage.ConfirmPassword = "!2345Qwert"; |
14 |
|
15 |
var shippingPage = registerPage |
16 |
.SubmitRegistration() |
17 |
.SelectGenreByName("Disco") |
18 |
.SelectAlbumByName("Le Freak") |
19 |
.AddToCart() |
20 |
.Checkout(); |
21 |
|
22 |
shippingPage.FirstName = "Homer"; |
23 |
shippingPage.LastName = "Simpson"; |
24 |
shippingPage.Address = "742 Evergreen Terrace"; |
25 |
shippingPage.City = "Springfield"; |
26 |
shippingPage.State = "Kentucky"; |
27 |
shippingPage.PostalCode = "123456"; |
28 |
shippingPage.Country = "United States"; |
29 |
shippingPage.Phone = "2341231241"; |
30 |
shippingPage.Email = "chunkylover53@aol.com"; |
31 |
shippingPage.PromoCode = "FREE"; |
32 |
var orderPage = shippingPage.SubmitOrder(); |
33 |
Assert.AreEqual(orderPage.Title, "Checkout Complete"); |
34 |
}
|
35 |
}
|
Zugegeben, der Testkörper hat nicht viel an Größe verloren, und tatsächlich musste ich sieben neue Klassen erstellen, um diesen Test zu unterstützen. Trotz der mehr erforderlichen Codezeilen haben wir nur viele Probleme behoben, die der ursprüngliche Sprödtest hatte (mehr dazu weiter unten). Lassen Sie uns zunächst etwas tiefer in das Seitenobjektmuster und das, was wir hier gemacht haben, eintauchen.
Mit dem Seitenobjektmuster erstellen Sie normalerweise eine Seitenobjektklasse pro getesteter Webseite, in der die Klasse Interaktionen mit der Seite modelliert und kapselt. Ein Textfeld auf Ihrer Webseite wird also zu einer Zeichenfolgeeigenschaft im Seitenobjekt. Um dieses Textfeld zu füllen, setzen Sie diese Texteigenschaft einfach auf den gewünschten Wert, anstatt:
1 |
driver.FindElement(By.Id("Email")).Clear(); |
2 |
driver.FindElement(By.Id("Email")).SendKeys("chunkylover53@aol.com"); |
wir können schreiben:
1 |
registerPage.Email = "chunkylover53@aol.com"; |
Dabei ist registerPage
eine Instanz der RegisterPage-Klasse. Ein Kontrollkästchen auf der Seite wird zu einer Bool-Eigenschaft im Seitenobjekt. Wenn Sie das Kontrollkästchen aktivieren oder deaktivieren, müssen Sie diese Boolesche Eigenschaft nur auf true oder false setzen. Ebenso wird ein Link auf der Webseite zu einer Methode im Seitenobjekt, und durch Klicken auf den Link wird die Methode im Seitenobjekt aufgerufen. Also statt:
1 |
driver.FindElement(By.LinkText("Admin")).Click(); |
wir können schreiben:
1 |
homepage.GoToAdminForAnonymousUser(); |
Tatsächlich wird jede Aktion auf unserer Webseite zu einer Methode in unserem Seitenobjekt. Als Reaktion auf diese Aktion (d.h. das Aufrufen der Methode für das Seitenobjekt) erhalten Sie eine Instanz eines anderen Seitenobjekts zurück, das auf die gerade angezeigte Webseite verweist Navigieren Sie zu, indem Sie die Aktion ausführen (z. B. Senden eines Formulars oder Klicken auf einen Link). Auf diese Weise können Sie Ihre Ansichtsinteraktionen einfach in Ihrem Testskript verketten:
1 |
var shippingPage = registerPage |
2 |
.SubmitRegistration() |
3 |
.SelectGenreByName("Disco") |
4 |
.SelectAlbumByName("Le Freak") |
5 |
.AddToCart() |
6 |
.Checkout(); |
Hier werde ich nach der Registrierung des Benutzers zur Startseite weitergeleitet (eine Instanz seines Seitenobjekts wird von der SubmitRegistration
-Methode zurückgegeben). Also rufe ich auf der HomePage-Instanz SelectGenreByName
auf, der auf einen 'Disco'-Link auf der Seite klickt, die eine Instanz von AlbumBrowsePage zurückgibt, und dann rufe ich auf dieser Seite SelectAlbumByName
auf, der auf das 'Le Freak'-Album klickt und eine Instanz von AlbumDetailsPage und so zurückgibt weiter und so weiter.
Ich gebe es zu: Es sind viele Klassen für das, was früher überhaupt keine Klasse war; Aber wir haben viele Vorteile aus dieser Praxis gezogen. Erstens ist der Code nicht mehr prozedural. Wir haben ein gut enthaltenes Testmodell, bei dem jedes Objekt eine gute Kapselung der Interaktion mit einer Seite bietet. Wenn sich beispielsweise etwas in Ihrer Registrierungslogik ändert, müssen Sie nur Ihre RegisterPage-Klasse ändern, anstatt Ihre gesamte Testsuite durchzugehen und jede einzelne Interaktion mit der Registrierungsansicht zu ändern. Diese Modularität sorgt auch für eine gute Wiederverwendbarkeit: Sie können Ihre ShoppingCartPage
überall dort wiederverwenden, wo Sie mit dem Warenkorb interagieren müssen. In einer einfachen Praxis des Übergangs vom prozeduralen zum objektorientierten Testcode haben wir fast drei der vier Probleme mit dem anfänglichen Sprödtest beseitigt, nämlich den prozeduralen Code sowie die Logik- und Selektorduplikation. Wir haben noch ein bisschen Duplizierung, die wir in Kürze beheben werden.
Wie haben wir diese Seitenobjekte tatsächlich implementiert? Ein Seitenobjekt im Stammverzeichnis ist nichts anderes als ein Wrapper um die Interaktionen, die Sie mit der Seite haben. Hier habe ich gerade UI-Interaktionen unserer spröden Tests extrahiert und sie in ihre eigenen Seitenobjekte eingefügt. Zum Beispiel wurde die Registrierungslogik in eine eigene Klasse namens RegisterPage
extrahiert, die folgendermaßen aussah:
1 |
public class RegisterPage : Page |
2 |
{
|
3 |
public HomePage SubmitRegistration() |
4 |
{
|
5 |
return NavigateTo<HomePage>(By.CssSelector("input[type='submit']")); |
6 |
}
|
7 |
|
8 |
public string Username { set { Execute(By.Name("UserName"), e => { e.Clear(); e.SendKeys(value);}); } } |
9 |
public string Email { set { Execute(By.Name("Email"), e => { e.Clear(); e.SendKeys(value);}); } } |
10 |
public string ConfirmPassword { set { Execute(By.Name("ConfirmPassword"), e => { e.Clear(); e.SendKeys(value);}); } } |
11 |
public string Password { set { Execute(By.Name("Password"), e => { e.Clear(); e.SendKeys(value);}); } } |
12 |
}
|
Ich habe eine Page
-Superklasse erstellt, die sich um einige Dinge kümmert, wie z. B. NavigateTo
, mit dessen Hilfe Sie zu einer neuen Seite navigieren können, indem Sie eine Aktion ausführen, und Execute
, die einige Aktionen für ein Element ausführt. Die Page
-Klasse sah aus wie:
1 |
public class Page |
2 |
{
|
3 |
protected RemoteWebDriver WebDriver |
4 |
{
|
5 |
get { return Host.Instance.WebDriver; } |
6 |
}
|
7 |
|
8 |
public string Title { get { return WebDriver.Title; }} |
9 |
|
10 |
public TPage NavigateTo<TPage>(By by) where TPage:Page, new() |
11 |
{
|
12 |
WebDriver.FindElement(by).Click(); |
13 |
return Activator.CreateInstance<TPage>(); |
14 |
}
|
15 |
|
16 |
public void Execute(By by, Action<IWebElement> action) |
17 |
{
|
18 |
var element = WebDriver.FindElement(by); |
19 |
action(element); |
20 |
}
|
21 |
}
|
Um im BrittleTest
mit einem Element zu interagieren, haben wir FindElement
einmal pro Aktion ausgeführt. Die Execute
-Methode bietet neben der Zusammenfassung der Interaktion des Webtreibers einen zusätzlichen Vorteil, der es ermöglicht, ein Element, das eine teure Aktion sein kann, einmal auszuwählen und mehrere Aktionen auszuführen:
1 |
driver.FindElement(By.Id("Password")).Clear(); |
2 |
driver.FindElement(By.Id("Password")).SendKeys("!2345Qwert"); |
wurde ersetzt durch:
1 |
Execute(By.Name("Password"), e => { e.Clear(); e.SendKeys("!2345Qwert");}) |
Bei einem zweiten Blick auf das RegisterPage
-Seitenobjekt oben haben wir noch ein bisschen Duplizierung. Testcode ist Code und wir möchten keine Duplizierung in unserem Code. Also lasst uns das umgestalten. Wir können den Code, der zum Ausfüllen eines Textfelds erforderlich ist, in eine Methode in der Page
-Klasse extrahieren und diesen einfach aus Seitenobjekten aufrufen. Die Methode könnte wie folgt implementiert werden:
1 |
public void SetText(string elementName, string newText) |
2 |
{
|
3 |
Execute(By.Name(elementName), e => |
4 |
{
|
5 |
e.Clear(); |
6 |
e.SendKeys(newText); |
7 |
} ); |
8 |
}
|
Und jetzt können die Eigenschaften auf RegisterPage
auf Folgendes verkleinert werden:
1 |
public string Username { set { SetText("UserName", value); } } |
Sie können auch eine fließende API erstellen, damit der Setter besser liest (z.B. Fill("UserName").With(value)
), aber das überlasse ich Ihnen.
Wir machen hier nichts Außergewöhnliches. Nur einfaches Refactoring unseres Testcodes, wie wir es immer für unseren, errrr, "anderen" Code getan haben!!
Den vollständigen Code für Page
- und RegisterPage
-Klassen finden Sie hier und hier.
Stark typisiertes Seitenobjekt
Wir haben Verfahrensprobleme mit dem Sprödtest gelöst, wodurch der Test lesbarer, modularer, trockener und effektiver wartbar wurde. Es gibt ein letztes Problem, das wir nicht behoben haben: Es gibt immer noch überall viele magische Fäden. Kein Albtraum, aber dennoch ein Problem, das wir beheben konnten. Geben Sie stark typisierte Seitenobjekte ein!
Dieser Ansatz ist praktisch, wenn Sie ein MV* -Framework für Ihre Benutzeroberfläche verwenden. In unserem Fall verwenden wir ASP.Net MVC.
Werfen wir noch einen Blick auf die RegisterPage
:
1 |
public class RegisterPage : Page |
2 |
{
|
3 |
public HomePage SubmitRegistration() |
4 |
{
|
5 |
return NavigateTo<HomePage>(By.CssSelector("input[type='submit']")); |
6 |
}
|
7 |
|
8 |
public string Username { set { SetText("UserName", value); } } |
9 |
public string Email { set { SetText("Email", value); } } |
10 |
public string ConfirmPassword { set { SetText("ConfirmPassword", value); } } |
11 |
public string Password { set { SetText("Password", value); } } |
12 |
}
|
Diese Seite modelliert die Register-Ansicht in unserer Web-App (kopieren Sie hier einfach das oberste Bit):
1 |
@model MvcMusicStore.Models.RegisterModel |
2 |
|
3 |
@{ |
4 |
ViewBag.Title = "Register"; |
5 |
}
|
Hmmm, was ist das für ein RegisterModel
dort? Es ist das Ansichtsmodell für die Seite: das M
in der MVC
. Hier ist der Code (ich habe die Attribute entfernt, um das Rauschen zu reduzieren):
1 |
public class RegisterModel |
2 |
{
|
3 |
public string UserName { get; set; } |
4 |
public string Email { get; set; } |
5 |
public string Password { get; set; } |
6 |
public string ConfirmPassword { get; set; } |
7 |
}
|
Das kommt mir sehr bekannt vor, nicht wahr? Es hat dieselben Eigenschaften wie die RegisterPage
-Klasse, was nicht überraschend ist, wenn man bedenkt, dass RegisterPage
basierend auf dieser Ansicht und diesem Ansichtsmodell erstellt wurde. Mal sehen, ob wir Ansichtsmodelle nutzen können, um unsere Seitenobjekte zu vereinfachen.
Ich habe eine neue Page
-Superklasse erstellt. aber eine generische. Sie können den Code hier sehen:
1 |
public class Page<TViewModel> : Page where TViewModel: class, new() |
2 |
{
|
3 |
public void FillWith(TViewModel viewModel, IDictionary<Type, Func<object, string>> propertyTypeHandling = null) |
4 |
{
|
5 |
// removed for brevity
|
6 |
}
|
7 |
}
|
Die Page<TViewModel>
-Klasse unterklassifiziert die alte Page
-Klasse und bietet alle ihre Funktionen. Es gibt aber auch eine zusätzliche Methode namens FillWith
, die die Seite mit der bereitgestellten Ansichtsmodellinstanz ausfüllt! Jetzt sieht meine RegisterPage
-Klasse so aus:
1 |
public class RegisterPage : Page<RegisterModel> |
2 |
{
|
3 |
public HomePage CreateValidUser(RegisterModel model) |
4 |
{
|
5 |
FillWith(model); |
6 |
return NavigateTo<HomePage>(By.CssSelector("input[type='submit']")); |
7 |
}
|
8 |
}
|
Ich habe alle Seitenobjekte dupliziert, um beide Variationen anzuzeigen und die Codebasis für Sie einfacher zu befolgen. In Wirklichkeit benötigen Sie jedoch eine Klasse für jedes Seitenobjekt.
Nachdem ich meine Seitenobjekte in generische konvertiert habe, sieht der Test nun wie folgt aus:
1 |
public class StronglyTypedPageObjectWithComponent |
2 |
{
|
3 |
[Test] |
4 |
public void Can_buy_an_Album_when_registered() |
5 |
{
|
6 |
var orderedPage = HomePage.Initiate() |
7 |
.GoToAdminForAnonymousUser() |
8 |
.GoToRegisterPage() |
9 |
.CreateValidUser(ObjectMother.CreateRegisterModel()) |
10 |
.SelectGenreByName("Disco") |
11 |
.SelectAlbumByName("Le Freak") |
12 |
.AddAlbumToCart() |
13 |
.Checkout() |
14 |
.SubmitShippingInfo(ObjectMother.CreateShippingInfo(), "Free"); |
15 |
|
16 |
Assert.AreEqual("Checkout Complete", orderedPage.Title); |
17 |
}
|
18 |
}
|
Das war's - der gesamte Test! Viel besser lesbar, trocken und wartbar, nicht wahr?
Die ObjectMother
-Klasse, die ich im Test verwende, ist eine Objektmutter, die Testdaten bereitstellt (Code finden Sie hier).
1 |
public class ObjectMother |
2 |
{
|
3 |
public static Order CreateShippingInfo() |
4 |
{
|
5 |
var shippingInfo = new Order |
6 |
{
|
7 |
FirstName = "Homer", |
8 |
LastName = "Simpson", |
9 |
Address = "742 Evergreen Terrace", |
10 |
City = "Springfield", |
11 |
State = "Kentucky", |
12 |
PostalCode = "123456", |
13 |
Country = "United States", |
14 |
Phone = "2341231241", |
15 |
Email = "chunkylover53@aol.com" |
16 |
};
|
17 |
|
18 |
return shippingInfo; |
19 |
}
|
20 |
|
21 |
public static RegisterModel CreateRegisterModel() |
22 |
{
|
23 |
var model = new RegisterModel |
24 |
{
|
25 |
UserName = "HJSimpson", |
26 |
Email = "chunkylover53@aol.com", |
27 |
Password = "!2345Qwert", |
28 |
ConfirmPassword = "!2345Qwert" |
29 |
};
|
30 |
|
31 |
return model; |
32 |
}
|
33 |
}
|
Halten Sie nicht am Seitenobjekt an
Einige Webseiten sind sehr groß und komplex. Früher habe ich gesagt, Testcode ist Code und wir sollten ihn als solchen behandeln. Normalerweise teilen wir große und komplexe Webseiten in kleinere und in einigen Fällen wiederverwendbare (Teil-) Komponenten auf. Auf diese Weise können wir eine Webseite aus kleineren, besser verwaltbaren Komponenten zusammenstellen. Wir sollten dasselbe für unsere Tests tun. Dazu können wir Seitenkomponenten verwenden.
Eine Seitenkomponente ähnelt einem Seitenobjekt: Es ist eine Klasse, die die Interaktion mit einigen Elementen auf einer Seite kapselt. Der Unterschied besteht darin, dass es mit einem kleinen Teil einer Webseite interagiert: Es modelliert ein Benutzersteuerelement oder eine Teilansicht, wenn Sie so wollen. Ein gutes Beispiel für eine Seitenkomponente ist eine Menüleiste. Eine Menüleiste wird normalerweise auf allen Seiten einer Webanwendung angezeigt. Sie möchten den Code, der für die Interaktion mit dem Menü in jedem einzelnen Seitenobjekt erforderlich ist, nicht wirklich wiederholen. Stattdessen können Sie eine Menüseitenkomponente erstellen und aus Ihren Seitenobjekten verwenden. Sie können auch Seitenkomponenten verwenden, um Datenraster auf Ihren Seiten zu verarbeiten, und um noch einen Schritt weiter zu gehen, kann die Rasterseitenkomponente selbst aus Rasterzeilenseitenkomponenten bestehen. Im Fall von Mvc Music Store könnten wir eine TopMenuComponent
und eine SideMenuComponent
haben und diese von unserer HomePage
aus verwenden.
Wie in Ihrer Webanwendung können Sie auch ein LayoutPage
-Seitenobjekt erstellen, das Ihr Layout / Ihre Masterseite modelliert, und dieses als Oberklasse für alle anderen Seitenobjekte verwenden. Die Layoutseite würde dann aus Menüseitenkomponenten bestehen, so dass alle Seiten die Menüs aufrufen können. Ich denke, eine gute Faustregel wäre, eine Seitenkomponente pro Teilansicht, ein Layout-Seitenobjekt pro Layout und ein Seitenobjekt pro Webseite zu haben. Auf diese Weise wissen Sie, dass Ihr Testcode so detailliert und gut zusammengesetzt ist wie Ihr Code.
Einige Frameworks für UI-Tests
Was ich oben gezeigt habe, war eine sehr einfache und erfundene Stichprobe mit einigen unterstützenden Klassen als Infrastruktur für Tests. In Wirklichkeit sind die Anforderungen für UI-Tests viel komplexer: Es gibt komplexe Steuerelemente und Interaktionen, Sie müssen auf Ihre Seiten schreiben und von diesen lesen, Sie müssen sich mit Netzwerklatenzen befassen und die Kontrolle über AJAX und andere Javascript-Interaktionen haben. müssen verschiedene Browser auslösen und so weiter, was ich in diesem Artikel nicht erklärt habe. Obwohl es möglich ist, all dies zu codieren, können Sie durch die Verwendung einiger Frameworks viel Zeit sparen. Hier sind die Frameworks, die ich sehr empfehlen kann:
Frameworks für .Net:
- Seleno ist ein Open Source-Projekt von TestStack, mit dem Sie automatisierte UI-Tests mit Selenium schreiben können. Es konzentriert sich auf die Verwendung von Seitenobjekten und Seitenkomponenten sowie auf das Lesen von und Schreiben auf Webseiten mit stark typisierten Ansichtsmodellen. Wenn Ihnen das, was ich in diesem Artikel getan habe, gefallen hat, wird Ihnen auch Seleno gefallen, da der größte Teil des hier gezeigten Codes aus der Seleno-Codebasis entlehnt wurde.
- White ist ein Open Source-Framework von TestStack zur Automatisierung von Rich Client-Anwendungen auf der Basis von Win32-, WinForms-, WPF-, Silverlight- und SWT-Plattformen(Java).
Offenlegung: Ich bin Mitbegründer und Mitglied des Entwicklungsteams der TestStack-Organisation.
Frameworks für Ruby:
- Capybara ist ein Akzeptanztest-Framework für Webanwendungen, mit dem Sie Webanwendungen testen können, indem Sie simulieren, wie ein echter Benutzer mit Ihrer App interagieren würde.
- Poltergeist ist ein Fahrer für Capybara. Sie können Ihre Capybara-Tests in einem von PhantomJS bereitgestellten kopflosen WebKit-Browser ausführen.
- Das page-object (ich habe dieses Juwel nicht persönlich verwendet) ist ein einfaches Juwel, das beim Erstellen flexibler Seitenobjekte zum Testen browserbasierter Anwendungen hilft. Ziel ist es, die Erstellung von Abstraktionsschichten in Ihren Tests zu vereinfachen, um die Tests von dem zu testenden Element zu entkoppeln und eine einfache Schnittstelle zu den Elementen auf einer Seite bereitzustellen. Es funktioniert sowohl mit Watir-Webdriver als auch mit Selenium-Webdriver.
Abschluss
Wir begannen mit einer typischen Erfahrung in der UI-Automatisierung, erklärten, warum UI-Tests fehlschlagen, lieferten ein Beispiel für einen spröden Test, diskutierten seine Probleme und lösten sie anhand einiger Ideen und Muster.
Wenn Sie einen Punkt aus diesem Artikel entnehmen möchten, sollte dies lauten: Testcode ist Code. Wenn Sie darüber nachdenken, war alles, was ich in diesem Artikel getan habe, die guten Codierungs- und objektorientierten Praktiken, die Sie bereits kennen, auf einen UI-Test anzuwenden.
Es gibt noch viel zu lernen über UI-Tests und ich werde versuchen, einige der fortgeschritteneren Tipps in einem zukünftigen Artikel zu diskutieren.
Viel Spaß beim Testen!