Тестирование

Инициализация приложения в тестах playwright

При написании тестов на playwright очень важно, чтобы они выполнялись стабильно, особенно при использовании CI. Если тесты будут случайно сваливаться не из-за ошибок в тестируемом приложении, то смысл CI теряется. Наоборот, это приводит к усложнению разработки.

Когда мы пишем frontend-приложение, часто используется devserver — код компилируется из ES6 или TypeScript непосредственно в момент открытия страницы. Это очень удобно для разработки, но если это применяется для тестирования, очень важно, что компиляция успела отработать до запуска тестов. Если тесты начнут выполняться в тот момент, когда компиляция ещё идёт, это приведёт к тому, что тесты свалятся не из-за ошибки в коде.

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

Тестируем готовый образ

Самый надёжный и универсальный вариант, это не использовать DevServer для тестов. Вместо этого собрать готовый образ для тестов и использовать его. Образ будет представлять собой готовый пакет кода на JS, либо образ Docker. В этом случае никаких проблем с компиляцией в процессе тестирования быть не может.

Этот подход работает в большинстве случаев. Но есть моменты, когда он не работает.

Например, если вы пишете приложение на Vue.JS, и используете Vue-локаторы Playwright, они будут работать только в версии для DevServer.

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

В конфиге Playwright можно указать директиву webServer:

webServer: {
  command: 'npm run start',
  url: 'http://127.0.0.1:3000',
  reuseExistingServer: !process.env.CI
}

В этом случае перед запуском тестов в режиме CI playwright запустит ваш сервер при помощи команды npm run start, затем дождётся, пока порт 3000 будет открыт, а только после этого запустит тесты. Использование этой директивы решает проблему с долгим ожиданием запуска сервера, потому что порт будет открыт только после того, как сервер запустится.

Увеличиваем число попыток запуска тестов

Другим путём решения этой проблемы является использованием директивы retries. Например, если установить retries=5, то в случае свалившегося теста playwright будет пытаться запустить его ещё 5 раз, и будет считать свалившимся только если не пройдёт ни одна из попыток (если тест прошёлся не с первой попытки, он будет помечен как flaky — ненадёжный тест, но запуск тестов всё равно будет считаться успешным).

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

Для решения этой проблемы хорошо бы запускать первый тест с retries=10, а если уже он прошёлся, то остальные запускать с retries=1. И playwright такую возможность предоставляет!

Используем проекты playwright

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

1. Написать простейший тест, который проверяет только общую работоспособность проекта (smoke-тест), поместить его в отдельный файл, например smoke.spec.ts.

2. Создать отдельный проект для smoke-тестов. В нём указать rerties: 10, а также указать testMatch: 'smoke.spec.ts'.

3. Для остальных тестов тоже создать отдельный проект (например назовём его regular). В нём указать retries: 0, а также testIgnore: 'smoke.spec.ts', чтобы избежать повторного запуска тестов.

4. Для того, чтобы тесты из проекта regular запускались только после того, как будут успешно выполнены тесты smoke, в проекте regular указываем dependencies: ['smoke'].

В итоге в конфиге playwright должна появиться директива на подобие такой:

  projects: [
    {
      name: 'smoke',
      testMatch: 'smoke.spec.ts',
      use: {
        ...devices['Desktop Chrome']
      },
      retries: 10
    },
    {
      name: 'regular',
      testIgnore: 'smoke.spec.ts',
      use: {
        ...devices['Desktop Chrome']
      },
      retries: 0,
      dependencies: ['smoke']
    }
  ]

Используем docker compose

Другим вариантом является использование docker compose — в одном контейнере запускается приложение, в другом — тесты. При этом есть возможность очень гибко настроить ожидание инициализации приложения через healthcheck. В этом случае в описании контейнера для запуска тестов добавляется секция healthcheck, в ней указывается параметр test, в котором приводится команда для проверки того, что приложение инициализировано успешно. Например, можно использовать wget, который проверит, что на указанном порту висит http-сервер и отдаёт корректный код ответа:

        healthcheck:
            test: "wget -w 1 --tries=1 http://localhost:3000 -O /dev/null"
            interval: 2s
            timeout: 2s
            retries: 100
            start_period: 10s

При этом весь файл docker-compose может выглядеть следующим образом:

version: '3.3'
name: myproject
services:
    app:
        image: node:20
        volumes:
            - ./app:/usr/src/app
        working_dir: /usr/src/app
        command: bash -c "npm install && npm run dev"
        healthcheck:
            test: "wget -w 1 --tries=1 http://localhost:3000 -O /dev/null"
            interval: 2s
            timeout: 2s
            retries: 100
            start_period: 10s
    tests-runner:
        image: mcr.microsoft.com/playwright:v1.36.1-focal
        volumes:
            - ./app:/usr/src/app
        working_dir: /usr/src/app
        command: bash -c "npx playwright test"

        depends_on:
            app:
                condition: service_healthy

Для того, чтобы запустить такой проект в среде CI (например github-actions), нужно, чтобы при завершении выполнения тестов среда узнала, выполнены ли тесты успешно или нет. Для этого можно использовать ключ exit-code-from и указать контейнер с тестами:

docker compose up --exit-code-from=tests-runner