W ostatnim artykule mieliście okazję zapoznania się z podejściem testowania wydajności aplikacji webowych wykonywanych z poziomu przeglądarki. Nadszedł czas, aby pokazać Wam, w jaki sposób zaimplementować takowe testy w praktyce.
Niestety na ten moment pula narzędzi open-source (bo na takich chciałbym się skupić), oferujących opcję testowania wydajności z poziomów przeglądarki i jednocześnie zasługujących na uwagę jest dosyć wąska.
Z ciekawszych narzędzi można by wymienić k6 Browser Module. Na ten moment jednak, nie rozważałbym na poważnie użycia tego modułu ze względu na fakt, iż jest on jeszcze w fazie eksperymentalnej. Z ciekawostek, API serwowane przez k6 browser module jest w znaczącym stopniu oparte o framework Playwright’a, co daje niską barierę wejścia dla osób mających wcześniej styczność z klasycznymi testami funkcjonalnymi UI.
Zostawmy jednak na ten moment k6 i skupmy się na drugim narzędziu, czyli Flood Element, którego twórcy są prekursorami testowania wydajności z poziomu przeglądarki i jako jedni z pierwszych na poważnie zajęli się stosowaniem tego podejścia.
Flood Element
Czym zatem jest Flood Element? To oparte na przeglądarce narzędzie do generowania obciążenia, zbudowane na bazie Puppeteera. Zapewnia łatwy w użyciu zestaw poleceń do automatyzacji większości aplikacji webowych, takich jak klikanie, przeciąganie, akcji naciśnięcia klawiszy klawiatury oraz pracy z danymi wejściowymi, przyciskami i menu.
Flood Element został zaprojektowany wyłącznie do generowania obciążenia poprzez symulację rzeczywistego zachowania użytkownika w przeglądarce. Nie próbuje manipulować stroną ani nie współpracuje z żadnym konkretnym frameworkiem front-end, chociaż świetnie sprawdza się do testowania aplikacji webowych zbudowanych w React, Angular, Ember lub jakimkolwiek innym środowisku klienckim JS.
Testy wydajnościowe we Flood Element mogą być wykonywane na małą skalę, z niskim obciążeniem np. na środowisku lokalnym. Jeśli chcielibyśmy jednak wygenerować bardzo duże obciążenie możemy skorzystać z płatnego rozwiązania chmurowego, jakim jest Flood.io.
Skalowanie testów Flood Element w Flood.io jest stosunkowo łatwe, i pozwala na uruchomienie setek, a nawet tysięcy instancji przeglądarki Google Chrome za pomocą szeregu dostępnych load generatorów znajdujących się w chmurze.
Flood posiada czytelny interfejs graficzny za pomocą którego możemy zuploadowac wykonany przez nasz wcześniej skrypt Flood Element, a następnie uruchomić test ze zdefiniowanym przez nas obciążeniem (ilością VUS) i czasem trwania testu. Wygenerowane wyniki testów, możemy łatwo porównywać między sobą oraz interpretować w poszukiwania wąskich gardeł wydajności testowanej przez nas aplikacji.
W tym artykule pominiemy wykonywanie testów z poziomu flood.io, skupiając się jedynie na wykonywaniu testów lokalnie w celu testowanie regresji wydajności aplikacji webowej.
Flood Element – Instalacja i pierwsze kroki
Najpierw upewnij się, że masz zainstalowaną najnowszą wersję Node.js dla swojej platformy.
Zacznijmy od instalacji wersji CLI Flood Element za pomocą npm’a czyli domyślnego managera pakietów dla środowiska Node.js:
npm -g install element-cli
Po wykonaniu powyższego kroku możemy wygenerować, bazowy szablon projektu z przykładowym testem:
element init my-element-project
Kolejne krok to wskazanie URL aplikacji webowej, która chcemy objąć testami. Może być nią np. URL wyszukiwarki Google: https://google.pl
Następnie podajemy nazwę skryptu testowego, np. domyślny: my-element-project.perf.ts
Na sam koniec dostajemy informacje o tym, jakie pliki zostały wygenerowane w folderze dopiero co utworzonego projektu:
package.json – jest plikiem, który opisuje każdy projekt oparty o Node.js. Jedną z jego najważniejszych ról jest przechowywanie informacji o zależnościach projektu.
tsconfig.json – określa główne pliki projektu oraz opcje kompilatora wymagane w projekcie, wskazuje na główny katalog projektu TypeScript
my-element-project.perf.ts – to wygenerowany na bazie wprowadzonych przez nas wcześniej danych skrypt testowy
.gitignore – wskazuje na pliki, które mają być pomijane przez Git’a podczas commitowania zmian
element.config.js – plik z zapisem konfiguracji testu
Przyjrzyjmy się teraz bliżej wygenerowanemu skryptowi testowemu, ponieważ zawiera on kilka istotnych elementów:
settings
export const settings: TestSettings = {
// userAgent: 'flood-chrome-test',
// loopCount: 1,
// Automatically wait for elements before trying to interact with them
waitUntil: 'visible',
}
Settings to obiekt, w którym możemy zdefiniować konfigurację na poziomie konkretnego skrypty testowego – domyślnie konfiguracja testu wczytywana jest z pliku element.config.js o ile nie wskazaliśmy oddzielnych parametrów na poziomie CLI. Konfiguracja określona w obiekcie settings ma większe znaczenie, niż ta zdefiniowana w element.config.js. Z drugiej strony jeśli dodamy określony parametr na poziomie CLI to nadpisze on zarówno konfiguracje na poziomie pliku testowego, jak również pliku konfiguracyjnego.
default
export default () => {
beforeAll(async browser => {
// Run this hook before running the first step
await browser.wait('500ms')
})
afterAll(async browser => {
// Run this hook after running the last step
await browser.wait('500ms')
})
// If you want to do some actions before/after every single step, use beforeEach/afterEach
// beforeEach(async browser => {})
// afterEach(async browser => {})
step('Start', async browser => {
// visit instructs the browser to launch, open a page, and navigate to https://google.pl
await browser.visit('https://google.pl')
await browser.takeScreenshot()
})
// browser keyword can be shorthanded as anything that is descriptive to you.
step('Step 2 find first heading', async browser => {
// Note the use of await here, this is the async/await pattern https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await
// and is required everytime you talk to the browser
const firstHeading = await browser.findElement(By.css('h1,h2,h3,p'))
const h1Text = await firstHeading.text()
// You can use console.log to write out to the command line
console.log(h1Text)
})
}
W funkcji default znajduje się już defacto nasz scenariusz testowy, może być on poprzedzony działaniami zdefiniowanymi w funkcjach beforeAll lub beforeEach Możemy również wykonywać pewne działania po zakończeniu testów – np. „czyszczenie” testu w funkcjach afterAll lub afterEach.
Czas na funkcje step, która definiuje mierzony podczas uruchomienia scenariusz testowy. W tym konkretnym wygenerowanym skrypcie testowym znajdują się dwa stepy:
Pierwszy: zawiera krok wejścia na stronę wyszukiwarki Google, oraz wykonanie snapshot’a przeglądarki:
step('Start', async browser => {
// visit instructs the browser to launch, open a page, and navigate to https://google.pl
await browser.visit('https://google.pl')
await browser.takeScreenshot()
})
Drugi: zawiera krok pobierania zawartości nagłówka na stronie, oraz loguje jego nazwę w terminalu
step('Step 2 find first heading', async browser => {
// Note the use of await here, this is the async/await pattern https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await
// and is required everytime you talk to the browser
const firstHeading = await browser.findElement(By.css('h1,h2,h3,p'))
const h1Text = await firstHeading.text()
// You can use console.log to write out to the command line
console.log(h1Text)
})
Dla obu powyższych stepów zostaną podane osobne wyniki czasów.
Czas na uruchomienie skryptu testowego – wykonujemy następującą komendę:
element run .\my-element-project\my-element-project.perf.ts --loop-count 1 –export
–-loop-count 1
oznacza, iż zostanie wykonana tylko jedna iteracja testu--export
oznacza, iż pod koniec testu zostanie wygenerowane raporty w postaci plików HTML oraz JSON. Raporty zostaną umieszczone w naszym przypadku w folderze:my-element-project\reports\my-element-project.perf\<timestamp>
Dodatkowo w folderze tmp
w naszym przypadku w: my-element-project\tmp\element-results\my-element-project.perf\<timestamp>\screenshots\*.jpg
znajdziecie screenshoty wygenerowane w określonych stepach.
Testowanie regresji wydajności
Naszym kolejnym zadaniem będzie stworzenie testu wydajnościowego regresji, weryfikującego czy performance, a w tym konkretnym wypadku szybkość ładowania elementów strony nie ulega pogorszenia. Ten typ testu wydajnościowego powinien być implementowany w procesach CI/CD, gdzie wykrycie pogorszenia wydajności powinno blokować wejście zmian na docelowy branch.
Przedmiot testu stanowić będzie aplikacji webowa o nazwie Signal Ocean Platform, która składa się z szeregu paneli/dashboardów zawierających dane/raporty wspierające zarządzenie w gałęzi transportu morskiego. Stwórzmy na niej darmowe konto i zalogujmy się.
Skrypt testowy
Naszym celem będzie stworzenie testu regresji wydajności dashboard’u o nazwie Fixtures – skupimy się na pomiarze czasu ładowania właśnie tego panelu:
import { step, beforeAll } from '@flood/element'
import { LoginPage } from '../pages/login/login'
import { NavBarPage } from '../pages/common/navBar'
import { FixturesPage } from '../pages/fixtures/fixtures'
import { Common } from '../pages/common/common'
const environmentUrl = "https://app.signalocean.com";
const email = "yourlogin";
const password = "yourPassword";
export default () => {
beforeAll(async browser => {
const loginPage = new LoginPage(browser);
const navBar = new NavBarPage(browser)
await loginPage.goto(environmentUrl)
await loginPage.login(email, password)
await navBar.waitForNavbar()
await navBar.changeModeIfNeeded("Tanker")
})
step('Fixtures Dashboard Performance Test', async browser => {
const fixtures = new FixturesPage(browser)
const common = new Common(browser)
await fixtures.visitDashboard(environmentUrl)
await common.waitForPacer()
await browser.click(fixtures.gridRow)
await browser.takeScreenshot()
})
}
Omówmy sobie powyższy skrypt. Właściwy test – czyli tam, gdzie wykonywany jest pomiar – (funkcja step
) poprzedza z kolei funkcja beforeAll
, gdzie przygotowujemy aplikację do testu: wchodzimy do aplikacji, logujemy się oraz ustawiamy tankowce (Tankers) jako pożądaną klasę statków. Dopiero potem, we właściwym teście – funkcja step
– wchodzimy na panel Fixtures, czekamy na załadowanie się strony: waitForPacer()
, następnie klikamy w pierwszy wiersz znajdujący się tabeli i na sam koniec wykonujemy snapshot widoku aplikacji w przeglądarce. Ten ostatni krok jest niezwykle ważny, ponieważ pozwala nam na wizualne potwierdzenie finalnego stanu aplikacji tzn. stwierdzić, czy rzeczywiście wszystkie elementy strony zostały załadowane. Jeśli nie, musimy udoskonalić testy, aby czekać na pojawienie się wszystkich elementów strony.
Patrząc na powyższy skrypt możecie zauważyć istnienie klas typu Page Object. Tak, nie mylicie się, w projekcie został zaimplementowany wzorzec POM. Należy pamiętać, aby w testach wydajnościowych na poziomie przeglądarki stosować takie same dobre praktyki i wzorce projektowe w pisaniu testów automatycznych jak w klasycznych testach funkcjonalnych UI.
Plik konfiguracyjny
W celu uruchomienia testów z odpowiednimi ustawieniami zdefiniujemy osobny plik konfiguracyjny o nazwie: performanceTests.config.js
:
module.exports = {
options: {
devtools: false,
failStatusCode: 1,
fastForward: false,
headless: true,
loopCount: 5,
sandbox: true,
slowMo: false,
verbose: false,
watch: false,
},
paths: {
workRoot: ".",
testDataRoot: ".",
testPathMatch: ["tests/**/*.perf*.ts"],
},
testSettings: {
actionDelay: "0s",
blockDomains: [],
browser: "chromium",
browserLaunchOptions: {},
clearCache: true,
clearCookies: true,
consoleFilter: [],
device: null,
disableCache: false,
extraHTTPHeaders: {},
ignoreHTTPSErrors: false,
incognito: false,
launchArgs: [],
responseTimeMeasurement: "step",
stages: [],
stepDelay: "1s",
userAgent: "",
viewport: { width: 1440, height: 900 },
waitTimeout: "10s",
waitUntil: "visible",
},
}
Pełny opis parametrów konfiguracyjnych możecie znaleźć w oficjalnej dokumentacji Flood Element.
Czas na uruchomienie testów. W tym celu użyjemy następujące komendy:
element run --export --config-file performanceTests.config.js
Parametr
config-file
wskazuje na plik konfiguracyjny, który zostanie wczytany.
Wszystkie testy zostaną wykonane w 5 iteracjach:
loopCount: 5
W trybie headless:
headless: true
Dodatkowo atrybut testPathMatch
wskazuje, iż zostaną uruchomione wszystkie testy spełniające poniższe warunki:
testPathMatch: ["tests/**/*.perf*.ts"]
Użycie parametru –export
w komendzie uruchamiającej testy zagwarantuje nam wygenerowanie dwóch raportów formatach json oraz HTML. Tak wygląda przykładowy raport HTML:
Jak widzimy, test rzeczywiście został wykonany w 5 iteracjach. W raporcie podane zostały czasy trwania całych iteracji, jak również to co nas interesuje najbardziej, czyli czasy ładowania dashboard’u Fixtures (czas wykonania kroków zdefiniowanych w funkcji step
)
Threshold Assertion
Mimo tego, iż Flood Element dostarcza w zasadzie kompletny framework do uruchamiania testów wydajnościowych z poziomu przeglądarki, to brakuje mu bardzo istotnej funkcjonalności, a mianowicie opcji weryfikacji akceptowalnych czasów trwania zdefiniowanych kroków scenariusza za pomocą asercji – tkz. thresholds assertions. Załóżmy, iż akceptowalny czas ładowania dashboard’u Fixtures to maksymalnie 5 sekund. Przy każdym uruchomieniu testów chcielibyśmy mieć pewność, iż czas ładowania panelu nie uległ pogorszenia – nie jest większy niż 5 sekund. Niestety Flood Element nie posiada w swoim API wbudowanych asercji sprawdzających tego typu przypadki.
Aby zapewnić taką weryfikację, skorzystamy z customowego rozwiązania, w którym pobierzemy wyniki czasu trwania każdej iteracji z raportu data.json
, a następnie w osobnej asercji zweryfikujemy czy spełnia on nasze zalozenia.
import { expect } from "chai"
import * as testHelper from "./helpers/testHelper"
const fixturesThreshold = 1000
describe("Performance Tests Thresholds Check", async () => {
it("Vessels Dashboard Thresholds Check", async () => {
const median = testHelper.thresholdTest("fixtures", "Fixtures", fixturesThreshold)
expect(median).to.be.at.most(fixturesThreshold)
}).timeout(60000)
})
W osobnym specu testowym korzystającym z biblioteki Mocha skorzystamy z funkcji thresholdstest, która przyjmuje 3 parametry:
testname
– to pierwszy człon nazwy pliku, w którym znajduje się test – fixtures.perf.tstestTitle
– nazwa testu widoczna w customowym raporcie w konsoliassertionValue
– tutaj definiujemy maksymalny akceptowalny przez nas czas ładowania dashboard’a Fixtures, w naszym przypadku będzie to 5000 ms (5 sekund)
Warto zwrócić uwagę, na to, iż do wygenerowania wyniku czasu ładowania dashboard’a korzystamy z miary statystycznej o nazwie mediana. Dla przypomnienia – mediana to środkowa liczba w zbiorze; wyznaczamy ją, porządkując zbiór liczb od liczby najmniejszej do największej i wybierając tę, która znajdzie się w środku. Dzięki zastosowaniu mediany nie wybieramy wartości skrajnych wyników czasu.
Oczywiście możemy zmodyfikować test w taki sposób, aby w asercji porównywane były inne miary statystyczne wyników np.: średnia, wartość minimalna i maksymalna, percentyl 90%, percentyl 95%. Wszystkie metody statystyczne przyjmujące zbiór wyników zdefiniowane są w klasie statistics.ts.
Czas na uruchomienie testu i zweryfikowania czy czas ładowania dashboarda Fixtures spełnia nasze założenie – nie powinien wynieść więcej niż 5 sekund.
Test uruchamiamy następującą komendą:
.\node_modules\.bin\mocha thresholdsCheck.spec.ts
Spójrzmy teraz na wygenerowany w konsoli raport:
Jak widzicie, zawiera on informacje o zdefiniowanej przez nas asercji: Median assertion: 5000 ms
oraz poszczególne miary statystyczne wyników czasów. Jak widzimy mediana w naszym teście wyniosła 3423ms – czyli zdecydowanie poniżej 5 sekund. Test zakończył się sukcesem 🙂
Rozpatrzmy teraz przypadek negatywny obniżając tymczasowo wartość FixturesThresholds
z 5000 ms do 1000 ms i uruchommy test ponownie.
Tym razem test zakończył się niepowodzeniem, ponieważ czas ładowania strony jest wyższy niż zakładane threshold value.
To wszystko! 🙂 Wielkie dzięki za przeczytanie tego artykułu do końca! Koniecznie dajcie znać co o nim myślicie w komentarzach.
Gotowy projekt z rozwiązaniem możecie znaleźć w repozytorium na GitHub Automatyzacja.It.
Aby nie przegapić kolejnego artykułu już teraz zapisz się do Newslettera.