Шаблоны проектирования (альтернативные названия: паттерны проектирования, паттерны программирования, паттерны дизайна, дизайн-паттерны) — готовые подходы, типичные способы решения часто встречающихся проблем при проектировании ПО, и в частности автотестов. Таких повторяемых архитектурных решений существует около 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-инженер получает возможность создавать мощные масштабируемые тестовые наборы валидации функциональности и производительности.
***