przemuh.dev

3 kroki do niesamowitych raportów w cypress.io

12/18/2019

W tym wpisie nauczymy się jak wygenerować rozbudowane raporty z testów napisanych w cypress.io. Wzbogacimy je także o zrzuty ekranu, co powinno pomóc w znacznie szybszej naprawie potencjalnych bugów 😄 Wszystko czego potrzebujemy to trzy proste kroki.

Egnyte + testy = ❤️

Utrzymanie możliwie jak najwyższej jakości produktu to jedno z naszych priorytetów w Egnyte. Dlatego kochamy testować 😍. Nasze aplikacje do małych nie należą, dlatego ich ręczne testowanie było by dla nas bardzo wyczerpujące. W związku z tym, automatyzacja testów i technika Continous Integration są naszymi najlepszymi przyjaciółmi. Piszemy wiele rodzajów testów: unit, integration, end-to-end, modułowe itd. Ale to nie nazwy są tu najważniejsze. To co się liczy najbardziej, to pewność, że gdy widzisz zielony pipeline na Jenkinsie, to potencjalnie nie popsułeś żadnej części systemu.

Więc w czym problem? Wiedzieliście, że testy nie zawsze przechodzą? 😱 I to jest jak najbardziej ok :) (przynajmniej do czasu znalezienia przyczyny). Nie musimy od razu panikować. Po pierwsze, spokój. Tylko spokój może nas uratować. Widzimy czerwony pipeline - wchodzimy na Jenkinsa, sprawdzamy co jest nie tak i fixujemy. To tyle. Problem polega na tym, że raport z testów na Jenkinsie to bardzo często, zwykły tekst wraz ze zrzutem ze stack-trace. Taki raport jest jak najbardziej ok dla unit testów albo testów integracyjnych dla naszych komponentów Reactowych, połączeń z Reduxem, itd. Z drugiej strony, taka ściana tekstu nie zawsze jest przydatna dla testów uruchamianych w przeglądarce (full UI). Spójrzmy na fragment raportu dla przykładowego testu:

Raport z testów na Jenkinsie
Raport z testów na Jenkinsie

Tak wygląda podstawowy raport z testu na Jenkinsie dla jednego z naszych produktów (Egnyte Protect). Do pisania testów integracyjnych-UI, korzystamy z bombowego 💣 narzędzia cypress.io. Muszę przyznać, że Cypress wraz z rozszerzeniem cypress-testing-library robią wspaniałą robotę jeśli chodzi o tzw. error messages. Na pierwszy rzut oka widać, że cypress nie potrafił znaleźć elementu z zadaną wartością tekstową. Timed out retrying: Expect to find element: findByText("..."), but never found it. Ok, ale jaki był stan wizualny naszej aplikacji? Jako developer biorący udział w projekcie Egnyte Protect, wiem, że element którego szukamy powinien znajdować się w dialogu. Ale czy dialog został otwarty? A może to tylko literówka? Tyle pytań a tak mało odpowiedzi. Jeśli teraz chcielibyśmy sprawdzić o co biega, musielibyśmy uruchomić testy lokalnie i zobaczyć stan wizualny naszej aplikacji. Dopiero wtedy dowiedzielibyśmy się, że mamy doczynienia (spoiler alert) z literówką :)

A co by było, gdyby zamiast ściany tekstu, pokazać zrzut ekranu z aplikacji?

Zrzut ekranu z testowanej aplikacji
Zrzut ekranu z testowanej aplikacji

Wow! Teraz wiemy, że dialog został otwarty, i że tekst w nagłówku jest niepoprawny! Dostaliśmy nieoceniony kontekst, potrzebny do zdiagnozowania błędu, od razu w raporcie wzbogaconym o zrzut ekranu.

Zatem, jak to osiągnąć? Jak dodać zrzuty ekranu do raportu z testów? I w końcu - jak taki raport wygenerować? Jedziemy z koksem!

Raport HTML pędzi z pomocą!

Cypress bazuje na frameworku mocha.js. To dla nas świetna wiadomość - ponieważ mocha.js to bardzo dojrzały projekt, posiadający mnóstwo rozszerzeń. Rezultaty testów generowane są w mocha.js za pomocą tzw. reporterów. Taki reporter możemy napisać samemu, albo możemy skorzystać już z istniejącego np. mochawesome. Jak sama nazwa wskazuje, generuje on AWESOME raporty! Badum tsss.

