Automatizované testování s elegancí: Bezchybný vývoj pomocí Cucumber a Playwright

Martin Topolánek
1/2/2024

Ukázka Gherkin syntaxe

V dnešním dynamickém světě vývoje softwaru je klíčové zajistit, aby aplikace byly nejen funkční, ale také odolné vůči nečekaným změnám. V tomto článku se podíváme na jedinečnou symbiózu mezi dvěma výkonnými knihovnami pro testování — Cucumber a Playwright — a jak je můžeme integrovat v JavaScriptu. Tato kombinace přináší nejen efektivitu, ale i eleganci do procesu automatizovaného testování.

Tento článek vás postupně provede instalací knihoven, konfigurací projektu, tvorbou prvního scénáře a v neposlední řadě si ukážeme implementaci kroku v Cucumber-js za pomocí Playwright API.

Tento článek výchází z kódu na Githubu.

Instalace

Začneme instalací knihoven:

yarn add @cucumber/cucumber @playwright/test playwright ts-node tsconfig-paths cross-env typescript

Poté si nainstalujeme potřebné prohlížeče:

yarn playwright install

Konfigurace

Založíme soubor cucumber.yml, na který se budeme odkazovat při spouštění testů.

# For further information please visit https://github.com/cucumber/cucumber-js/blob/main/docs/configuration.md
default:
  language: cs
  requireModule:
      - ts-node/register
      - dotenv/config
  require:
      - supports/hooks.ts
      - supports/typeDefinitions.ts
      - supports/world.ts
      - steps/**/*.ts
  format:
      - html:reports/html/index.html
      - summary
      - progress-bar
  formatOptions:
      snippetInterface: async-await

Konfigurační soubor pro knihovnu cucumber-js

Postupně si ukážeme a vysvětlíme existenci souborů world.ts, hooks.ts a typeDefinitions.ts. Do složky steps budeme přidávat soubory implementující jednotlivé kroky scénářů.

Jen podotknu — kód budeme psát v Typescriptu. Proto musíme načíst modul ts-node/register. Dále kvůli nastavování systémových proměnných budeme potřebovat i knihovnu dotenv/config.

Seznam všech dostupných parametrů a jejich popis naleznete v oficiální dokumentaci.

World

Za snad jedinou větší nevýhodu spojení těchto dvou knihoven bychom mohli pokládat, že nemůžeme využít test runner od Playwrightu. Čímž přicházíme mimo jiné i o nativní spouštění prohlížeče. Proto tuto funkcionalitu musíme explicitně naprogramovat. Bohužel Playwright ani v budoucnu neplánuje podporu jiných test runnerů.

Pro přehlednost naprogramujeme otevření prohlížeče na dané URL adrese ve třídě CustomWorld. Tato třída rozšiřuje World třídu. Při spuštění testovacího scénáře je vytvořena instance třídy CustomWorld. Instance sdílí stav mezi jednotlivými kroky testovacího scénáře a zároveň stav je izolovaný od ostatních testovacích scénářů.

Víc o této třídě se dozvíte v oficiální dokumentaci.

import { setWorldConstructor, World, IWorldOptions } from '@cucumber/cucumber';
import * as messages from '@cucumber/messages';
import { BrowserContext, Page, ChromiumBrowser } from '@playwright/test';
import { sanitizeFilename } from 'utils/sanitizeFilename';
import { chromium } from 'playwright';
import { ScenarioStorage } from 'types/ScenarioStorage';
import { PATHS } from 'constants/paths';

export interface ICustomWorld extends World {
    createPage: () => Promise<Page>;
    scenarioStorage: ScenarioStorage;
    page: Page;
    browser?: ChromiumBrowser;
    feature?: messages.Pickle;
    context?: BrowserContext;
    resolution?: { width: number; height: number };
}

export class CustomWorld extends World implements ICustomWorld {
    scenarioStorage = {};
    context?: BrowserContext;
    browser?: ChromiumBrowser;
    feature?: messages.Pickle;
    page: Page;
    resolution?: { width: number; height: number };

    constructor(options: IWorldOptions & ICustomWorld) {
        super(options);
        this.page = options.page;
        this.resolution = { width: 1600, height: 800 };
    }

    public async createPage() {
        const context = await this.createContext();
        const page = await context.newPage();

        await page.goto(this.getBaseUrl());

        return page;
    }

    private async getBrowser() {
        if (!this.browser) {
            this.browser = await chromium.launch();
        }

        return this.browser;
    }

    private async createContext() {
        const browser = await this.getBrowser();
        const context = await browser.newContext({
            acceptDownloads: true,
            recordVideo:
                process.env.RECORD_VIDEO === 'true' && this.feature
                    ? {
                          dir: `${PATHS.VIDEOS}/${sanitizeFilename(
                              this.feature.name
                          )}`,
                      }
                    : undefined,
            viewport: this.resolution,
        });

        await context.tracing.start({
            screenshots: true,
            snapshots: true,
        });

        return context;
    }

    private getBaseUrl() {
        return process.env.BASE_URL ?? '';
    }
}

setWorldConstructor(CustomWorld); 

Definice třídy CustomWorld

Jak si můžete všimnout — v metodě createPage je řešena inicializace prohlížeče a následně vytvoření kontextu. V kontextu nastavujeme mimo jiné nahrávání videí a také rozlišení prohlížeče. To je nastavováno dynamicky. Výchozí hodnotu viewportu definujeme v konstruktoru třídy CustomWorld.

Hooky

Cucumber-js knihovna nabízí řadu hooků, které se volají v jednotlivých životních cyklech testů.

Bavíme se o následujících cyklech:

  • cyklus před spuštěním všech scénářů,
  • cyklus před spuštěním scénáře,
  • cyklus před spuštěním kroku,
  • cyklus po dokončení kroku,
  • cyklus po dokončení scénáře,
  • cyklus po dokončení všech scénářů.

Přikládám odkazy na seznam všech hooků a také na dokumentaci jejich API.

Tagy

Občas se děje, že máte napsaný scénář ale funkcionalita v aplikaci ještě není hotová. V tomto případě můžete implementovat vlastní tag, který scénář přeskočí. Tagy se implementují skrz hooky Before/After. Definujete název hooku a také funkci, která se má vykonat.

Before({ tags: '@wip' }, async function () {
    return 'pending';
});
Definice tagu @wip

Nebo můžete chtít implementovat hook, který spustí testy s rozlišením pro mobilní telefony.

Before({ tags: '@mobile'}, async function (this: ICustomWorld) {
    this.resolution = { width: 375, height: 812 };
});

Definice tagu @mobile

Pozn.: Objekt resolution je implementovaný v CustomWorld třídě a tudíž není součástí knihovny Cucumber-js.

Tag aplikujete tím, že jej napíšete před začátek scénáře.

Použití tagu @mobile

Použití tagu @mobile

Psaní scénářů

Nyní, když už jste si projekt nakonfigurovali a máte povědomí o Worldinstanci, jaké hooky knihovna nabízí a jak fungují, můžeme se pustit do psaní scénářů.

V následujícím scénáři chceme otestovat, že uživatel si na stráknách imdb.com vyhledá film Pulp fiction, vidí jej ve výsledcích vyhledávání. Při otevření detailu filmu pak zkontrolujeme, že vidí jeho název a rok vydání.

Testovací scénář v Gherkin syntaxi

Scénáře se píší v .feature souborech. Každý takový soubor je psán v Gherkin syntaxi. Jedná se o syntaxi, která si klade za cíl být co nejvíce čitelná i lidem mimo IT obor. Skládá se z pár klíčových slov. Jedná se například o slova Požadavek, Kontext, Pokud, A, Scénář, Pak, apod.

Každý .feature soubor pak začíná klíčovými slovy Požadavek: <název-požadavku>. Jedná se o název skupiny testovacích scénářů.

Co následuje dál od toho, zdali všechny scénáře v souboru mají společné kroky či nikoliv. Klíčové slovo Kontext značí skupinu kroků, které Cucumber knihovna spustí před každým scénářem v daném souboru.

Klíčové slovo Scénář pak značí posloupnost kroků, kterým buďto testovací scénář začíná (pokud není přítomný Kontext) anebo, kterými scénář dál navazuje na kroky z Kontextu.

Více informací a podrobnou dokumentaci ke Gherkin syntaxi naleznete zde https://cucumber.io/docs/gherkin/.

My se nyní podíváme, jak vypadá implementace kroků v knihovně Cucumber-js.

Implementace kroků

Nyní si ukážeme příklad implementace kroku za pomocí knihoven Cucumber-js a Playwright.

Pro implementaci kroku musíte v adresáři steps vytvořit .ts soubor, který bude volat funkci When, Then, And, aj. Je jedno, co z toho použijete. Když všechy kroky (i když začínají ve vašem testovacím scénáři na slovo Pokud, A, Když, Pak, Potom, aj.) budete psát za pomocí funkce When, vše bude fungovat.

Prvním parametem funkce je string. V něm by měla být obsažena věta, kterou jste použili ve scénáři. Pokud vaše věta zahrnuje parametry, pak je musíte vyznačit ve složených závorkách. V případě, že byste chtěli odchytit z věty paramer, který je typu string, pak stačí napsat:

