Retry механизмы

Retry-механизмы в тестировании с Puppeteer необходимы для повышения надежности автоматизации. В веб-среде частые проблемы — это динамическая загрузка контента, задержки рендеринга элементов, временные ошибки сетевого взаимодействия и асинхронные события. Без корректного механизма повторных попыток тесты становятся нестабильными, что снижает доверие к результатам автоматизации.

В Puppeteer нет встроенной функции «retry», поэтому приходится реализовывать её вручную через JavaScript. Основная идея заключается в повторении действия до тех пор, пока оно не выполнится успешно или пока не будет исчерпан лимит попыток.

Реализация базового retry

Простейший способ реализовать retry — создать универсальную функцию-обертку для асинхронных операций:

async function retryOperation(operation, retries = 3, delay = 1000) {
    for (let attempt = 1; attempt <= retries; attempt++) {
        try {
            return await operation();
        } catch (error) {
            if (attempt === retries) {
                throw error;
            }
            console.log(`Попытка ${attempt} не удалась. Повтор через ${delay} мс...`);
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }
}

В этой функции:

  • operation — асинхронная функция, которую необходимо выполнить;
  • retries — максимальное количество попыток;
  • delay — время ожидания между попытками в миллисекундах.

Применение в Puppeteer для клика по элементу:

await retryOperation(async () => {
    const button = await page.waitForSelector(&
    await button.click();
}, 5, 500);

В примере используется waitForSelector с ограничением по времени. Если элемент не появляется, операция выбросит ошибку, которая будет обработана retry-функцией.

Retry при проверке условий

Помимо действий, retry часто используют для проверки условий на странице. Например, нужно дождаться появления текста или изменения состояния элемента:

async function waitForTextWithRetry(selector, expectedText, retries = 3, delay = 1000) {
    return await retryOperation(async () => {
        const element = await page.waitForSelector(selector, { timeout: 2000 });
        const text = await page.evaluate(el => el.textContent, element);
        if (text.includes(expectedText)) {
            return true;
        } else {
            throw new Error(`Текст не совпадает. Ожидалось: ${expectedText}, найдено: ${text}`);
        }
    }, retries, delay);
}

Адаптивные задержки и экспоненциальный backoff

Для уменьшения нагрузки на страницу и сетевой трафик рекомендуется использовать адаптивные задержки, например экспоненциальный backoff:

async function retryWithBackoff(operation, retries = 5, initialDelay = 500) {
    let delay = initialDelay;
    for (let attempt = 1; attempt <= retries; attempt++) {
        try {
            return await operation();
        } catch (error) {
            if (attempt === retries) {
                throw error;
            }
            console.log(`Попытка ${attempt} не удалась. Повтор через ${delay} мс...`);
            await new Promise(resolve => setTimeout(resolve, delay));
            delay *= 2; // экспоненциальный рост задержки
        }
    }
}

Такой подход полезен при нестабильных сетевых запросах и долгих асинхронных загрузках.

Retry для навигации и сетевых операций

Часто проблемы возникают при переходе по страницам или ожидании загрузки ресурсов. Использование retry позволяет минимизировать ошибки из-за временных сбоев:

await retryOperation(async () => {
    await page.goto('https://example.com', { waitUntil: 'networkidle2', timeout: 5000 });
}, 3, 1000);

Для сетевых запросов внутри тестов можно оборачивать page.waitForResponse с retry, чтобы гарантировать получение корректного ответа.

Интеграция с тестовыми фреймворками

В Jest или Mocha retry можно объединять с тестовыми хуками. Например, в Mocha:

it('должен дождаться кнопки и кликнуть', async function() {
    this.retries(3); // встроенный механизм Mocha
    await page.goto('https://example.com');
    await retryOperation(async () => {
        const button = await page.waitForSelector('#submit', { timeout: 2000 });
        await button.click();
    }, 3, 500);
});

Использование встроенных retry-хуков позволяет сочетать возможности Puppeteer с фреймворком тестирования, обеспечивая устойчивость тестов.

Логирование и диагностика

Для сложных проектов важно логировать каждую попытку. Это позволяет:

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

Пример расширенного логирования:

async function retryOperationWithLogging(operation, retries = 3, delay = 1000) {
    for (let attempt = 1; attempt <= retries; attempt++) {
        try {
            console.log(`Попытка ${attempt} запуска операции...`);
            const result = await operation();
            console.log(`Попытка ${attempt} успешна.`);
            return result;
        } catch (error) {
            console.error(`Ошибка на попытке ${attempt}: ${error.message}`);
            if (attempt === retries) {
                console.error('Все попытки исчерпаны.');
                throw error;
            }
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }
}

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

Заключение

Retry-механизмы в Puppeteer — это критически важный инструмент для стабильной автоматизации. Они позволяют корректно обрабатывать асинхронные события, задержки в DOM, нестабильные элементы и сетевые ошибки. Использование базовых оберток, адаптивных задержек, экспоненциального backoff и интеграция с тестовыми фреймворками делает тесты устойчивыми и предсказуемыми.