Добро пожаловать!

В сегодняшней статье я расскажу о одном из моих любимых паттернов для тестирования пользовательского интерфейса. Я не буду вдаваться в подробности о том, что это такое и почему его следует использовать. Моя цель сегодня — продемонстрировать реализацию этого паттерна при работе с Playwright и Javascript/Typescript. Если после прочтения и анализа примеров реализации у вас все еще останутся вопросы, я рекомендую подробнее почитать об этом паттерне.

Итак, начнем ?

Сначала на Java

Данная статья это перевод с английского с некоторыми адаптациями. Перевод сделан Inzhenerka.Tech совместно с автором тренажера по “Playwright для тестировщика” Дмитрием Ереминым

Мне очень нравится, когда при написании автоматических тестов мне не нужно помнить, на каком этапе сценария я нахожусь и какая страница открыта в данный момент. Кажется, что это мелочь, но на самом деле это усложняет разработку из-за так называемого информационного шума. Прежде всего, когда я занят написанием теста, я хочу сосредоточиться на логике теста, а не держать в голове кучу служебной информации.

Эту проблему легко решить в языках программирования с типизацией (таких как Java, C#). Я покажу вам пример, и вы сразу поймете, что речь идет о паттерне Fluent API

@Test
void testExample() {
    HomePage page = Pages.login.open()
        .fillUsernameInput(Config.correctUsername)
	.fillUsernamePassword(Confid.correctPass)
	.clickLoginButton();
    assertTrue(page.isInventoryListVisible());

}

Помимо того, что код выглядит очень читаемым, есть еще один побочный эффект: мне не нужно думать о том, где я нахожусь в приложении после выполнения определенного действия, вся логика переходов (по сути, граф нашего приложения) описана внутри объектов страниц (Page Objects). Каждый метод объекта страницы возвращает тип страницы, который ожидается после выполнения действия.

Однако есть проблема…

В отличие от синхронного кода в вышеупомянутых языках, Javascript или Typescript по своей природе не являются синхронными, что означает, что такой код не может быть написан, потому что все методы взаимодействия со страницей будут возвращать Promise<T>. Позвольте мне показать вам это на примере метода open() какой-то страницы (это будет полезно тем, кто не очень знаком с Promise или только начинает понимать основы Javascript):

export class LoginPage {
    // locators, constructors etc.
/*
Here we need to return this page as a result as we know we're on current
page after it's open
*/
public async open(): Promise&lt;LoginPage&gt; {
    await this.page.goto(this.url);
    return this;
}

}

И теперь я не могу использовать паттерн Fluent API из-за асинхронности Javascript:

Снимок экрана 2024-07-24 в 16.32.18.png
Снимок экрана 2024-07-24 в 16.32.18.png

Какие у нас есть варианты реализации?

Самый простой

Наиболее очевидный способ — ждать завершения действия и возвращения следующего объекта страницы шаг за шагом:

let loginPage: LoginPage;
test.beforeEach(({page}) => {
loginPage = new LoginPage(page);
});
test('User can login', async () => {
let login = await loginPage.open();
login = await loginPage.setUsername('standard_user');
login = await loginPage.setPassword('secret_sauce');
let home = await loginPage.clickLoginButton();
expect(await home.isInventoryListVisible()).toBeTruthy();

});

Честно говоря, это выглядит не очень хорошо ?. Нужно отметить, что такой код также не решает проблему контекста. Мне все равно приходится помнить, на какой странице я нахожусь в данный момент.

Следующий шаг — использование then()

Класс Promise позволяет нам использовать метод then() и возвращаемое значение для построения цепочек вызовов. Давайте немного изменим наш код и посмотрим, что у нас получится:

let loginPage: LoginPage;
test.beforeEach(({page}) => {
loginPage = new LoginPage(page);
});
test('User can login', async () => {
await loginPage.open()
.then(async (page) => await loginPage.setUsername('standard_user'))
.then(async (page) => await loginPage.setPassword('secret_sauce'))
.then(async (page) => await loginPage.clickLoginButton())
.then(async (page) => expect(await page.isInventoryListVisible()).toBeTruthy());
});

Теперь лучше, не нужно запоминать текущую страницу. В принципе, можно использовать это, но все еще выглядит не очень красиво.

Элегантный способ

Прежде всего, хочу поблагодарить Энтони Гора — именно он познакомил меня с замечательной библиотекой, которая помогает решить проблему вызова цепочки методов.

Итак, первое, что нужно сделать, — это добавить новую зависимость в проект:

npm i proxymise

Если вы работаете с проектом на JavaScript, все в порядке, можно использовать библиотеку.

Если проект работает с TypeScript, потребуются дополнительные действия:

  1. Проверьте, что команда tsc -v работает. Если нет, вам нужно установить дополнительную зависимость: npm i tsc.

  2. Затем выполните tsc --init и посмотрите на результат. Там также будет указано, как исправить ошибки, если они возникнут. Если команда была выполнена успешно, в вашем проекте должен появиться файл tsconfig.json.

  3. Теперь необходимо подключить proxymise в файлы объектов страниц и обернуть класс так, чтобы его можно было использовать в тестах в формате Fluent API. Полный код проекта выглядит так:

// pages/login-page.ts
import {Locator, Page} from "@playwright/test";
import {HomePage} from "./home-page";
import proxymise from "proxymise";
export class LoginPage {
private page: Page;
// Make this field static to use in static method
private static url = '<https://www.saucedemo.com/>';
private usernameField: Locator;
private passwordField: Locator;
private loginButton: Locator;

constructor(page: Page) {
    this.page = page;

    this.usernameField = this.page.locator('#user-name');
    this.passwordField = this.page.locator('#password');
    this.loginButton = this.page.locator('#login-button');
}

// Этот метод теперь статический. Это необходимо для корректной работы proxymise.
public static async open(page: Page): Promise&lt;LoginPage&gt; {
    await page.goto(LoginPage.url);
    return new LoginPage(page);
}

public async setUsername(name: string): Promise&lt;LoginPage&gt; {
    await this.usernameField.fill(name);
    return this;
}

public async setPassword(pass: string): Promise&lt;LoginPage&gt; {
    await this.passwordField.fill(pass);
    return this;
}

public async clickLoginButton(): Promise&lt;HomePage&gt; {
    await this.loginButton.click();
    return new HomePage(this.page);
}

}
// Оберните этот класс с помощью proxymise, чтобы избежать этого в коде тестов.
export default proxymise(LoginPage);
// pages/home-page.ts
import {Locator, Page} from "@playwright/test";
import proxymise from "proxymise";
export class HomePage {
private page: Page;
private static url = '<https://www.saucedemo.com/inventory.html>';
private inventoryList: Locator;

constructor(page: Page) {
    this.page = page;

    this.inventoryList = page.locator('div.inventory_list');
}

public static async open(page: Page): Promise&lt;HomePage&gt; {
    await page.goto(HomePage.url);
    return new HomePage(page);
}

public async isInventoryListVisible(): Promise&lt;boolean&gt; {
    return await this.inventoryList.isVisible();
}

}
export default proxymise(HomePage);
// tests/saucedemo.spec.ts
import {test, expect} from '@playwright/test';
import LoginPage from "../pages/login-page";
test('User can login', async ({page}) => {
const isInventoryShown = await LoginPage.open(page)
.setUsername('standard_user')
.setPassword('secret_sauce')
.clickLoginButton()
.isInventoryListVisible();
expect(isInventoryShown).toBeTruthy();

});

Обратите внимание, насколько лаконичным стал наш тест. Это решение ничем не отличается от стандартного Fluent API в Java или C#.

Выводы

Этот пример является базовым для понимания того, как добиться Fluent API в тестах, написанных на Playwright и Typescript. Однако он не является окончательным, его можно улучшать, добавляя новые функции и концепции для облегчения работы разработчиков и поддержки кода. В любом случае, выполнение такого упражнения будет полезным для развития навыков программирования ?

Удачи с вашими проектами и не прекращайте автоматизировать!

Данная статья это перевод с английского с некоторыми адаптациями. Перевод сделан Inzhenerka.Tech совместно с автором первого тренажера по “Playwright для тестировщика” Дмитрием Ереминым

Комментарии (1)


  1. ivegner
    24.07.2024 18:22

    В примере с ".then()" использование синтаксиса async/await совершенно излишне.