Zobaczmy teraz, jak możemy zintegrować mochawesome z cypress w celu wygenerowania raportu HTML wraz ze zrzutem ekranu dla testów zakończonych błędem. Aby zwizualizować wszystkie zmiany, które wprowadziłem na potrzeby tego wpisu, skorzystałem z przykładowego repozytorium cypress-example-kitchensink. Integrację przeprowadzimy w trzech prostych krokach. Do dzieła!

Krok 1 - setup reportera

Po pierwsze, musimy zainstalować odpowiednie reportery. Tak! Dokładnie - liczba mnoga - reportery. Oprócz generowania raportów HTML, nadal chcielibyśmy wyświetlać wyniki w konsoli, a być może także w specjalnym formacie JUnit XML. Dla każdego, z tego typu wyników, musimy mieć osobny reporter. W tym celu skorzystamy z paczki cypress-multi-reporters, która umożliwia skorzystanie z wielu reporterów dla cypress.io. Oprócz tego musimy zainstalować jeszcze pakiet mocha, oraz oczywiście mochawesome.

npm install --save-dev mocha cypress-multi-reporters mochawesome

Lub jeśli korzystacie z yarn:

yarn add -D mocha cypress-multi-reporters mochawesome

Następnie w pliku konfiguracyjnym cypress.config, musimy wskazać reporter, z którego będziemy korzystać:

{
  "reporter": "cypress-multi-reporters",
    "reporterOptions": {
      "configFile": "reporter-config.json"
    }
}

Pole configFile wskazuje na plik konfiguracyjny reporter-config.json, który zawiera opcje dla poszczególnych reporterów. Plik ten powinniśmy dodać do repozytorium. Zobaczmy teraz jak wygląda jego zawartość:

{
    "reporterEnabled": "mochawesome",
    "mochawesomeReporterOptions": {
        "reportDir": "cypress/results/json",
        "overwrite": false,
        "html": false,
        "json": true
    }
}

Ustawiamy folder docelowy, do którego trafią nasze wyniki. Chcemy, aby zostały one zapisane w formacie JSON, osobno dla każdego pliku z testami. Dlatego ustawiamy flagę html na wartość false. Cypress potrafi uruchamiać testy równolegle, dlatego aby nie nadpisać wygenerowanych już wyników, ustawiamy flagę overwrite na false.

Spróbujemy teraz uruchomić nasze testy za pomocą komendy npm run local:run.

Running:  examples/location.spec.js                                                      (9 of 19)
Location
    ✓ cy.hash() - get the current URL hash (169ms)
    ✓ cy.location() - get window.location (101ms)
    ✓ cy.url() - get the current URL (78ms)
3 passing (1s)
[mochawesome] Report JSON saved to /Users/przemuh/dev/cypress-example-kitchensink/cypress/results/json/mochawesome_008.json

Jak widzicie, w konsoli dostaliśy wyniki dla poszczególnych testów (domyślny spec-reporter). Zaraz pod nimi widzimy informację na temat utworzonego pliku mochawesome_008.json. Każdy plik z testami wygeneruje nam osobny plik z wynikami.

Lista wygenerowanych plików z wynikami testów
Lista wygenerowanych plików z wynikami testów

Jesteśmy gotowi do implementacji następnego kroku.

Krok 2 - generowanie raportu

Zebraliśmy pliki z wynikami testów. Teraz, musimy połączyć je w jeden plik i na jego podstawie wygenerować raport HTML. W tym celu skorzysamy z narzędzia mochawesome-merge. Zainstalujmy je!

npm i --save-dev mochawesome-merge
yarn add -D mochawesome-merge

Teraz, dodajmy skrypt npm, który będzie odpowiedzialny za uruchamianie narzędzia do łączenia wyników.

"report:merge": "mochawesome-merge --reportDir cypress/results/json > cypress/results/mochawesome-bundle.json"

