Autor
Felix Schaffernicht
Team Leader Software Development
bei SYZYGY Techsolutions
Lesedauer
8 Minuten
Publiziert
11. Juni 2024
In der Welt des Softwaretestings ist Klarheit der Schlüssel zum Erfolg. Fachliche End-to-End (E2E)-Tests sind dabei ein Eckpfeiler, um sicherzustellen, dass unsere Anwendungen nicht nur funktionieren, sondern auch den Anforderungen und Erwartungen unserer Benutzer:innen gerecht werden. In diesem Artikel werden wir einen genaueren Blick darauf werfen, wie wir mithilfe von Playwright und Fixtures die Fachlichkeit unserer E2E-Tests besser abbilden können.
Widerstandsfähigkeit gegenüber Änderungen
Die einzige Konstante in der Softwareentwicklung ist die Veränderung. Indem wir uns bei E2E-Tests auf die Fachlichkeit konzentrieren, machen wir unsere Tests widerstandsfähiger gegenüber den sich ständig ändernden Implementierungsdetails. Dies bedeutet, dass unsere Tests auch dann noch funktionieren sollten, wenn wir internen Code umstrukturieren oder aktualisieren.
Damit wir nicht ständig unsere Tests anpassen müssen, wenn wir Änderungen am Code durchführen, können wir uns sogenannte Fixtures zunutze machen.
Vorteile von Fixtures
Im Kontext von E2E-Tests werden Fixtures typischerweise verwendet, um die Ausgangsbedingungen für Tests zu konfigurieren, wie z.B. das Anmelden in einem Benutzerkonto, das Bereitstellen von Testdaten in einer Datenbank oder das Starten einer Anwendung in einem bestimmten Zustand.
Dabei bieten Fixtures mehrere Vorteile:
- Wiederverwendbarkeit: Fixtures ermöglichen es, Testumgebungen und -daten auf einfache Weise wiederzuverwenden, was die Effizienz bei der Entwicklung und Wartung von Tests erhöht.
- Konsistenz: Durch die Verwendung von Fixtures wird sichergestellt, dass Tests unter denselben Bedingungen ausgeführt werden, was die Zuverlässigkeit und Reproduzierbarkeit der Testergebnisse verbessert.
- Modularität: Fixtures können modular gestaltet werden, sodass sie unabhängig von den Tests entwickelt und gewartet werden können. Dies fördert eine klare Trennung von Testlogik und Setup-/Aufräumvorgängen.
- Komposition: Fixtures lassen sich miteinander kombinieren oder können voneinander abhängig sein.
- Lesbarkeit und Wartbarkeit: Durch die Auslagerung von Setup- und Aufräumlogik in Fixtures können Tests sauberer und leichter verständlich gemacht werden, was ihre Wartbarkeit erhöht.
Neben dem, was man so online findet …
In vielen Beispielen, die man online findet, sind Fixtures komplette Seiten (pages). Und es gibt Beispiele, die dann heißen: `TodoPage` oder `UserSettingsPage`. Fixtures können aber auch einfach Daten sein:
export const test = base.extend<{ data: number }>({
data: 42,
})
Welche wir in Tests als Argumente entgegennehmen können:
test('should do something', async ({ page, data }) => {
Fixtures machen also nicht unbedingt Vorgaben bezüglich ihres Designs. Können wir Fixtures also auch nutzen, um die Lesbarkeit unserer Tests zu erhöhen und nur die Fachlichkeit abzubilden? Natürlich!
Übrigens: Das `page` argument ist auch nichts anderes als ein Fixture!
Beispiele
Sehen wir uns ein paar Beispiele mit Playwright an. Bleiben wir bei einem einfachen Fall den wir testen wollen:
Szenario: Nach Benutzen eines Filters möchten wir testen, dass eine spezifische Nachricht angezeigt wird, wenn keine Daten gefunden werden.
Beispiel ohne Abstraktion
Hier benutzen wir playwright ohne Helferfunktionen oder Fixtures. Wir holen uns die Seitenelemente, mit denen wir arbeiten möchten, direkt. Dazu nutzen wir die gängigen Methoden die mit `page` fixture mitkommen:
test('should show noDataFound message on unsuccessful filtering', async ({
page,
}) => {
await expect(page.getByText('Keine Daten vorhanden')).not.toBeVisible()
await page.getByTestId('filter-container').click()
await page.getByPlaceholder('Schlagwortsuche (z.b. Sketch)').fill('cypress')
await page.getByText('Übernehmen').click()
await page.getByText('Adopt').scrollIntoViewIfNeeded()
await expect(page.getByText('Keine Daten vorhanden')).toBeVisible()
})
Zugegeben, dieser Test ist nicht unleserlich, müsste aber schon angepasst werden, sollte sich der Source Code ändern. Auch ist hier nicht klar was zum Beispiel `page.getByText(‘Übernehmen’)` ist. Oder was das hier ist: `page.getByText(‘Adopt’)`. Wir können nur sehen, wie wir gewisse Elemente auf der Seite suchen. Was sie in ihrer Fachlichkeit sind oder tun, ist aber nicht ersichtlich.
Wir könnten Abhilfe schaffen, indem wir mit einfachen Funktionen arbeiten.
Funktionen
test('should show noDataFound message on unsuccessful filtering', async ({
page,
}) => {
await expect(getNoDataFoundMessageInTabs(page)).not.toBeVisible()
await openFilter(page)
await fillFilterInput(page, 'cypress')
await applyFilter(page)
await getTabs(page).scrollIntoViewIfNeeded()
await expect(getNoDataFoundMessageInTabs(page)).toBeVisible()
})
Hier haben wir einen schon viel sprechenderen Test, welcher aber noch ein paar Nachteile hat:
- Der Test erfordert eine ständige Übergabe des page-Objekts, damit Funktionen ohne Seiteneffekte sind.
- Um Kontext zu geben, muss der Funktionsname sprechend sein. Das kann schnell in sehr lange Funktionsnamen münden.
Page Object Models (POM)
Hier bedienen wir uns dem so genannten Page Object Model (POM) um Implementierungsdetails zu kapseln:
test('should show noDataFound message on unsuccessful filtering', async ({
page,
}) => {
const tabs = new Tabs(page)
const filter = new Filter(page)
await expect(tabs.getNoDataFoundMessage()).not.toBeVisible()
await filter.open()
await filter.fillInput('cypress')
await filter.applyAndClose()
await tabs.getTabs().scrollIntoViewIfNeeded()
await expect(tabs.getNoDataFoundMessage()).toBeVisible()
})
Damit haben wir folgende Vorteile:
- Bessere Abstraktion von Seitenelementen und Aktionen.
- Methoden beziehen sich auf Objekte und können dadurch noch sprechender werden, wie z.B. `fillInput` an Stelle von `fillFilterInput`.
- Reduzierte Abhängigkeit vom page-Objekt. Das page-Objekt wird nur einmal während der Initialisierung der Klassen übergeben. Danach können wir einfach damit weiterarbeiten.
Fixtures
Mit Fixtures bekommen wir die beste Test API.
test('should show noDataFound message on unsuccessful filtering', async ({
filter,
tabs,
}) => {
await expect(tabs.getNoDataFoundMessage()).not.toBeVisible()
await filter.open()
await filter.fillInput('cypress')
await filter.applyAndClose()
await tabs.getTabs().scrollIntoViewIfNeeded()
await expect(tabs.getNoDataFoundMessage()).toBeVisible()
})
So erhalten wir folgende Vorteile:
- Alle Vorteile des Page Object Models (POM).
- Ermöglicht das Vermeiden der Initialisierung von Klassen in jedem Test.
- Es werden nur Objekte initialisiert, welche auch benutzt werden.
- Klarheit über die verwendeten Objekte durch Übergabe als Testparameter.
- Keine Seiteneffekte. Fixtures werden als Argumente in jeden Test übergeben.
Beispiel Filter Class
Sowohl für das POM-Beispiel als auch für Fixtures könnte die Filter Klasse in etwa so aussehen:
export class Filter {
private readonly inputBox: Locator
constructor(public readonly page: Page) {
this.inputBox = this.page.getByPlaceholder('Schlagwortsuche (z.b. Sketch)')
}
async open() {
await this.page.getByTestId('filter-button').click()
}
async fillInput(text: string) {
this.inputBox.fill(text)
}
}
Der Vorteil liegt eigentlich auf der Hand. Sollte sich mal der Platzhalter Text in der Applikation ändern, müsste man nicht jeden Test, welcher mit dem Filter Input interagiert ändern, sondern nur die Filter Klasse. Ganz im Sinne von Separation of Concerns, kümmert sich die Filter-Klasse um die Implementierungsdetails, während die Tests die Fachlichkeit beinhalten.
Zusammenführen aller Fixtures
Wir können alle Fixtures miteinander in einer Datei kombinieren und eine Konstante `test` exportieren:
export const test = base.extend<TestSuits>({
page: async ({ page }, use) => {
await page.goto('/')
await use(page)
},
filter: async ({ page }, use) => {
await use(new Filter(page))
},
tabs: async ({ page }, use) => {
await use(new Tabs(page))
},
})
Damit haben wir in jedem Test die Möglichkeit neben `page` auch `filter` oder `tabs` zu benutzen:
import { test } from './fixtures'
test('should do something', async ({ page, filter, tabs }) => {
Fazit
Die Verwendung von Fixtures kann die Effizienz und Wartbarkeit unserer Tests erheblich verbessern. Dabei sind sie nicht darauf beschränkt komplette Seiten abzubilden. Sie können auch schlicht dazu dienen Implementierungsdetails zu abstrahieren und Tests im Allgemeinen leserlicher und resilienter zu gestalten.
Head of Technology