E2E-Testing mit Playwright: Der Weg der Mitte

Seite 3: Fortgeschrittenes Beispiel und Ausblick

Inhaltsverzeichnis

Die gezeigten Befehle erlauben bereits, vielfältige Tests zu schreiben. Ein weiterer Test soll nun das Verwenden fortgeschrittener Features zeigen. Der Test soll mit einem eingeloggten Benutzer funktionieren, der Login aber nicht jedes Mal durchgeführt werden, sondern nur einmal, und das entsprechende Cookie oder andere Sitzungsdaten sollen für weitere Tests wiederverwendet werden.

Der eigentliche Test soll zum "Holidays"-Bereich wechseln und für Wien die Reisebroschüre anfordern. Bei der Eingabe der Adresse soll der Netzwerk-Request auf zweifache Art gemockt werden: Einmal soll es eine valide und einmal eine invalide Anfrage sein. Schlussendlich soll auch das Page-Object-Muster für "Holidays" und die Broschürenanfrage verwendet werden. Zusätzlich soll Playwright die URL der Anwendung aus der Konfiguration automatisch auslesen. Hierfür wird zunächst eine neue Datei mit dem Namen advanced-holidays.spec.ts erstellt und der in Listing 3 gezeigte Inhalt eingefügt.

Das Beispiel importiert Dateien, die später hinzugefügt werden. Aus diesem Grund ist Listing 3 unvollständig und lässt sich noch nicht ausführen.

import { test as base, expect, Page } from '@playwright/test';
import { GetBrochure } from './page-objects/get-brochure';
import { Holidays } from './page-objects/holidays';

const test = base.extend<{ holidays: Holidays; getBrochure: GetBrochure }>({
  holidays: async ({ page }, use) => {
    await use(new Holidays(page));
  },
  getBrochure: async ({ page }, use) => {
    await use(new GetBrochure(page));
  },
});

test.describe('Advanced Holidays', () => {
  test('sanity test', async ({ page }) => {
    await page.goto('');
    await expect(page.locator('data-testid=p-username')).toHaveText(
      'Welcome John List'
    );
  });

  for (const { response, lookupResult } of [
    { response: [], lookupResult: 'Address not found' },
    { response: [true], lookupResult: 'Brochure sent' },
  ]) {
    test(`should return ${lookupResult} for nominatim response ${response}`, async ({
      page,
      holidays,
      getBrochure,
    }) => {
      await page.goto('');
      await holidays.navigateTo();
      page.route(/nominatim/, (route) =>
        route.fulfill({
          status: 200,
          body: JSON.stringify(response),
        })
      );
      await holidays.clickGetBrochure('London');
      await getBrochure.setAddress('Domgasse 5');
      await getBrochure.search();
      await expect(getBrochure.lookupResult).toHaveText(lookupResult);
    });
  }
});

Listing 3: Erstellen der Datei advanced-holidays.spec.ts

Test Fixtures

Das ist eine ganze Menge Code, weshalb im Folgenden ein Feature nach dem anderen behandelt werden soll. Mit dem extend-Befehl lassen sich Test Fixtures erstellen (Listing 4). Das heißt, die Objekte holidays und getBrochure sind ab sofort in jedem Test in dieser Datei vorhanden und lassen sich verwenden. Sie stellen Instanzen der Page-Object-Klassen Holidays und GetBrochure dar, die später noch genauer betrachtet werden.

const test = base.extend<{ holidays: Holidays; getBrochure: GetBrochure }>({
  holidays: async ({ page }, use) => {
    await use(new Holidays(page));
  },
  getBrochure: async ({ page }, use) => {
    await use(new GetBrochure(page));
  },
});

Listing 4: Test Fixtures

Storage

Da jeder Test in diesem Beispiel mit einem eingeloggten Benutzer starten soll, sind einige Änderungen an der Konfiguration vorzunehmen. In der Testanwendung kommt Auth0 zum Einsatz, das für die Speicherung Cookies nutzt. Auth0 ist ein bekannter Service für Authentifizierung und Autorisierung mit Anbindung an soziale Netzwerke. Der Code für den Login wird in eine Datei ausgelagert, die jedes Mal zu Beginn der Tests pro Browsertyp gestartet werden soll. Dazu ist zunächst im Root-Verzeichnis die Datei ./global-setup.ts anzulegen (Listing 5).

import { chromium, firefox, webkit } from '@playwright/test';

async function globalSetup() {
  for (const browserType of [chromium, firefox, webkit]) {
    const browser = await browserType.launch();
    const page = await browser.newPage();
    await page.goto('https://genuine-narwhal-f0f8ad.netlify.app/');
    await page.locator('data-testid=btn-sign-in').click();
    await page.locator('input[name=email]').fill('john.list@host.com');
    await page.locator('input[name=password]').fill('John List');
    await page.locator('button[type=submit]').click();
    await page
      .locator('data-testid=p-username', { hasText: 'John List' })
      .waitFor();

    await page
      .context()
      .storageState({ path: `john-list.${browserType.name()}.json` });
    await page.close();
  }
}