Flaga reportDir mówi o tym, w jakim folderze znajdują się pliki do połączenia. Wynik łączenia wyrzucony będzie na standardowe wyjście (w naszym przypadku będzie to konsola) dlatego, przekierowujemy wyjście do pliku mochawesome-bundle.json. Jedna uwaga: wynik łączenia powinien znajdować się w innym folderze niż pliki z poszczególnymi wynikami testów. W przeciwnym wypadku dostaniemy błąd.

Po zmergowaniu wyników jesteśmy gotowi do wygenerowania raportu HTML. Potrzebujemy do tego jeszcze jednej paczki mochawesome-report-generator.

npm i --save-dev mochawesome-report-generator
yarn add -D  mochawesome-report-generator

Tak jak w przypadku łączenia, utworzymy sobie teraz skrypt, który będzie uruchamiał narzędzie do generowania raportu HTML:

"report:generate": "marge cypress/results/mochawesome-bundle.json -o cypress/reports/html"

Słówko marge to skrót od MochawesomeReportGEnerator - to tak w ramach gdybyście się zastanawiali :)

Po poprawym wykonaniu skryptu, nasz raport HTML powinien pojawić się w katalogu cypress/results/html.

Widok raportu HTML
Widok raportu HTML

Została tylko jedna, mała rzecz. Dodanie zrzutu ekranu do wyniku testu zakończonego błędem.

Krok 3 - zrzut ekranu

Cypress automatycznie generuje zrzuty ekranu dla testów, które z jakiegoś powodu nie przeszły. Jest to domyślne zachowanie, które można wyłączyć. Wygenerowane obrazki zbierane są w folderach, które przyjmują następujacą strukturę:

path-to-the-specfile/spec.file.js/context - describe - describe - testTitle (failed).png

Rozważmy sobie test, który znajduje się w katalogu examples/actions.spec.js:

context('Actions', () => {
  context("nested context", () => {
      it('.type() - type into a DOM element', () => {})
   })
})

Wygeneruje on zrzut ekranu, który znajdzie się w następującej strukturze folderów:

Struktura folderów ze zrzutem ekranu
Struktura folderów ze zrzutem ekranu

Ok, to jak połączyć te dwa elementy: zrzut ekranu wygenerowany przez Cypress i wynik testu wygenerowany przez mochawesome reporter?

Po pierwsze, skopiujmy sobie nasze zrzuty ekranu do folderu, w którym trzymamy nasz raport HTML. W tym celu napiszemy sobie kolejny skrypt npm:

"report:copyScreenshots": "cp -r cypress/screenshots cypress/results/html/screenshots"

Następnie, w pliku cypress/support/index.js, napiszmy kawałek kodu odpowiedzialny za nasłuchiwanie na event test:after:run

Cypress.on("test:after:run", (test, runnable) => {
    if (test.state === "failed") {
        // do something
    }
});

Aby dodać zrzut ekranu do wyniku testu, musimy skorzystać z metody addContext z pakietu mochawesome. Metoda ta, przyjmuje dwa argumenty: obiekt z testem oraz tzw. context. Jeśli context jest poprawnym adresem URL (może być lokalną ścieżką) do obrazka, wtedy obrazek ten będzie wyświetlany pod wynikiem testu. Oczywiście to nie musi być tylko obrazek. Po więcej info odsyłam do dokumentacji.

import addContext from 'mochawesome/addContext'

Cypress.on("test:after:run", (test, runnable) => {
    if (test.state === "failed") {
        const imageUrl = "?";
        addContext({ test }, imageUrl);
    }
});

Wszystko spoko, ale skąd mamy wziąć ten imageUrl ? ...

Pora na odrobinę magii

Żartuję :) Skorzystamy z API jakie daje nam mocha, a dokładniej z tzw. runnable object. Jak widzieliśmy wcześniej, Cypress generuje nazwy obrazków na podstawie struktury testu. Musimy to teraz odtworzyć:

Cypress.on('test:after:run', (test, runnable) => {
  if (test.state === 'failed') {
    let item = runnable
    const nameParts = [runnable.title]

    // Iterate through all parents and grab the titles
    while (item.parent) {
      nameParts.unshift(item.parent.title)
      item = item.parent
    }

    const fullTestName = nameParts
            .filter(Boolean)
            .join(' -- ')           // this is how cypress joins the test title fragments

    const imageUrl = `screenshots/${
      Cypress.spec.name
    }/${fullTestName} (failed).png`

    addContext({ test }, imageUrl)
  }
})

