Powrót do: Praktyczne wprowadzenie do testów automatycznych z Playwright
Rozwiązanie – podstawowy POM w testach
TIP: Ta lekcja jest częścią rozwijanego Programu Testy Automatyczne z Playwright 🎭
Dodatkowe materiały
Rozwiązanie prezentowane w tej lekcji zajdziesz w naszym repozytorium: L06_pom_refactor
Modele stron
Zawartość payment.page.ts:
import { Page } from '@playwright/test'; export class PaymentPage { constructor(private page: Page) {} transferReceiverInput = this.page.getByTestId('transfer_receiver'); transferToInput = this.page.getByTestId('form_account_to'); transferAmountInput = this.page.getByTestId('form_amount'); transferButton = this.page.getByRole('button', { name: 'wykonaj przelew' }); actionCloseButton = this.page.getByTestId('close-button'); messageText = this.page.locator('#show_messages') }
Zawartość pulpit.page.ts:
import { Page } from '@playwright/test'; export class PulpitPage { constructor(private page: Page) {} transferReceiverInput = this.page.locator('#widget_1_transfer_receiver'); transferAmountInput = this.page.locator('#widget_1_transfer_amount'); transferTitleInput = this.page.locator('#widget_1_transfer_title'); transferButton = this.page.getByRole('button', { name: 'wykonaj' }); actionCloseButton = this.page.getByTestId('close-button'); messageText = this.page.locator('#show_messages'); topupReceiverInput = this.page.locator('#widget_1_topup_receiver'); topupAmountInput = this.page.locator('#widget_1_topup_amount'); topupAgreementCheckbox = this.page.locator( '#uniform-widget_1_topup_agreement span' ); topupExecuteButton = this.page.getByRole('button', { name: 'doładuj telefon', }); moneyValueText = this.page.locator('#money_value'); userNameText = this.page.getByTestId('user-name') }
Testy wykorzystujące modele stron
Zawartość payment.spec.ts:
import { test, expect } from '@playwright/test'; import { loginData } from '../test-data/login.data'; import { LoginPage } from '../pages/login.page'; import { PaymentPage } from '../pages/payment.page'; test.describe('Payment tests', () => { test.beforeEach(async ({ page }) => { const userId = loginData.userId; const userPassword = loginData.userPassword; await page.goto('/'); const loginPage = new LoginPage(page) await loginPage.loginInput.fill(userId) await loginPage.passwordInput.fill(userPassword) await loginPage.loginButton.click() await page.getByRole('link', { name: 'płatności' }).click(); }); test('simple payment', async ({ page }) => { // Arrange const transferReceiver = 'Jan Nowak'; const transferAccount = '12 3456 7890 1234 5678 9012 34568'; const transferAmount = '222'; const expectedMessage = `Przelew wykonany! ${transferAmount},00PLN dla Jan Nowak`; // Act const paymentPage = new PaymentPage(page) await paymentPage.transferReceiverInput.fill(transferReceiver) await paymentPage.transferToInput.fill(transferAccount) await paymentPage.transferAmountInput.fill(transferAmount) await paymentPage.transferButton.click() await paymentPage.actionCloseButton.click() // Assert await expect(paymentPage.messageText).toHaveText(expectedMessage); }); });
Zawartość pulpit.spec.ts:
import { test, expect } from '@playwright/test'; import { loginData } from '../test-data/login.data'; import { LoginPage } from '../pages/login.page'; import { PulpitPage } from '../pages/pulpit.page'; test.describe('Pulpit tests', () => { test.beforeEach(async ({ page }) => { const userId = loginData.userId; const userPassword = loginData.userPassword; await page.goto('/'); const loginPage = new LoginPage(page) await loginPage.loginInput.fill(userId) await loginPage.passwordInput.fill(userPassword) await loginPage.loginButton.click() }); test('quick payment with correct data', async ({ page }) => { // Arrange const receiverId = '2'; const transferAmount = '150'; const transferTitle = 'pizza'; const expectedTransferReceiver = 'Chuck Demobankowy'; // Act const pulpitPage = new PulpitPage(page) await pulpitPage.transferReceiverInput.selectOption(receiverId); await pulpitPage.transferAmountInput.fill(transferAmount); await pulpitPage.transferTitleInput.fill(transferTitle); await pulpitPage.transferButton.click(); await pulpitPage.actionCloseButton.click(); // Assert await expect(pulpitPage.messageText).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 const pulpitPage = new PulpitPage(page) await pulpitPage.topupReceiverInput.selectOption(topUpReceiver); await pulpitPage.topupAmountInput.fill(topUpAmount); await pulpitPage.topupAgreementCheckbox.click(); await pulpitPage.topupExecuteButton.click(); await pulpitPage.actionCloseButton.click(); // Assert await expect(pulpitPage.messageText).toHaveText(expectedMessage); }); test('correct balance after successful mobile top-up', async ({ page }) => { // Arrange const pulpitPage = new PulpitPage(page) const topUpReceiver = '500 xxx xxx'; const topUpAmount = '50'; const initialBalance = await pulpitPage.moneyValueText.innerText(); const expectedBalance = Number(initialBalance) - Number(topUpAmount); // Act await pulpitPage.topupReceiverInput.selectOption(topUpReceiver); await pulpitPage.topupAmountInput.fill(topUpAmount); await pulpitPage.topupAgreementCheckbox.click(); await pulpitPage.topupExecuteButton.click(); await pulpitPage.actionCloseButton.click(); // Assert await expect(pulpitPage.moneyValueText).toHaveText(`${expectedBalance}`); }); });
Zawartość login.spec.ts:
import { test, expect } from '@playwright/test'; import { loginData } from '../test-data/login.data'; import { LoginPage } from '../pages/login.page'; import { PulpitPage } from '../pages/pulpit.page'; test.describe('User login to Demobank', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); }); test('successful login with correct credentials', async ({ page }) => { // Arrange const userId = loginData.userId; const userPassword = loginData.userPassword; const expectedUserName = 'Jan Demobankowy'; // Act const loginPage = new LoginPage(page); await loginPage.loginInput.fill(userId); await loginPage.passwordInput.fill(userPassword); await loginPage.loginButton.click(); // Assert const pulpitPage = new PulpitPage(page); await expect(pulpitPage.userNameText).toHaveText(expectedUserName); }); test('unsuccessful login with too short username', async ({ page }) => { // Arrange const incorrectUserId = 'tester'; const expectedErrorMessage = 'identyfikator ma min. 8 znaków'; // Act const loginPage = new LoginPage(page); await loginPage.loginInput.fill(incorrectUserId); await loginPage.passwordInput.click(); // Assert await expect(loginPage.loginError).toHaveText( expectedErrorMessage ); }); test('unsuccessful login with too short password', async ({ page }) => { // Arrange const userId = loginData.userId; const incorrectPassword = '1234'; const expectedErrorMessage = 'hasło ma min. 8 znaków'; // Act const loginPage = new LoginPage(page); await loginPage.loginInput.fill(userId); await loginPage.passwordInput.fill(incorrectPassword); await loginPage.passwordInput.blur(); // Assert await expect(loginPage.passwordError).toHaveText( expectedErrorMessage ); }); });
Hej, robiąc samemu zwróciłem uwagę na to, że w PulpitPage łatwo robi się bałagan w property. Nie pomyślałem, że można trochę to wszystko uporządkować pustymi liniami i stwierdziłem, że stworzę obiekty z lokatorami właściwymi dla każdego okienka.
W samych testach trochę się wydłużyła ścieżka do lokatora. Pytanie, czy to w ogóle ma sens, czy to już przerost formy nad treścią?
wykorzystanie w teście:
Hej,
Bardzo słuszna obserwacja i bardzo dobry krok, szczególnie w bardziej rozbudowanych stronach 🙂
Takie podejście poprawia czytelność, a to jest spory plus. Również dłuższe ścieżki w testach
await pulpitPage.quickPayment.transferTitle.fill(transferTitle)
to tylko chwilowa niedogodność – w przyszłych lekcjach (o agregacji akcji) zobaczysz dlaczego 😉Zauważ też, że możemy iśc jeszcze dalej – z wydzielonych przez Ciebie sekcji (jak
quickPayment
) przygotować nowe moduły (pliki) z klasami odpowiedzialnymi za daną sekcję (np.export class QuickPaymentSection
). Takie klasy stają się wtedy komponentami, a strona składałaby się z takich komponentów.I tego typu podejście to kompozycja 😉
Dzięki temu możemy jeszcze zmniejszyć wielkość implementacji takiej strony o całe takie sekcje:
Reasumując – w przypadku rozbudowanych stron, gdzie można na stronie wydzielić takie logiczne całości/sekcje, to dobry krok w stronę zmniejszenia wielkości plików i klas 🙂
Natomiast w przypadku niewielkich testów (tych prezentowanych w tej lekcji) mozna jeszcze trzymać wszystko w jednym pliku.
Czy istnieje możliwość zadeklarowania obiektów typu page.ts w poszczególnych klasach testów, żeby były dostępne we wszystkich testach, które znajdują się w danej klasie, bez konieczności duplikacji linijki w formie np. const loginPage = new LoginPage(page); w każdym z testów w danej klasie? 🙂
Jak najbardziej, jest kilka sposobów na to.
1. Na początek (najprościej) można to zrobić klasyczną kompozycją. Czyli wydzielić kod logowania do osobnej funkcji (i umieścić ją w osobnym pliku z np. funkcjami pomocniczymi lub po prostu login.helper.ts ):
i po prostu wywoływać ją przed wszystkimi akcjami w teście:
2. Można to zrealizować za pomocą projektów zależnych (omawiamy je na tym webinarze: https://jaktestowac.pl/pw-strategie/ lub jeśli masz dostęp do pełnego Programu to jest to tutaj: https://jaktestowac.pl/lesson/pw9-web1/). W tym przypadku używamy pliku sesji w testach i nie musimy się logować w testach, które podłączymy pod projekt.
3. Można również zrobić poprzez fixture, który umożliwi zdefiniowanie takich akcji i użycie ich w dowolnym teście (np. zalogowanie się). Jeśli masz dostęp do pełnego Programu tu znajdziesz pełną sekcję o fixtures: https://jaktestowac.pl/lesson/pw2s09l01/).
Zacznij od najprostszych rozwiązań a potem śmiało rozwijaj i testuj różne podejścia 😎
Hej,
W pulpit.page.ts i payment.page.ts mamy dwa identyczne properties:: actionCloseButton i messageText.
Czy złą praktyką jest umieszczenie ich tylko w jednym pliku (np. w pulpit.page,ts) i potem wykorzystywanie z tego miejsca również w testach payment?
Hej,
Gdybyśmy umieścili te elementy na stronie pulpit.page,ts, a później z nich korzystali w payment.page.ts – to mamy tu zależność elementów między stronami. Strona payment zależy od pulpit – co nie jest uznawane za dobrą praktykę 😉
Dlatego w kolejnych lekcjach opowiadam o komponentach (i kompozycji) – czyli wydzielaniu takich wspólnych elementów, a później ich reużywanie w rożnych stronach 🙂 Dzięki temu zmniejszamy duplikacje.
W komponentach możemy wydzielać różne elementy, które są wspólne dla wielu stron – lewe menu, popupy etc 🙂