'uživatel vyhledá frázi {string}'

Použití ve scénáři by pak vypadalo následovně:

uživatel vyhledá frázi "Pulp Fiction"

Kompletní seznam všech použitelných parametrů naleznete v oficiální dokumentaci. Samozřejmě si můžete definovat také vlastní typy. Více o definici vlastních typů naleznete v další kapitole tohoto článku.

Druhým parametrem funkcí When, Then, And, apod. je asynchronní/synchronní funkce. Tato funkce by pak měla implementovat to, co se v daném kroku žádá. Parametry těchto funkcí jsou parametry získané z předchozího parametru (tedy z textové podoby kroku).

V případě, že se opřeme o předchozí příklad a chtěli byste získat hledanou frázi a vypsat ji do konzole, můžete to udělat následovně.

When(
    'uživatel vyhledá frázi {string}',
    function (phrase: string) {
        console.log(`User searched for phrase "${phrase}"`);
    }
);

Nicméně my budeme chtít z 99% případů pracovat i s instancí CustomWorld. Toho docílíme tak, že definujeme typ symbol this. Pokud do kroku implementujeme i logiku výsledek pak vypadá takto:

When(
     'uživatel vyhledá frázi {string}',
      async function (this: ICustomWorld, phrase: string) {
           await this.page.locator('input#suggestion-search').fill(phrase);
           await this.page.locator('button#suggestion-search-button').click();

           await expect(
                this.page.locator('h1').filter({ hasText: `Search "${phrase}"` })
            ).toBeVisible();
       }
);

V kroku bylo pomocí API playwrightu implementované hledání na webových stránkách imdb.com.

Definice vlastních typů

Dostáváme se k definici vlastních typů. Zmínil jsem se o tom už v předchozí kapitole. Vlastní typy budeme psát do souboru ./supports/typeDefinitions.ts.

Ukážeme si to na příkladu s typem year. Jak je z ukázky níže patrné, důležité jsou parametry name, regexp a transformer.

defineParameterType({
    name: 'year',
    useForSnippets: true,
    regexp: /"(19|20)\d{2}"/,
    transformer: (year) => parseInt(year),
});
Definice vlastního typu “year”

Nově můžeme tedy použít vlastní typ year, díky kterému můžeme v testovacím scénáři počítat na vstupu výhradně s rokem. Použití nového typu v kroku by vypadalo následovně:

'uživatel vidí rok vydání {year}'

Pro více informací o definici vlastních typů navštivte oficiální dokumentaci.

Nastavení systémových proměnných

V implementaci world.ts počítáme s definovanou systémovou proměnnou BASE_URL.

Proto založíme .env soubor v kořenovém adresáři:

BASE_URL=https://www.imdb.com
RECORD_VIDEO=true
Definované systémové proměnné

Můžete si všimnout, že kromě BASE_URL, máme také definovanou proměnnou RECORD_VIDEO. Ta jen v podstatě určuje, zdali chceme při každém spuštění testů nahrávat videa či nikoliv.

Spuštění testů

V souboru package.json si definujeme dva vlastní skripty.

"scripts": {
    "test": "cross-env TS_NODE_PROJECT=tsconfig.json cucumber-js --config cucumber.yaml",
    "test:debug": "cross-env PWDEBUG=true yarn test"
}
Vytvoření npm skriptů pro spuštění testů

První z nich spouští testy v tzv. headless režimu (po spuštění neuvidíte průběh testu v prohlížeči; scénář se spouští na pozadí). Druhý skript vám spustí test i s otevřeným prohlížečem. V prohlížeči pak vidíte mimo jiné i průběh scénáře. Více o debug módu se dozvíte na stránkách playwright.com.

Následujícím příkazem spustíme testy, tak abychom viděli i jejich průběh.

yarn test:debug <cesta-k-feature-souboru>

Závěrem

Nyní, když máme správně nakonfigurovaný projekt a můžeme psát testovací scénáře a také jejich implementaci, je na čase se zamyslet, jak je psát správně. Mám na mysli, že na implementaci kroků záleží. Ze zkušeností dokážu říct, že při špatně napsaných implementacích kroků, se můžou testovací scénáře stát velmi nespolehlivými.

Pokud chcete psát testy stabilně, přečtěte si můj další článek. Promítl jsem do něj své nabyté zkušenosti z psaní end-to-end testů. Jestli se budete držet toho, co v něm píši, budou vám testy padat pouze na chybách v aplikaci.

Doufám, že vám tento článek pomohl a věřím, že na Gherkin syntaxi si velmi rychle zvyknete.