Настройка GitHub Actions для автоматизированного тестирования средствами Python в конвейере CI/CD
В преддверии старта курса «Python QA Engineer» традиционно публикуем перевод полезного материала.
Также приглашаем присоединиться к открытому вебинару на тему «Автоматизация тестирования API».
В этой статье описываются операции по тестированию клиентской части приложения с помощью TestProject и pytest, а также способы выполнения тестов через GitHub Actions. Если у вас общедоступный репозиторий GitHub, все это будет совершенно бесплатно. Эта возможность хорошо подходит для изучения TestProject и выполнения интеграционного тестирования в ваших проектах. Если вы хотите выполнять эти операции из закрытого репозитория, то GitHub предлагает очень большое количество бесплатных минут, см. https://github.com/features/actions#pricing-details.
Карточки репозиториев GitHub
Сперва я решил изучить возможности, доступные в репозиториях GitHub, поскольку они показались мне довольно простыми для понимания, а к тому же я давно не пользовался Selenium.
Я закрепил свои любимые репозитории в верхней части своего сайта waylonwalker.com. Информация для заполнения этих карточек динамически подтягивается на сторону клиента через API-интерфейс GitHub. Это означает, что по мере загрузки страниц JavaScript выполняет скрипты для получения информации от API-интерфейса GitHub, а затем преобразует эти данные в DOM и визуализирует их на странице. Вот как выглядят карточки репозиториев GitHub:
Получение ключей
Прежде всего, вам понадобятся TPDEVTOKEN и TPAPIKEY. Эти ключи позволят TestProject получить доступ к вашей учетной записи, чтобы автоматически размещать результаты на вашей панели отчетности.
В своем репозитории GitHub перейдите в раздел settings >Secrets (или добавьте settings/secrets к URL-адресу своего репозитория) и добавьте секретные токены. GitHub получит безопасный доступ к токенам, при этом они не будут доступны широкой публике (включая участников проекта со статусом contributor), не будут отображаться в файлах журналов и т. д.
Настройка среды разработки
Чтобы ускорить работу, я настроил среду разработки в Digital Ocean. Делать это необязательно: все может работать и с вашего локального компьютера или полностью из GitHub Actions. Просто я решил, что, настроив Droplet с Ubuntu в Digital Ocean, я получу условия, близкие к продакшн-среде, а значит, смогу быстрее разработать свои тесты. Это также позволило мне немного ускорить работу всех моих тестов по сравнению с их выполнением через GitHub. При этом процесс был практически таким же, как при использовании GitHub. Таким образом, я смог детально изучить принципы настройки TestProject без необходимости выполнять полную установку при каждом запуске GitHub Actions.
Я не буду здесь подробно описывать настройку машины для разработки. Мои заметки по ее настройке можно прочитать здесь: https://waylonwalker.com/notes/new-machine-tpio.
Pytest
Все тесты, выполняющиеся с помощью Pytest, приведены на github.
Я решил воспользоваться Pytest. Мне понравилась идея применять фикстуры, автоматически выполнять мои тестовые функции и использовать кое-какие возможности Pytest для формирования отчетов по ходу разработки (при этом платформа TestProject будет работать и без фреймворка для тестирования вроде Pytest).
ПРИМЕЧАНИЕ. Следуя стандартным рекомендациям по работе с Pytest, я назвал каталог с тестами именем tests. В целом все работает, но платформа TestProject.io использует этот каталог в качестве имени проекта по умолчанию. Если бы можно было вернуться назад, я либо переименовал бы каталог, задав имя, которое я хочу видеть на TestProject.io, либо задал бы имя проекта в конфигурации.
conftest.py
В файле conftest.py обычно размещаются фикстуры, которые используются несколькими модулями. Pytest автоматически импортирует все модули conftest.py из каталога, в котором вы работаете. Это отличное место для размещения фикстур с драйверами TestProject. Обратите внимание: когда вы используете фикстуру в качестве аргумента в другой функции, фикстура выполнит настройку, передаст все из оператора yield в тестовую функцию, выполнит тестовую функцию, а затем освободит ресурсы.
conftest.py хранит фикстуры для всех модулей, находящихся в каталоге.
# tests/conftest.py import time import pytest from src.TestProject.sdk.drivers import web driver @pytest.fixture def driver(): "creates a webdriver and loads the homepage" driver = webdriver.Chrome() driver.get("https://waylonwalker.com/") yield driver driver.quit()
Приведенный выше пример является немного упрощенным. В реальной версии я столкнулся с некоторыми несоответствиями и обнаружил, что процент прохождения некоторых тестов был выше при добавлении оператора time.sleep. В полном проекте я остановился на фикстурах driver и slowdriver. Таким образом, у меня еще есть драйвер, который ждет выполнения JavaScript немного дольше.
testrepos.py
Изначально я настроил три разных теста для карточек репозиториев. Я сформировал список репозиториев, которые должны были отображаться в карточках. Эти тесты довольно легко выполнить с помощью платформы TestProject.io, поскольку она использует Selenium и безголовый браузер для выполнения JavaScript. Область REPOS создана здесь как глобальный список. Ее можно легко переделать в файл конфигурации, если возникнет такая необходимость.
Для тех, кто не знает, скажу, что безголовый (headless) браузер работает как обычный браузер, только без графического интерфейса пользователя. JavaScript полностью загружается и парсится, а все взаимодействие с DOM осуществляется программно.
Читайте строки документации к каждой функции. В них описано то, что происходит на каждом шаге.
""" Test that GitHub repo data dynamically loads the client-side. """ REPOS = [ "find-kedro", "kedro-static-viz", "kedro-action", "steel-toes", ] def test_repos_loaded(slow_driver): """ Test that each repo-name exists as a title in one of the repo cards. On waylonwalker.com repo cards have a title with a class of "repo-name" """ repos = slow_driver.find_elements_by_class_name("repo-name") # get innertext from elements header_text = [ header.text for header in repos ] for repo in REPOS: assert repo in header_text def test_repo_description_loaded(slow_driver): """ Test that each repo has a description longer than 10 characters On waylonwalker.com repo cards have a descriptiion with a class of "repo-description" """ repo_elements = slow_driver.find_elements_by_class_name("repo") for el in repo_elements: desc = el.find_element_by_class_name("repo-description") assert len(desc.text) > 10 def test_repo_stars_loaded(slow_driver): """ Ensure that stars are correctly parsed from the API and loaded client-side On waylonwalker.com repo cards have a stars element with a class of "repo-stars" and is displayed as "n stars" """ repo_elements = slow_driver.find_elements_by_class_name("repo") for el in repo_elements: stars = el.find_element_by_class_name("repo-stars") num_stars, label = stars.text.split() assert int(num_stars) > 0 assert label == 'stars'
Форум
Я немного запутался с настройкой TestProject.io в Actions. На форуме TestProject я быстро нашел ответ со ссылкой именно на тот пример, который был мне нужен. Пример был написан на Java, но в нем были нужные мне шаги настройки docker-compose.
GitHub Actions
Итак, репозиторий GitHub настроен, а мои тесты успешно работают в Pytest. Теперь давайте сделаем так, чтобы они автоматически выполнялись в GitHub Actions.
Сервис Actions — это решение GitHub для CI/CD. Он позволяет выполнять код на виртуальной машине, управляемой GitHub, которая может получать дополнительную информацию из вашего репозитория, например секретные токены, которые мы настроили в самом начале. Что, как и когда выполняется — все это настраивается в файле yaml .
# .github/workflows/test-waylonwalker-com.yml name: Test WaylonWalker.com # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the master branch on: push: branches: [ main ] pull_request: branches: [ main ] schedule: - cron: '*/10 * * * *'
Как видно в приведенном выше куске кода, я задал выполнение действия при отправке изменений в ветку main (команда push) или при создании pull-запроса, затрагивающего эту ветку. Я также задал достаточно агрессивный график выполнения тестов — каждые 10 минут. Это было необходимо, чтобы убедиться в эффективности тестов и получить больше данных для анализа в отчетах. Позднее я, скорее всего, увеличу этот интервал.
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@main - uses: actions/setup-python@v2 with: python-version: '3.8' - run: pip install -r requirements.txt - name: Run TestProject Agent env: TP_API_KEY: $> # < Let Secrets handle your keys run: | envsubst < .github/ci/docker-compose.yml >docker-compose.yml cat docker-compose.yml docker-compose -f docker-compose.yml up -d - name: Wait for Agent to Register run: bash .github/ci/wait_for_agent.sh - run: pytest env: TP_DEV_TOKEN: $> # < Let Secrets handle your tokens TP_AGENT_URL: http://localhost:8585
В коде задания test видно, что я решил выполнять его на ubuntu-latest. Первые три шага — это достаточно шаблонные действия: переключение на ветку репозитория, установка Python 3.8 и установка зависимостей из файла requirements.txt через pip. Затем ключ TPAPIKEY подставляется в docker-compose.yml с помощью envsubst, далее запускается docker-compose и ожидает готовности агента. Я также предоставил pytest наш токен TPDEVTOKEN и запустил pytest.
docker-compose.yml
Следующий файл docker-compose.yml был предоставлен Виталием Буховским (одним из сооснователей и директором по безопасности TestProject) в репозитории testproject-io/java-sdk. Здесь настраивается шаблон с ключом TPAPIKEY в качестве переменной для envsubst , безголовыми браузерами Chrome и Firefox и агентом TestProject.io.
version: "3.1" services: testproject-agent: image: testproject/agent:latest container_name: testproject-agent depends_on: - chrome - firefox environment: TP_API_KEY: "$" TP_AGENT_TEMP: "true" TP_SDK_PORT: "8686" CHROME: "chrome:4444" CHROME_EXT: "localhost:5555" FIREFOX: "firefox:4444" FIREFOX_EXT: "localhost:6666" ports: - "8585:8585" - "8686:8686" chrome: image: selenium/standalone-chrome volumes: - /dev/shm:/dev/shm ports: - "5555:4444" firefox: image: selenium/standalone-firefox volumes: - /dev/shm:/dev/shm ports: - "6666:4444"
Ожидание регистрации агента
На мой взгляд, самая интересная часть приведенного выше потока операций — это то, как мы ожидаем регистрации агента. Скрипт оболочки довольно лаконичный. Он обнаруживает превышение допустимого количества попыток запуска ( max_attempts ) или наличие запущенного агента путем проверки статуса через REST API (адрес: /api/status). Это позволяет нам избежать потери времени, вызванной заданием слишком большого времени ожидания, или слишком раннего начала процесса, при котором pytest запускается, когда агент еще не работает.
trap 'kill $(jobs -p)' EXIT attempt_counter=0 max_attempts=100 mkdir -p build/reports/agent docker-compose -f docker-compose.yml logs -f | tee build/reports/agent/log.txt& until curl -s http://localhost:8585/api/status | jq '.registered' | grep true; do if [ $ -eq $ ]; then echo "Agent failed to register. Terminating. " exit 1 fi attempt_counter=$(($attempt_counter+1)) echo sleep 1 done
Панель отчетности TestProject
После запуска тестов они отображаются на панели TestProject. На ранней стадии разработки тестов возникло несколько сбоев, но после устранения неполадок тесты прогоняются стабильно.
Панель: прохождение одного теста
Панель отчетности позволяет находить отдельные тесты, которые были выполнены, выбирать их и просматривать отчеты по каждому тесту. Она автоматически преобразует шаги, выполненные драйвером, в понятную человеку схему процесса, при этом каждый шаг можно открыть и посмотреть значения, которые были получены драйвером от сайта.
Подробнее о панелях отчетности TestProject можно прочитать по следующим ссылкам:
Удачного тестирования!