5 tipů pro stabilní end-to-end testy

Martin Topolánek
10/4/2024

Každý, kdo používá ve svém projektu CI/CD pipeline ví, jak nepříjemné je, když end-to-end testy selžou kvůli chybě, která nebyla způsobena aktuální změnou v kódu. Pokud se vám to zatím nestalo, máte štěstí. Tento článek vám poskytne 5 tipů pro psaní stabilnějších testů. Pokud budete tyto rady dodržovat, vaše testy budou odhalovat pouze chyby způsobené aktuální změnou.

Tento článek předpokládá, že pro psaní end-to-end testů používate knihovny Cucumber-js a Playwright. O instalaci, konfiguraci a prvním seznámení s knihovnami píši ve svém dalším článku.

První tip: Naučte se pracovat se selektory

Ti z vás, kteří mají zkušenosti s CSS nebo s Javascriptem budou vědět, jak se selektory pracovat. Pokud vám ale slovo selektor nic neříká, doporučuji vám si je nastudovat (například na W3.org).

Často začínající vývojáři tápou mezi třemi používanějšími druhy zápisu selektorů:

  1. více selektorů oddělených mezerou (tzv. selektor následníka),
  2. více selektorů oddělených čárkou (tzv. seskupování selektorů),
  3. více selektorů psaných dohromady bez mezery (tzv. selektor s atributy).

Vezměme v potaz, že máme následující HTML strukturu:

