Шаблоны проектирования (альтернативные названия: паттерны проектирования, паттерны программирования, паттерны дизайна, дизайн-паттерны) — готовые подходы, типичные способы решения часто встречающихся проблем при проектировании ПО, и в частности автотестов. Таких повторяемых архитектурных решений существует около 100, рассмотрим самые распространенные.
Page Object Pattern
Популярный паттерн, делающий код реюзабельным (легким для повторного использования во многих местах) и упрощающий его обслуживание в дальнейшем, путем инкапсуляции данных приложения в специальных объектах. Эти объекты представляют собой отдельные страницы или секции веб-приложения, содержащие нужные элементы и методы для работы с ними.
Далее пример JS-кода, как Page Object Pattern (POM-паттерн) работает в Playwright [прим.ред. — здесь полный гайд по Playwright, или маленький быстрый туториал для первого знакомства с этим замечательным фреймворком для веба]. Здесь мы создаем Page Object для страницы логина и используем ее для взаимодействия с элементами страницы и выполнения операции входа.
Создаем файл LoginPage.js с классом LoginPage:
class LoginPage {
constructor(page) {
this.page = page;
}
async navigate(url) {
await this.page.goto(url);
}
async setUsername(username) {
await this.page.fill('#username', username);
}
async setPassword(password) {
await this.page.fill('#password', password);
}
async clickLoginButton() {
await this.page.click('#login-button');
}
async login(username, password) {
await this.setUsername(username);
await this.setPassword(password);
await this.clickLoginButton();
}
}
module.exports = LoginPage;
Теперь создаем test.js, задействующий класс LoginPage в операции логина, используя Playwright:
const { chromium } = require('playwright');
const LoginPage = require('./LoginPage');
(async () => {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
const loginPage = new LoginPage(page);
await loginPage.navigate('https://example.com/login');
await loginPage.login('username', 'password');
// Add your test assertions or further actions here.
await browser.close();
})();
В этом примере мы создали класс LoginPage, инкапсулирующий взаимодействия с элементами страницы логина. Файл test.js использовал класс LoginPage для: перехода на страницу, заполнения юзернейма и пароля, и нажатия кнопки входа.
Чем полезен паттерн Page Object Model:
- Упрощает рутинную работу со скриптами — выведением сложных взаимодействий на более абстрактный уровень
- Это отделение делает тесты более легкими в обслуживании потом
- Снижает дублирование кода, следовательно уменьшает вероятность рутинных ошибок
Подробнейший гайд по этому паттерну давно лежит на сайте.
Factory Method Pattern
Паттерн создания объектов без указания их конкретных классов. В QA он применяется для генерации тестовых объектов (тестовых данных, тест-кейсов, или даже тестовых окружений).
Далее пример на TypeScript, как этот паттерн создает тестовые данные для регистрации пользователей. Описываем абстрактный класс UserDataFactory и несколько factory-классов, имплементирующих create()-метод для генерации пользовательских данных с нужными характеристиками.
Сначала создаем файл UserData.ts с интерфейсом UserData:
interface UserData {
username: string;
email: string;
password: string;
}
export default UserData;
Далее создаем файл UserDataFactory.ts, содержащий абстрактный класс UserDataFactory:
import UserData from './UserData';
abstract class UserDataFactory {
abstract create(): UserData;
}
export default UserDataFactory;
Теперь создаем два конкретных factory-класса в отдельных файлах. Сначала создаем файл ValidUserDataFactory.ts:
import UserDataFactory from './UserDataFactory';
import UserData from './UserData';
class ValidUserDataFactory extends UserDataFactory {
create(): UserData {
return {
username: 'ValidUser',
email: 'validuser@example.com',
password: 'ValidPassword123',
};
}
}
export default ValidUserDataFactory;
Теперь файл InvalidUserDataFactory.ts:
import UserDataFactory from './UserDataFactory';
import UserData from './UserData';
class InvalidUserDataFactory extends UserDataFactory {
create(): UserData {
return {
username: 'InvalidUser',
email: 'invaliduser@invalid',
password: 'short',
};
}
}
export default InvalidUserDataFactory;
И в завершение создаем файл test.ts, использующий factory-классы для генерации тестовых данных:
import { chromium } from 'playwright';
import UserDataFactory from './UserDataFactory';
import ValidUserDataFactory from './ValidUserDataFactory';
import InvalidUserDataFactory from './InvalidUserDataFactory';
(async () => {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
const validUserDataFactory: UserDataFactory = new ValidUserDataFactory();
const invalidUserDataFactory: UserDataFactory = new InvalidUserDataFactory();
const validUserData = validUserDataFactory.create();
const invalidUserData = invalidUserDataFactory.create();
// Use the generated test data to interact with the web application and perform test assertions.
await browser.close();
})();
Чем полезен паттерн:
- Упрощает создание объектов, абстрагируя создание экземпляров
- Улучшает масштабируемость — проще добавление новых типов объектов
- Подстановка имплементации объектов без изменения кода
Data-Driven-тестирование
Паттерн, основанный на отделении тестовых данных от тестовых сценариев. Это позволяет запускать тест-кейсы каждый раз с новыми наборами входных данных. Полезно в сценариях валидации с множеством комбинаций входных данных (то есть довольно часто в QA).
Пример на TypeScript в vitest. Проверяется метод, суммирующий два числа:
test.each([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
])('add(a %i, b %i) -> c %i', (a, b, expected) => {
expect(a + b).toBe(expected)
})
Чем полезен такой паттерн:
- Увеличивает тестовое покрытие, с минимальным дублированием кода
- Упрощает обслуживание тестовых данных
- Улучшает читаемость кода
Паттерн Singleton
Дизайн-паттерн, ограничивающий создание экземпляра класса — всего одним экземпляром. В QA-тестировании Singleton применяется для расшаривания ресурсов (тех же тестовых конфигураций), или экземпляров страницы или браузера, на несколько тест-кейсов. Это полезно, когда понадобилось создать глобальные конфигурации запуска тестов, например для проверки отдельных геолокаций.
Далее пример на TypeScript, как расшарить экземпляр браузера в Playwright на несколько тест-кейсов. Синглтон-паттерн создает только один экземпляр браузера, и передает его в тестовый набор.
Создаем файл BrowserSingleton.ts для экземпляра браузера:
import { Browser, chromium } from 'playwright';
class BrowserSingleton {
private static instance: Browser | null = null;
private constructor() {}
public static async getInstance(): Promise<Browser> {
if (!this.instance) {
this.instance = await chromium.launch();
}
return this.instance;
}
}
export default BrowserSingleton;
Теперь в тестовых файлах используем расшаренный (то есть общий для этих тестов) экземпляр браузера из BrowserSingleton.ts:
import { Page, BrowserContext } from 'playwright';
import BrowserSingleton from './BrowserSingleton';
let context: BrowserContext;
let page: Page;
beforeAll(async () => {
const browser = await BrowserSingleton.getInstance();
context = await browser.newContext();
page = await context.newPage();
});
afterAll(async () => {
await context.close();
});
// Write your tests using the shared browser instance.
Синглтон-паттерн в QA:
- Предотвращает проблемы, связанные с общим использованием ресурсов
- Создает глобальную точку доступа к ресурсам (данным)
- Снижает нагрузку на железо, повышая производительность
- Делает конфигурации реюзабельными
Strategy-паттерн
«Поведенческий» (бихевиоральный) паттерн дизайна ПО, в котором описывают группу (семейство) алгоритмов, инкапсулируя их и делая взаимозаменяемыми. В QA применяется для активации разных тестовых стратегий в процессе выполнения.
В частности, уменьшает стоимость операций непрерывной интеграции. Например, при локальном запуске теста в Playwright применяем реальный API, а при запуске в CI-окружении, например в GitHub Actions, просто переключаемся на эмулированные данные из моков. Это экономно, поскольку «время в CI» стОит достаточно дорого, а тестов может быть очень много.
Преимущества Strategy-паттерна в QA:
- Гибкое оперативное применение разнообразных подходов к тестированию (стратегий)
- Что улучшает возможности валидации тестов
- Экономия времени и стоимости CI
Итак
Дизайн-паттерны в руках опытного автоматизатора играют заметную роль в повышении эффективности автотестов. Применяя хотя бы перечисленные выше паттерны, QA-инженер получает возможность создавать мощные масштабируемые тестовые наборы валидации функциональности и производительности.
***