Абстракции и инкапсуляция

В тестировании веб-приложений на JavaScript с помощью Puppeteer абстракция и инкапсуляция играют ключевую роль в поддерживаемости и масштабируемости тестов. Puppeteer предоставляет низкоуровневый API для управления браузером Chromium, но прямое использование всех методов без структуры быстро приводит к хаосу в проекте. Абстракции позволяют скрыть детали реализации, а инкапсуляция — ограничить доступ к внутренним механизмам, оставляя внешний интерфейс понятным и стабильным.


Создание абстракций для страниц и компонентов

Паттерн Page Object Model (POM) является стандартом для организации тестов. Каждая страница или компонент приложения инкапсулируется в отдельный класс, который управляет взаимодействием с DOM и предоставляет методы высокого уровня.

class LoginPage {
    constructor(page) {
        this.page = page;
        this.selectors = {
            usernameInput: &
            passwordInput: '#password',
            loginButton: 'button[type="submit"]'
        };
    }

    async navigate() {
        await this.page.goto('https://example.com/login');
    }

    async login(username, password) {
        await this.page.type(this.selectors.usernameInput, username);
        await this.page.type(this.selectors.passwordInput, password);
        await this.page.click(this.selectors.loginButton);
    }

    async isErrorVisible() {
        return await this.page.$eval('.error', el => el.style.display !== 'none');
    }
}

Ключевые моменты:

  • selectors инкапсулируют локаторы, чтобы их изменение не ломало тесты.
  • Методы navigate, login, isErrorVisible предоставляют высокий уровень взаимодействия, скрывая детали DOM.
  • Любые изменения на странице требуют правки только в одном месте — внутри класса.

Инкапсуляция логики ожиданий и навигации

Puppeteer требует явных ожиданий элементов перед взаимодействием. Инкапсуляция этих действий повышает надёжность тестов и снижает дублирование кода:

class BasePage {
    constructor(page) {
        this.page = page;
    }

    async click(selector) {
        await this.page.waitForSelector(selector, { visible: true });
        await this.page.click(selector);
    }

    async type(selector, text) {
        await this.page.waitForSelector(selector, { visible: true });
        await this.page.type(selector, text);
    }

    async getText(selector) {
        await this.page.waitForSelector(selector, { visible: true });
        return await this.page.$eval(selector, el => el.textContent.trim());
    }
}

Использование базового класса позволяет наследовать общий функционал во всех страницах:

class DashboardPage extends BasePage {
    constructor(page) {
        super(page);
        this.selectors = {
            welcomeMessage: '.welcome'
        };
    }

    async getWelcomeText() {
        return await this.getText(this.selectors.welcomeMessage);
    }
}

Преимущества:

  • Повторное использование методов click, type, getText для любых страниц.
  • Централизация ожиданий элементов уменьшает вероятность flaky-тестов.
  • Внесение изменений в логику взаимодействия требует модификации только в BasePage.

Абстракции для сложных сценариев

Некоторые сценарии требуют комбинации действий: заполнение форм, переходы между страницами, проверка состояний. В этом случае удобно создавать сервисные объекты или helper-классы:

class AuthService {
    constructor(loginPage, dashboardPage) {
        this.loginPage = loginPage;
        this.dashboardPage = dashboardPage;
    }

    async loginAndCheck(username, password) {
        await this.loginPage.navigate();
        await this.loginPage.login(username, password);
        return await this.dashboardPage.getWelcomeText();
    }
}

Такой подход отделяет сценарий от деталей реализации страниц. Тесты становятся короткими, читаемыми и понятными:

const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
const authService = new AuthService(loginPage, dashboardPage);

const welcome = await authService.loginAndCheck('user1', 'pass1');
console.assert(welcome.includes('Welcome'), 'Login failed');

Управление состоянием и инкапсуляция данных

В Puppeteer важно контролировать состояние страницы и браузера. Инкапсуляция позволяет безопасно хранить состояние между тестами, например, сессии или cookies:

class SessionManager {
    constructor(page) {
        this.page = page;
    }

    async saveCookies() {
        this.cookies = await this.page.cookies();
    }

    async loadCookies() {
        if (this.cookies) {
            await this.page.setCookie(...this.cookies);
        }
    }
}

Инкапсуляция работы с cookies обеспечивает повторное использование и предсказуемость тестов, без необходимости дублировать логику в каждом тесте.


Абстракции для сетевых запросов

Puppeteer поддерживает перехват и модификацию сетевых запросов через page.setRequestInterception. Создание абстракций для работы с API повышает гибкость:

class NetworkInterceptor {
    constructor(page) {
        this.page = page;
        this.requests = [];
    }

    async start() {
        await this.page.setRequestInterception(true);
        this.page.on('request', request => {
            this.requests.push(request.url());
            request.continue();
        });
    }

    getRequests() {
        return this.requests;
    }
}

Использование такого класса позволяет:

  • Легко проверять, что определённые запросы были сделаны.
  • Инкапсулировать логику перехвата, не загрязняя тестовые сценарии.
  • Расширять функционал (например, блокировка или модификация запросов) без изменения тестов.

Выводы по абстракции и инкапсуляции

Использование этих подходов в Puppeteer позволяет строить стабильные, читаемые и поддерживаемые тесты, где:

  • Абстракции скрывают детали низкоуровневого взаимодействия с DOM и браузером.
  • Инкапсуляция защищает внутренние состояния и данные, предоставляя чистый интерфейс.
  • Комбинация классов страниц, сервисов и утилит создаёт гибкую архитектуру тестового проекта, готовую к масштабированию и рефакторингу.

Эти принципы создают основу для любых сложных тестовых сценариев, включая интеграционные и e2e-тесты с Puppeteer, сохраняя баланс между деталями реализации и удобством использования.