DRY i hook beforeEach

TIP: Ta lekcja jest częścią rozwijanego Programu Testy Automatyczne z Playwright 🎭

Prezentacja

DRY i hook beforeEach

DRY i hook beforeEach

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();
  });
TIP: Pamiętaj o zasadzie: jeśli używamy zmiennej w obrębie tylko jednego bloku kodu staramy się aby nie była ona dostępna dla innych.

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ę.

TIP: Zauważ:

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.

28 komentarzy

  1. 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 🙁

    Avatar Jacek Kraś
      1. 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);
        
        Avatar Jacek Kraś
        1. 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!

          Przemek Przemek
    1. 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 😉

      Krzysiek Kijas Krzysiek Kijas
      1. 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

        Avatar Jakub Kruszyński
  2. 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

    Avatar Jakub Kruszyński
  3. 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.

    Avatar Marek
    1. 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ść…

      Avatar Marek
      1. 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 🙂

        Krzysiek Kijas Krzysiek Kijas
  4. 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 🙂

    Avatar Franciszek Klocek
    1. 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ć.

      Przemek Przemek
      1. 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 🙂

        Avatar Franciszek Klocek
  5. 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 🙂

    Avatar Paulina Gruca
    1. 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:

      [chromium-non-logged]  🟦 Sample test suit › 🟧 Inner sample test suit › test 2
      🟦 Before each
      🟧 Before each
      🟧 test 2
      [chromium-non-logged]  🟦 Sample test suit › test 1
      🟦 Before each
      🟦 test 1
        2 passed (2.5s)
      

      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:

      [chromium-non-logged]  🟦 Sample test suit › test 2
      🟧 test 2
      [chromium-non-logged]  🟦 Sample test suit › test 1
      🟦 Before each
      🟦 test 1
      
      Krzysiek Kijas Krzysiek Kijas
      1. 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!

        Avatar Paulina Gruca
        1. 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 😉

          Krzysiek Kijas Krzysiek Kijas
  6. 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

    Avatar Karolina Zakrzewska
      1. 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 Hooks
        
        Avatar Karolina Zakrzewska
        1. video jest białe – przez 29 s trwania

          Trace:
          
          Call:
          locator.fill: Target closed
          =========================== logs ===========================
          waiting for locator('#widget_1_topup_amount')
          ============================================================
          
          Avatar Karolina Zakrzewska
        2. 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

          Avatar Tomasz Solarz
          1. 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 🤔

            Krzysiek Kijas Krzysiek Kijas

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *