Избежание flaky тестов

Flaky тесты — это тесты, которые непредсказуемо проходят или не проходят при каждом запуске, даже если код не изменялся. Основные причины flaky тестов:

  1. Нестабильное окружение: Проблемы с сетью, сервером или аппаратным обеспечением могут привести к непредсказуемым результатам тестов.
  2. Асинхронность: Веб-приложения часто зависят от асинхронных операций, таких как запросы к серверу, динамическая загрузка контента. Если тесты не учитывают задержки, они могут завершиться ошибкой.
  3. Проблемы синхронизации: Когда тесты выполняются быстрее, чем происходит загрузка элементов на странице или реакции от сервера.
  4. Конкурентный доступ: Когда несколько тестов выполняются одновременно, это может привести к состояниям гонки, если тесты не изолированы друг от друга.
  5. Недостаточная изоляция: Если тесты зависят от предыдущих тестов или состояния данных, это может привести к flaky тестам.
  6. Проблемы с локаторами: Часто используемые локаторы могут стать ненадежными из-за изменений на странице или динамического контента.

Техники предотвращения flaky тестов

1. Использование явных ожиданий (Explicit Waits)

Один из самых частых источников flaky тестов — это проблемы синхронизации, когда элементы на странице еще не загружены, а тест уже пытается с ними взаимодействовать. В WebDriverIO можно использовать явные ожидания для того, чтобы тест ждал появления элемента на странице.

Пример:

const button = await $('#submit-button');
await button.waitForExist({ timeout: 5000 }); // Ожидание до 5 секунд, пока элемент не появится
await button.click();

waitForExist гарантирует, что WebDriverIO не продолжит выполнение теста до тех пор, пока элемент не будет найден, что предотвращает возможные ошибки из-за раннего обращения к элементу.

2. Обработка асинхронных операций

Современные веб-приложения часто взаимодействуют с сервером, выполняя асинхронные запросы и обновляя DOM. Это может вызвать flaky тесты, если тест не учитывает завершение этих операций. Для таких случаев следует использовать методы, которые обеспечивают ожидание завершения запросов или анимаций.

Пример:

await browser.waitUntil(async () => {
    const value = await $('#status').getText();
    return value === 'loaded';
}, {
    timeout: 10000,  // Ожидание до 10 секунд
    timeoutMsg: 'Status did not load in time'
});

В этом примере waitUntil будет проверять значение на элементе до тех пор, пока оно не станет равным 'loaded', или пока не истечет тайм-аут.

3. Управление состоянием тестов

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

В WebDriverIO можно использовать хуки для подготовки и очистки тестового окружения перед и после выполнения каждого теста:

beforeEach(async () => {
    await browser.url('https://example.com');
    // Другие действия по подготовке окружения
});

afterEach(async () => {
    await browser.reloadSession();
    // Очистка данных после теста
});

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

4. Применение стабильных локаторов

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

Рекомендуется использовать более стабильные локаторы, такие как:

  • ID элементов: Это уникальные идентификаторы, которые не меняются.
  • CSS-классы: Можно использовать классы, которые не зависят от изменения контента.
  • Реляционные локаторы: Использование структурных и логических связей между элементами для стабильных локаторов.

Пример использования стабильного локатора:

const submitButton = await $('#submit-button'); // Уникальный ID
await submitButton.click();

5. Параллельное выполнение и очереди

Когда тесты запускаются параллельно, важно избегать конфликтов между ними. Для этого необходимо изолировать данные и состояние каждого теста. В WebDriverIO можно использовать parallel для параллельного выполнения тестов, а также гарантировать, что они не будут взаимодействовать друг с другом.

Пример:

exports.config = {
    runner: 'local',
    specs: ['./tests/**/*.js'],
    maxInstances: 5, // Параллельный запуск 5 тестов
    capabilities: [{
        browserName: 'chrome',
        'goog:chromeOptions': {
            args: ['--headless']
        }
    }],
    // Другие настройки конфигурации
};

В таком случае WebDriverIO автоматически управляет количеством параллельных инстансов, а изоляция данных тестов обеспечивается настройками конфигурации.

6. Использование перехватов и моков (Mocking)

Иногда flaky тесты возникают из-за зависимостей от сторонних сервисов или нестабильных API. Для предотвращения таких проблем можно использовать моки, чтобы заменять реальное взаимодействие с сервером на заранее подготовленные данные.

Пример использования моков:

browser.url('https://example.com');

// Мокаем запрос к API
browser.mockRequest('GET', '/api/data', (req) => {
    req.respond(200, { data: 'mockedData' });
});

// Выполняем тест, который теперь использует мокированные данные
await $('#getDataButton').click();
const result = await $('#result').getText();
expect(result).toBe('mockedData');

Моки позволяют контролировать состояние данных в тестах и предотвращают зависимость от внешних источников.

7. Проверка состояния страницы

Не менее важным является проверка состояния страницы перед выполнением действия. Это поможет избежать ситуаций, когда тест пытается взаимодействовать с элементом, который не готов к использованию.

Пример проверки состояния:

const isPageReady = await browser.execute(() => {
    return document.readyState === 'complete';
});
expect(isPageReady).toBe(true);

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

8. Использование устойчивых к изменениям интерфейсов

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

Пример:

const product = await $('[data-test="product-123"]');
await product.click();

Использование атрибутов data-* позволяет обеспечивать стабильность локаторов при изменениях в структуре страницы.

Заключение

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