Powrót do: Praktyczne wprowadzenie do testów automatycznych z Playwright
DRY i hook beforeEach
Prezentacja
Dodatkowe materiały
Bazujemy na kodzie lekcji L05_rozwiazanie_aaa
Kod wynikowy tej lekcji znajduje się tu: L06_dry_hooks
Pamiętaj, aby po danej porcji pracy: uruchamiać test, commitować poprawnie działający kod 😉
Czym jest DRY
DRY (czyli Don’t Repeat Yourself) jest zasadą w programowaniu, która mówi, żeby nie powtarzać tego samego kodu w różnych częściach programu.
Zamiast powtarzać kod, można wydzielić powtarzające się części i umieścić je w jednym miejscu (pliku, funkcji, metodzie, etc.). Taka praktyka często ułatwia zrozumienie kodu, ułatwia wprowadzanie zmian oraz redukuje ryzyko popełnienia błędów.
Dzięki temu możemy zaoszczędzić czas i wysiłek, ponieważ nie musimy pisać tego samego kodu wielokrotnie 😉
Hook beforeEach
beforeEach() to hook, czyli funkcja wywoływana przed każdym testem. Występuje ona w wielu bibliotekach do testów w języku JavaScript/TypeScript.
beforeEach() służy do wykonania pewnych czynności przed testami, np. przygotowanie danych, logowanie, przygotowanie środowiska, etc. Dzięki temu możemy uniknąć powielania kodu w każdej funkcji testowej.
Wyciągnięcie url do hook beforeEach
beforeEach() w pliku login.spec.ts wyciągamy powtarzający się kod wspólny dla wszystkich testów:
test.beforeEach(async ({ page }) => {
const url = 'https://demo-bank.vercel.app/';
await page.goto(url);
});
Struktura pliku login.spec.ts będzie wyglądać tak:
test.describe('User login to Demobank', () => {
test.beforeEach(async ({ page }) => {
const url = 'https://demo-bank.vercel.app/';
await page.goto(url);
});
test('successful login with correct credentials', async ({ page }) => {
// Arrange
const userId = 'testerLO';
const userPassword = '10987654';
Pamiętamy o usunięciu kodu, który został wyciągnięty do beforeEach() ze wszystkich testów w ramach całego describe()
Uruchomienie testów dla danego pliku
Konsola bash w VSC na Windows:
Kliknij strzałkę w dół w prawym górnym menu konsoli i wybierz Git Bash.
Wprowadź polecenie npx playwright test
Zacznij wpisywać nazwy folderów lub plików, a następnie uzupełniać podpowiedzi klawiszem TAB.
Dla login.spec.ts będzie to polecenie:
npx playwright test tests/login.spec.ts
Implementacja DRY w w testach pulpitu
Możemy wyciągnąć kod przejścia do strony i zalogowania się:
const url = 'https://demo-bank.vercel.app/'; const userId = 'testerLO'; const userPassword = '10987654';
await page.goto(url);
await page.getByTestId('login-input').fill(userId);
await page.getByTestId('password-input').fill(userPassword);
await page.getByTestId('login-button').click();
z testów do beforeEach():
test.beforeEach(async ({ page }) => {
const userId = 'testerLO';
const userPassword = '10987654';
const url = 'https://demo-bank.vercel.app/';
await page.goto(url);
await page.getByTestId('login-input').fill(userId);
await page.getByTestId('password-input').fill(userPassword);
await page.getByTestId('login-button').click();
});
Przykład: zmienna na poziomie describe jest dostępna we wszystkich testach a na poziomie beforeEach() tylko dla tego bloku. Jeśli zmienna nie jest używana w testach a tylko w beforeEach to powinna tylko tam się znaleźć.
Zawartość pliku pulpit.spec.ts:
import { test, expect } from '@playwright/test';
test.describe('Pulpit tests', () => {
test.beforeEach(async ({ page }) => {
const userId = 'testerLO';
const userPassword = '10987654';
const url = 'https://demo-bank.vercel.app/';
await page.goto(url);
await page.getByTestId('login-input').fill(userId);
await page.getByTestId('password-input').fill(userPassword);
await page.getByTestId('login-button').click();
});
test('quick payment with correct data', async ({ page }) => {
// Arrange
const receiverId = '2';
const transferAmount = '150';
const transferTitle = 'pizza';
const expectedTransferReceiver = 'Chuck Demobankowy';
// Act
await page.locator('#widget_1_transfer_receiver').selectOption(receiverId);
await page.locator('#widget_1_transfer_amount').fill(transferAmount);
await page.locator('#widget_1_transfer_title').fill(transferTitle);
await page.getByRole('button', { name: 'wykonaj' }).click();
await page.getByTestId('close-button').click();
// Assert
await expect(page.locator('#show_messages')).toHaveText(
`Przelew wykonany! ${expectedTransferReceiver} - ${transferAmount},00PLN - ${transferTitle}`
);
});
test('successful mobile top-up', async ({ page }) => {
// Arrange
const topUpReceiver = '500 xxx xxx';
const topUpAmount = '50';
const expectedMessage = `Doładowanie wykonane! ${topUpAmount},00PLN na numer ${topUpReceiver}`;
// Act
await page.locator('#widget_1_topup_receiver').selectOption(topUpReceiver);
await page.locator('#widget_1_topup_amount').fill(topUpAmount);
await page.locator('#uniform-widget_1_topup_agreement span').click();
await page.getByRole('button', { name: 'doładuj telefon' }).click();
await page.getByTestId('close-button').click();
// Assert
await expect(page.locator('#show_messages')).toHaveText(expectedMessage);
});
});
Wyciągnięcie zmiennej url do konfiguracji
Z beforeEach() w pliku login.spec.ts wyciągamy wartość zmiennej url:
test.beforeEach(async ({ page }) => {
const url = 'https://demo-bank.vercel.app/';
await page.goto(url)
});
I umieszczamy ją w pliku playwright.config.ts pod zmienną baseURL (wystarczy jej odkomentowanie):
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'https://demo-bank.vercel.app',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'retain-on-failure',
video: 'retain-on-failure',
},
Po takiej zmianie, w kodzie naszych testów logowania możemy zmienić:
const url = 'https://demo-bank.vercel.app/'; await page.goto(url);
na:
await page.goto('/');
Również w testach pulpitu aplikujemy tę zmianę.
Zmienna kod dla każdego testu, gdy jest on wydzielony do beforeEach() przebiega dużo szybciej.
Posiadanie globalnie ustawionego adresu url pozwala nam szybko zmienić go w obrębie wszystkich testów.
DRY – czy zawsze i wszędzie?
Nie zawsze warto stosować DRY. Dla przykładu w testach w pliku login.spec.ts mamy powtórzony kod userId ale nie we wszystkich testach.
Wyciąganie nadmiarowych danych powoduje ich dostępność w obrębie wszystkich testów, nawet takich, które danego rozwiązania nie potrzebują.
Dlatego warto się zastanowić jak zastosowanie DRY wpłynie na nasze bloki i czy nie idziemy w drugą stronę zbyt nadmiarowo wyciągając i współdzieląc dane.



Hej,
to moja pierwsza styczność z Playwright i wstępem do automatyzacji i jestem mega wdzięczny za ten kurs.
Bardzo mi odpowiada taka forma, gdzie pokazujecie jak się zmieniają testy na jednym projekcie.
To duże ułatwienie dla mnie jako osoby, która nie koduje
Hej,
Ciesze się że Ci się podoba😀
Bo chcieliśmy wyjśc poza ramy prostych tutoriali aby pokazać rozwój frameworka, decyzje i jak podchodzić do długo technicznego😉
Dodatkowo jakiś czas temu opublikowaliśmy ekspresowe wprowadzenie do TypeScript dla Testerów: https://jaktestowac.pl/tsx – rzuć okiem jesli chcesz poznać ten język 😁
Hej, pytanie o AAA.
Zgodnie z zasadą DRY wyniosłam do beforeEach zmienne user_id i user_pass i wykorzystałąm je w pierwszym teście, Ale w drugim teście też z nich korzystam i nie mam co wpisać pod ‘//Arrange’ . Jak do tego podejść, żeby to miało sens, zostawić puste arrange, zrezygnować z niego, czy jest może jeszcze jakies inne sensowne podejście?
test.describe('Adding to the cart tests', () => { // Arrange test.beforeEach(async ({ page }) => { const userName = 'standard_user'; const userPass = 'secret_sauce'; await page.goto('/'); await page.locator('#user-name').fill(userName); await page.locator('#password').fill(userPass); }); // Act test('listing all items from the main page', async ({ page }) => { await page.locator('#login-button').click(); // Assert await expect(page.locator('[data-test="inventory-item"]')).toHaveCount(6); }); // Arrange // Act test('adding an item to the cart', async ({ page }) => { await page.locator('#login-button').click(); await page.locator('#add-to-cart-sauce-labs-backpack').click(); await page.locator('[data-test="shopping-cart-link"]').click(); // Assert await expect(page.locator('[data-test="inventory-item"]')).toHaveCount(1) });Hej Karolina!
Zdecydowanie można pominąć sekcję jeśli jej nie potrzebujemy i nie jest to zła praktyka.
Czytający test będzie wtedy próbował odnaleźć sekcję Arrange gdzieś powyżej i w końcu trafi na odpowiednie miejsce.
Jedynie w Twoim kodzie dałbym Act do wewnątrz testu gdyż tam rozgrywa się główny scenariusz i kroki.
Pozdrawiam!
Cześć,
mam jakiś problem z dostępnością constów użytych w beforeEach:
const amount = ‘150’;
Test nie może ich znaleźć, trzeba je jakoś specjalnie przekazać tak jak page?
Cannot find name ‘amount’ gdy próbuję wywołać stała w teście 🙁
Hej!
Jak wygląda Twój cały kod
Zarówno z deklaracją tych zmiennych jak i miejscami gdzie chcesz się do nich odwołać 🙂
Chodzi o wykorzystanie amount (daem tą samą kwote w 2 testach dlatego tak)
Nie znajduje go wtedy w expecedSuccessTransfer i przy wypełnianiu pola
Można to naprawić przenosząc deklarację bespośrednio do test.describe, pytanie czy to dobra praktyka i jakie dane powinniśmy tam umieścić? (pomijając kwestię nadmiarowości danych w pozostałych testach)
test.describe('Pulpit Tests', () => { test.beforeEach(async ({ page }) =>{ const userLogin = 'tester11'; const userPass = 'password'; const amount = '150'; await page.goto('/'); await page.getByTestId('login-input').fill(userLogin); await page.getByTestId('password-input').fill(userPass); await page.getByTestId('login-button').click(); }) test('Quick payment - correct data', async ({ page }) => { //Arrange const receiver = '2'; const title = 'pizza'; const expectedReceiver = 'Chuck Demobankowy'; const expecedSuccessTransfer = `Przelew wykonany! ${expectedReceiver} - ${amount},00PLN - ${title}`; //Act await page.locator('#widget_1_transfer_receiver').selectOption(receiver); await page.locator('#widget_1_transfer_amount').fill(amount); await page.locator('#widget_1_transfer_title').fill(title);Hej Jacek!
Wystarczy daną zmienną ustawić pod describe:
test.describe('Pulpit Tests', () => { const amount = '150';i można z niej korzystać w obrębie całego describe.
Jeśli chciałbyś modyfikowalny obiekt ustawiany np w beforeEach i dostępny ze zmodyfikowaną wartością w testach to wyglądało by to tak:
test.describe('Pulpit Tests', () => { let amount = string; test.beforeEach(async ({ page }) =>{ amount = "150"; }) test('Quick payment - correct data', async ({ page }) => { await expect(page.amount).toHaveText(amount);To standardowa praktyka.
Warto tu zwrócić uwagę, że te zmienne są dostępne w całym obszarze describe.
Powinny być one wykorzystywane przez wszystkie testy gdyż nadmiarowe stałe/zmienne niepotrzebnie udostępniane (wyciekające) stanowią narzut przy czytaniu i zrozumieniu testów.
Jeśli potrzebujesz zmiennych tylko do pewnej grupy testów należy wydzielić zagnieżdżone describe wyłącznie ze zmiennymi dotyczącymi danych testów.
Oczywiście jest to rekomendacja i czasem są od niej odstępstwa.
Mam nadzieję, że to w miarę rozjaśnia sytuację.
Pozdro!
Hej,
I jeszcze jedo pytanie – czy warto w hookach robić soft asercje dla danych działań czy raczej całkowicie unikać soft asercji w hookach ??
Powiedziałbym, zę to zależy od przypadku.
Gdy w hooku umieścimy tylko soft assert, który zakończy się niepowodzeniem, to test zostanie uruchomiony – i nawet jesli test zakończy sie powodzeniem, to przez hook zostanie oznaczony jako failed 🙂
Dlatego trzebaby się zastanowić co chcemy osiągnąc i co da nam tego typu sprawdzenie 🙂 I najlepiej przygotować jakiś POC, czyli prosty kawałek kodu, na którym zobaczymy wynik 😉
Hej hej,
Dzięki za odpowiedź super czyli nie jest to całkowicie bad practice ale lepiej uważać na asercjowanie innych niż główne elementy – dzięki za odpowiedź,
Pozdrawiam i spokojności życzę,
Jakub K
Hej hej,
Mam pytanie – jest projekt na kilku środowiskach – z tego co kojarzę użycie globalnie url’a do projektu jest spoko jeśli podstawa adresu się nie zmienia a co z puszczaniem testów na różnych środowiskach, gdzie adres url na dane środo jest inny na każdym ze środowisk – czy wtedy używać base url tylko dodawać ifa w hook’u, czy duplikować pliki z kodem pod każde środowisko yżywając innego baseURL w zależności od np wybranej zmiennej w teście – przygotowane w playwright.config.json np baseURL, baseURL2 itd czy jest jakieś inne prostrze i nie wymagające duplikowania testów rozwiązanie które oferuje playwright??
Pozdrawiam i dzięki z góry za odpowiedź,
Jakub K
Dobre pytanie! 🙂
Przy testach na różnych środowiskach można użyć kilka różnych mechanizmów (projects w Playwright i biblioteka dotenv). Temat ten opisujemy na naszym blogu: https://playwright.info/playwright-testy-na-roznych-srodowiskach
Również dedykowany materiał na ten temat właśnie przygotowuje w ramach: https://jaktestowac.pl/playwright/ 🙂
Hej super,
Dzięki za odpowiedź i za to że ten temat również macie zamiar umieścić.
Pozdrawiam i spokojności życzę,
Jakub K
Hej, mam pewien problem z uruchomieniem testów z poziomu konsoli:
‘npx playwright test tests/login.spec.ts’
Przy każdej próbie otrzymuję błąd:
“Error: page.goto: Protocol error (Page.navigate): Cannot navigate to invalid URL
Call log:
– navigating to “/”, waiting until “load””
Ustawiłem zmienną baseURL w pliku playwright.config.ts podmieniając w beforeEach zmienną url w metodzie goto na:
await page.goto('/');Powyższy problem nie występuje gdy uruchamiam testy z poziomu IDE przy użyciu wtyczki.
Hmm wygląda na to, że już działa.
Nie wiem na ile to pomogło ale uruchomiłem testy poprzez skrypt:
npm run test i wszystko przeszło.
Od tamtej pory uruchamiam już testy: ‘npx playwright test tests/login.spec.ts’ i działa.
Trochę martwi ta stabilność…
Hej,
Może miałeś jakiś niezapisany plik?
Również pytanie jak dokładnie wyglądał Twój kod, czy nie było tam dodatkowego “/” w adresie?
Przy automatyzacji i konfiguracji trzeba kilka rzeczy przypilnować – jednak najważniejsze, że obecnie działa 🙂
Daj prosze znać jakbyś miał jeszcze jakies problemy 🙂
Hej
Mam pytanie jak zrobi, żeby na moim trace viewer wydoczne były te znaczki “>” które dają opcje rozwinięcia danego kroku?
Moment o którym mówię, występuje w tej lekcji w 22:41, gdy Przemek rozwija sobie hook beforeEach i ma podgląd na kod, ja nie mam takiej możliwości, i znak > mam tylko przy beforeEach ale jak go rozwijam to nie widze kodu 🙁
P.S. Przydałaby się możliwość wklejania niewielkich screenów w komentarzach 🙂
Fajne pytanie – obecnie już nie ma możliwości bezpośrednio w raporcie podglądnąć kodu z danych akcji.
Takie informacje są zawarte w podglądzie Trace Viewer (w obecnym kodzie pojawia się tylko w przypadku faila testu) lub z pomocą UI mode (otwieranego
npx playwright test --ui)Tutaj przyznam, że twórcy wprowadzają wiele zmian, nie zawsze na plus🫡
Co do obrazków w komentarzach to niestety nasza obecna platforma nie ma wsparcia ale śmiało przyjmie linki z dowolnego serwisu gdzie takie obrazki można bezkarnie wklejać.
Hej, dzięki za odpowiedź. Tak się właśnie domyślałem, że przypuszczalnie brak podglądu to wynik zmian w Playwright, ale wolałem się upewnić 🙂
fajny pomysł, na przyszłość, będę uploadował i podrzucał link 🙂
Cześć, czy możemy w jakiś prosty sposób wyłączyć użycie beforeEach dla jednego testu, który jest w zestawie. Jednocześnie chcemy, żeby ten test był zawarty w danym zestawie testów. W skrócie chodzi o to, że dla jednego testu chcielibyśmy użyć innych danych do logowania 🙂
Hej,
Są przynajmniej 2 sposoby 🙂
Dużo zależy co dokładnie chcesz osiągnąć oraz jak wyglądają Twoje testy.
Sposób pierwszy: odpowiednia hierarchia describe oraz test:
import { test } from '@playwright/test'; test.describe('🟦 Sample test suit', () => { test.beforeEach(async ({ page }) => { console.log('🟦 Before each'); }); test('test 1', async ({ page }) => { console.log('🟦 test 1'); }); test.describe('🟧 Inner sample test suit', () => { test.beforeEach(async ({ page }) => { console.log('🟧 Before each'); }); test('test 2', async ({ page }) => { console.log('🟧 test 2'); }); }); });W rezultacie otrzymamy całość w takiej kolejności:
Sposób drugi: warunki w beforeEach. Poniżej pomijamy beforeEach przed testem o nazwie “test 2”:
test.describe.only('🟦 Sample test suit', () => { test.beforeEach(async ({ page }, testInfo) => { if (testInfo.title === 'test 2') { return; } console.log('🟦 Before each'); }); test('test 1', async ({ page }) => { console.log('🟦 test 1'); }); test('test 2', async ({ page }) => { console.log('🟧 test 2'); }); });A na konsoli otrzymamy:
Super, bardzo dziękuję za odpowiedź i gotowe rozwiązania. Ten drugi sposób już przetestowałam w praktyce i zadziałało od razu 🙂 nie wiem jeszcze na ile tego typu rozwiązania sprawdzają się w komercyjnych, dużych projekcie (w firmie jesteśmy na dość wczesnym etapie wdrażania testów automatycznych), ale na teraz bardzo się przydało. Jeszcze raz dzięki!
Osobiście sugerowałbym pierwsze – pozwala dodatkowo na grupowanie testów w zagnieżdżone
;) Co do rozwiązania z [js]if (testInfo.title === 'test 2') {– ma jeden poważny minus – opiera się na nazwie testów. Jednak można je wykorzystać, gdy w testach wprowadzimy tagi ( https://playwright.dev/docs/test-annotations#tag-tests ), czyli w nazwie testu dodamy np
"@high"czy"@low"(np. w kontekście priorytetów). Wtedy w takim Before możemy zastosować daną akcję jedynie w kontekście testów z danym tagiem:if (testInfo.title.includes("@high")) {Jedno i drugie rozwiązanie ma swoje wady i zalety – wszystko zależy od kontekstu i potrzeb 😉
błąd odnaleziony 🙂 braki/za dużo ‘});’ w całym kodzie -składnia
testy już działają
Doskonale! 😉
1) [chromium] › desktop.spec.ts:36:5 › successful mobile top-up ──────────────────────────────────
Test timeout of 50000ms exceeded.
Error: locator.selectOption: Target closed
=========================== logs ===========================
waiting for locator(‘#uniform-widget_1_topup_receiver’)
prośba o wyjaśnienie 🙂 zminiłam timeout na 5000 ale pomimo tego jakoś długo szuka
Hej,
Jak wygląda Twój cały kod projektu wraz z konfiguracją? 🙂
Jak prezentuje się przebieg tego testu na video i screenshotach (powstałych po teście)?
import { test, expect } from '@playwright/test'; test.describe('Desktop tests', () => { test.beforeEach(async ({ page }) => { const userId = 'testerLO'; const userPassword = '10987654'; const url = 'https://demo-bank.vercel.app/'; await page.goto(url); await page.getByTestId('login-input').fill(userId); await page.getByTestId('password-input').fill(userPassword); await page.getByTestId('login-button').click(); }); test('quick payment with correct data', async ({ page }) => { // Arrange const receiverId = '2'; const transferAmount = '150'; const transferTitle = 'pizza'; const expectedTransferReceiver = 'Chuck Demobankowy'; // Act await page.locator('#widget_1_transfer_receiver').selectOption(receiverId); await page.locator('#widget_1_transfer_amount').fill(transferAmount); await page.locator('#widget_1_transfer_title').fill(transferTitle); await page.getByRole('button', { name: 'wykonaj' }).click(); await page.getByTestId('close-button').click(); // Assert await expect(page.locator('#show_messages')).toHaveText( `Przelew wykonany! ${expectedTransferReceiver} - ${transferAmount},00PLN - ${transferTitle}`, ); }); }); test.only('successful mobile top-up', async ({ page }) => { // Arrange const topUpReceiver = '500 xxx xxx'; const topUpAmount = '50'; const exprectedMessage = `Doładowanie wykonane! ${topUpAmount},00PLN na numer ${topUpReceiver}`; // Act await page.locator('#widget_1_topup_receiver').selectOption(topUpReceiver); await page.locator('#widget_1_topup_amount').fill(topUpAmount); await page.locator('#uniform-widget_1_topup_agreement span').click(); await page.getByRole('button', { name: 'doładuj telefon' }).click(); await page.getByTestId('close-button').click(); // Assert await expect(page.locator('#show_messages')).toHaveText(exprectedMessage); });Tu wynik z test viewera:
successful mobile top-up desktop.spec.ts:36 Test timeout of 30000ms exceeded. Error: locator.fill: Target closed =========================== logs =========================== waiting for locator('#widget_1_topup_amount') ============================================================ 43 | await page.locator('#widget_1_topup_receiver').selectOption(topUpReceiver); 44 | await page.locator('#widget_1_topup_amount').fill(topUpAmount); > 45 | await page.locator('#uniform-widget_1_topup_agreement span').click(); | ^ 46 | await page.getByRole('button', { name: 'doładuj telefon' }).click(); 47 | await page.getByTestId('close-button').click(); 48 | at C:\Projects\demo-bank-tests\tests\desktop.spec.ts:45:48 Pending operations: - locator.fill at tests\desktop.spec.ts:45:48 Test Steps 1.0s OK Before Hooks 29.8s X locator.fill(#widget_1_topup_amount)— desktop.spec.ts:45 3.2s OK After Hooksvideo jest białe – przez 29 s trwania
Trace: Call: locator.fill: Target closed =========================== logs =========================== waiting for locator('#widget_1_topup_amount') ============================================================test ‘successful mobile top-up’ jest poza describe wiec beforeEach czyli nawigacja i logowanie sie nie wykonuje. Zastanawiajace jest jednak czemu sie nie sypie na linijce 43 tylko na linijce 44
Fakt, zastanawiające, chociaż u mnie na wersji Playwright 1.38.1 błąd leci właśnie na linii 43:
42 | // Act > 43 | await page.locator('#widget_1_topup_receiver').selectOption(topUpReceiver); | ^ 44 | await page.locator('#widget_1_topup_amount').fill(topUpAmount); 45 | await page.locator('#uniform-widget_1_topup_agreement span').click(); 46 | await page.getByRole('button', { name: 'doładuj telefon' }).click();Więc obstawiałbym, że może coś w tamtej wersji Playwrighta mogło być zepsute 🤔