<div class="modal">
  <div class="modal-header">
    <h5 class="title">Welcome!</h5>
  </div>
  <div class="modal-content">
    <h5 class="title">We are glad you want to join!</h5>
    <p>Are you ready?</p>
  </div>
  <div class="modal-footer">
    <button class="button">Sure!</button>
    <button class="button is-disabled">No :(</button>
  </div>
</div>

Dejme tomu, že bychom chtěli získat titulek z hlavičky, hlavičku a patičku modalu a neaktivní tlačítko. V ukázce níže vidíte, jak by vypadaly selektory pro získání elementů:

// 1. Selektor následníka
document.querySelector(".modal-header .title");

// 2. Seskupování selektorů
document.querySelector('.modal-header, .modal-footer');

// 3. Selektor s atributy
document.querySelector(".button.is-disabled");

Druhý tip: Naučte se používat Locatory

Velmi často pozoruji, že někteří vývojáři používají zastaralé metody pro získání elementu. Tyto metody nepočítají se změnami ve struktuře DOM a proto jsou pro SPA aplikace nepoužitelné. Samotní vývojáři knihovny Playwright tyto metody nedoporučují (viz v dokumentaci třídy ElementHandle — předchůdce třídy Locator) a radí, ať použijeme spíše metody, vracející instanci třídy Locator (více informací o třídě Locator naleznete v oficiální dokumentaci).

// ❌ Metoda, která vrací objekt typu ElementHandle
await page.$(selector);

// ❌ Metoda, která vrací pole objektů typu ElementHandle
await page.$$(selector);

// ❌ Metoda, která vrací objekt typu ElementHandle
await page.waitForSelector(selector);

// ✅ Metoda, která vrací objekt typu Locator
page.locator(selector);

Locator mimojiné nabízí zajímavé metody. Na příkladech z předchozí kapitoly si ukážeme, jak bychom takové selektory přepsali do instancí této třídy.

// 1. Selektor následníka
page.locator(".modal-header .title");
// Nebo alternativně lze zapsat i takto
page.locator(".modal-header").locator('.title');

// 2. Seskupování selektorů
page.locator('.modal-header, .modal-footer');
// Nebo alternativně lze zapsat i takto
page.locator(".modal-header").or(page.locator('.modal-footer'));

// 3. Selektor s atributy
page.locator(".button.is-disabled");
// Nebo alternativně lze zapsat i takto
page.locator(".modal-header").and(page.locator('.is-disabled'));

Dokumentaci ke všem metodám z třídy Locator najdete v oficiální dokumentaci.

Třetí tip: Propisujte stavy komponent do data atributů

Data atributy jsou speciální sadou HTML atributů, které začínají prefixem data-*. Tento prefix umožňuje definovat vlastní atributy. Atributy pak mohou nabývat libovolných hodnot typu string (řetězec znaků).

Data atributy nám umožňují sdílet rozhraní dostupných parametrů komponenty a také jejich aktuální stav na úrovni HTML elementu. Atributy nám poté pomůžou jednak při vývoji — lze na atributy vázat styly komponenty, popř. i jednodušší logiku — a jednak se díky atributům usnaďnuje testování. Můžeme počkat na požadovaný stav komponenty pomocí komplexnějších selektorů.

Uvedeme si příklad. Máme seznam položek, který je dynamický načítaný z API. Na seznam přidáme data atribut data-is-loaded značící, zdali jsou data již načtená.

type ListProps = {
  items: Array<string>;
  isLoading: boolean;
};

const List: FC<ListProps> = ({items, isLoading}) => (
  <ul data-is-loaded={!isLoading}>
    {items.map((textContent) => (
      <li key={textContent}>
        {textContent}
      </li>
    )}
  </ul>
);

Místo toho, abychom čekali statický čas, než se načtou data z API, počkáme si na hodnotu true v data atributu data-is-loaded. To je důležité, protože statické čekání může vést k nestabilním testům. Pokud budete čekat příliš dlouho, budou testy pomalé a neefektivní. A naopak pokud budete čekat příliš málo, může se stát, že komponenta ještě nebude ve správném stavu a test selže.

Selektor, který by počkal na načtená data, by mohl být pomocí knihovny Playwright zapsaný následovně:

// Jeden z možných zápisů
page.locator('ul[data-is-loaded="true"]');

// Alternativní zápis
page.locator('ul').and(page.locator('[data-is-loaded="true"]'));

Čtvrtý tip: Používejte asserty

Každý krok testovacího scénáře, který nastavuje aplikaci do nového stavu, by měl obsahovat aserci na požadovaný stav. Za stavy aplikace můžeme například považovat:

  • vytvoření nové entity,
  • editace stávající entity,
  • přepínání se mezi stránkami,
  • vyhledávání v seznamech,
  • aj.

Nový stav můžeme detekovat například následujícími způsoby:

  • kontrola nové entity v seznamu entit,
  • kontrola změněného atributu entity,
  • uživatel byl přesměrován na požadovanou stránku,
  • uživatel vidí ve výsledcích vyhledávání požadovaný výsledek,
  • aj.

Aserce nám pomůže zajistit stabilitu testů a přesnou identifikaci chyb. Pokud test selže, budeme přesně vědět, ve kterém kroku nebyla aplikace nastavená do správného stavu.

Knihovna Playwright obsahuje funkci expect, která umožňuje provádět aserce. Další informace o funkci expect najdete v oficiální dokumentaci. Seznam všech možných asercí nad objektem Locator najdete také v oficiální dokumentaci.

Pátý tip: Pracujte s testId data atributem

V předchozích kapitolách jsem doporučil používat objekty Locator. Metoda locator z třídy Page vrací objekt tohoto typu. Nevýhodou této metody je, že musíme vždy psát celý selektor. Metoda getByTestId nám umožní předat identifikátor elementu bez nutnosti psaní celého selektoru.

Ukážeme si, jak lze získat element pomocí metody locator a metody getByTestId:

// Element získaný pomoctí metody locator
page.locator('[data-testid="list"]');

// Element získaný pomocí metody getByTestId
page.getByTestId('list');

Metoda getByTestId vrací elementy podle nastavené hodnoty atributudata-testid. To usnaďnuje práci se selektory a pomáhá být vývojářům obezřetnější při vývoji aplikace. Bude se stávat méně častěji, že selektor pro získání elementů bude jakkoliv narušen. Nestane se totiž, že by vývojář nevěděl, že se s komponentou pracuje v rámci testovacích scénářů.

Pokud se vydáte cestou data-testid atributů, pak je vhodné používat systém, který zaručí unikátnost identifikátorů. Nabízí se tedy vytvoření objektu pro účely psaní testovacích scénářů:

export const TEST_ID = {
    SUBMIT_BUTTON: 'submit-button'
} as const;

Objekt pak použijete v data atributu:

const SubmitButton: FC = (props) => (
  <Button data-testid={TEST_ID.SUBMIT_BUTTON} {...props}>
    ...
  </Button>
);

A následně můžeme získat element pomocí metody getByTestId:

page.getByTestId(TEST_ID.SUBMIT_BUTTON);

Samozřejmě s metodou getByTestId můžeme pracovat, jako bychom pracovali s metodou locator. Je tedy možný i následující zápis:

page
  .getByTestId(TEST_ID.SUBMIT_BUTTON)
  .and(
    page.locator('[data-is-disabled="true"]')
  );

Závěrem

Mám bohaté zkušenosti s psaním end-to-end testů. Prošli jsme si obdobím, kdy testy padaly na náhodných faktorech. Studoval jsem pečlivě každý spadnutý test a snažil jsem si z něj vzít ponaučení. Nyní už vím, jak je psát, aby šance na jejich náhodný pád byla minimální. Tyto zkušenosti jsem sepsal do tohoto článku. Věřím, že vám pomůžou s psaním spolehlivějších testů.