Od tej chwili, jeśli nasz test zawiedzie, w naszym pliku wynikowym zostanie doklejony context z adresem do wygenerowanego obrazka:

{
  "title": ".type() - type into a DOM element",
  "fullTitle": "Actions .type() - type into a DOM element",
  "timedOut": null,
  "duration": 10395,
  "state": "failed",
  "speed": null,
  "pass": false,
  "fail": true,
  "pending": false,
  "context": "screenshots/examples/actions.spec.js/Actions -- .type() - type into a DOM element (failed).png",
}

Co więcej - obrazek ten zostanie dodany do raportu HTML.

Raport HTML wraz ze zrzutem ekranu
Raport HTML wraz ze zrzutem ekranu

TADA 🎉 Mamy to!

Wszystkie zmiany, których dokonaliśmy, możecie zobaczyć w jednym miejscu: https://github.com/przemuh/cypress-example-kitchensink/pull/1/files

Kroki opcjonalne

Dobrym pomysłem jest dodanie do .gitignore folderów cypress/results i cypress/reports.

Fajnie było by też "czyścić" foldery z generowanymi assetami (wyniki, zrzuty, itp.). Jak to zrobić? Zgodnie z "tradycją" dodamy teraz skrypt npm odpowiedzialny za tę akcję:

"precy:run": "rm -rf cypress/screenshots cypress/results cypress/reports"

Te trzy literki "pre", zanczą ni mniej ni więcej, jak to, że skrypt ten odpalony będzie przed wywołaniem skrypu cy:run. Więcej info w dokumentacji npma.

W użytym, przykładowym repo, zainstalowany został pakiet npm-run-all. Możemy z niego skorzystać aby uruchomić wszystkie, stworzone do tej pory skrypty w zadanej kolejności (sekwencyjnie):

"report": "run-s report:*",
"report:merge": "mochawesome-merge --reportDir cypress/results/json > cypress/results/mochawesome-bundle.json",
"report:generate": "marge cypress/results/mochawesome-bundle.json -o cypress/reports/html",
"report:copyScreenshots": "cp -r cypress/screenshots cypress/reports/html/screenshots"

Pozostała jeszcze jedna sprawa do rozważenia. W większości systemów operacyjnych, nazwa pliku ograniczona jest do 255 znaków. Co się stanie jeśli zagnieździmy nasz test wielopoziomo? To proste! Zostanie przycięty :) Cypress sam przycina nazwy wygenerowanych obrazków do 220 znaków. Powinniśmy to odzwierciedlić w naszym kodzie:

const MAX_SPEC_NAME_LENGTH = 220;
const fullTestName = nameParts
    .filter(Boolean)
    .join(" -- ")
    .slice(0, MAX_SPEC_NAME_LENGTH);

Niestety jest to tzw. szczegół implementacyjny. Nie wiemy czy programiści cypressa zmienią kiedyś ten numer. Dlatego, lepszą opcją będzie nie zagnieżdżanie testów na tak głębokich poziomach. Polecam lekturę posta od Kent C Doddsa - Avoiding nesting when you are testing, w którym pokazuje, do jakich problemów może prowadzić zagnieżdżanie w testach.

Podsumowanie

Mam nadzieję, że ten artykuł pomógł Wam w poprawnym skonfigurowaniu środowiska pod generowanie raportów HTML z cypressa. Tego typu raporty bardzo pomagają nam, w Egnyte, w szybkim sprawdzeniu gdzie leży źródło problemu - wierzę, że pomogą i Wam :)

Podsumujmy nasze 3 proste kroki:

  1. Instalacja i konfiguracja reporterów
  2. Zebranie wyników i wygenerowanie raportu HTML
  3. Dekoracja raportu kontekstem - zrzutem ekranu - za pomocą metody addContext.

Wszystkie niezbędne zmiany, możecie zobaczyć w przystępnej formie pull requesta: https://github.com/przemuh/cypress-example-kitchensink/pull/1/files. Oczywiście po wygenerowaniu raportu musicie jeszcze go podpiąć pod narzędzie CI. Ale to już historia na osobnego posta. :)

To jak? Jesteście gotowi na stworzenie swojego raportu HTML z cypress.io?