export default globalSetup;

Listing 5: Erstellen der Datei ./global-setup.ts

Anschließend ist es nötig, die Datei in der Konfiguration zu aktivieren. Dazu öffnet man die playwright.config.ts und fügt der config-Variable eine neue Property namens globalSetup mit dem Wert require.resolve('./global-setup.ts') hinzu (Listing 6).

const config: PlaywrightTestConfig = {
  globalSetup: require.resolve('./global-setup.ts'),
  testDir: './tests',
  // ...
};

Listing 6: Aktivieren der Konfiguration in playwright.config.ts

Zudem ist es erforderlich, pro Browser die entsprechende json-Datei anzugeben, aus der die Session-Dateien herausgelesen werden sollen. Dafür gibt es die storagePath-Variable, die pro Browser in der projects-Variable zu setzen ist.

Ab sofort startet jeder Test mit einem eingeloggten Benutzer.

Page Objects

Das Einbinden von Page Objects in die Test Fixtures war bereits zu sehen. Diese Klassen zentralisieren die Selektions- und Aktionslogik, damit die einzelnen Tests bei Änderungen der Selektoren unberührt bleiben können. Es sind die in Listing 7 und 8 gezeigten zwei Dateien mit entsprechendem Inhalt zu erstellen.

import { Page } from '@playwright/test';

export class Holidays {
  constructor(private page: Page) {}

  async navigateTo() {
    await this.page.locator('data-testid=btn-holidays').click();
  }

  async clickGetBrochure(holiday: string) {
    await this.page
      .locator('data-testid=holiday-card', {
        has: this.page.locator(`text=${holiday}`),
      })
      .locator('data-testid=btn-brochure')
      .click();
  }
}

Listing 7: Erstellen von ./tests/page-object/holidays.ts

import { Page } from '@playwright/test';

export class GetBrochure {
  readonly lookupResult;
  constructor(private page: Page) {
    this.lookupResult = this.page.locator('data-testid=lookup-result');
  }

  async setAddress(address: string) {
    await this.page.locator('data-testid=address').fill(address);
  }

  async search() {
    await this.page.locator('data-testid=btn-search').click();
  }
}

Listing 8: Erstellen von ./tests/page-objects/get-brochure.ts

Listing 8 zeigt, dass Aktionen asynchrone Methoden sind und Lokatoren als read-only Properties nach außen gegeben werden. Die Instanziierung erfordert das Page-Objekt des jeweiligen Tests. Darum kümmern sich bereits die Test Fixtures.

Netzwerk-Mocking

In der Praxis ist häufig aus verschiedensten Gründen das Mocken von Netzwerkrequests nötig. Auch das ist mit Playwright möglich, wie die Verwendung des Befehls page.route zeigt.

Basis-URL

In Listing 3 ist der page.goto-Befehl mit einem leeren String versehen. Playwright greift auf die URL laut Konfiguration zu. Um das zu erreichen, ist ein Subproperty von use im Playwright-Konfigurationsobjekt hinzuzufügen (Listing 9).

const config: PlaywrightTestConfig = {
  globalSetup: require.resolve('./global-setup.ts'),
  testDir: './tests',
  // ...
  use: {
    baseURL: 'https://genuine-narwhal-f0f8ad.netlify.app/',
    // ...
  },
};

Listing 9: Konfigurieren einer Basis-URL

Beim Experimentieren mit neuen Funktionen ist es hilfreich, die Dokumentation stets geöffnet zu haben. Zum Abschluss folgt eine Auflistung weiterer Funktionen, die es nicht in diesen Artikel geschafft haben, die aber beim Testen nützlich sein können.

  1. Komponententesting: Für Vue, Svelte und React bietet Playwright ab Version 1.22 einen experimentellen Komponententest an. Dadurch entfällt die Notwendigkeit, die komplette Anwendung hochzufahren. Stattdessen lassen sich gezielt einzelne Komponenten oder Komponentengruppen isoliert testen.
  2. Coverage: Test Coverage ist implizit eingebaut. Allerdings kann hier je nach Framework ein manuelles Eingreifen erforderlich sein.
  3. Visuelle Regression: Playwright erstellt einen Screenshot einer gerenderten Komponente und bietet Matcher toMatchSnapshot für einen pixelgenauen Abgleich an.
  4. Android: Chromium, Firefox und WebKit sind die primären Browser. Es gibt aber mittlerweile einen experimentellen Support für Android.
  5. Sharding: Verteilung der Last auf unterschiedliche Maschinen. Praktisch in der Continuous Integration (CI).
  6. Video: Playwright bietet neben dem Tracer auch die Möglichkeit einer Videoaufzeichnung.
  7. Geolocation: Durch die externe Browsersteuerung ist es sehr einfach möglich, die Geolocation so einzustellen, wie man sie gerade benötigt.