Настройка 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 на GitHub.
В файле 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
Полная версия testrepos.py есть на GitHub.
Изначально я настроил три разных теста для карточек репозиториев. Я сформировал список репозиториев, которые должны были отображаться в карточках. Эти тесты довольно легко выполнить с помощью платформы 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: ${{ secrets.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: ${{ secrets.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_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 [ ${attempt_counter} -eq ${max_attempts} ]; then
echo "Agent failed to register. Terminating..."
exit 1
fi
attempt_counter=$(($attempt_counter+1))
echo
sleep 1
done
Панель отчетности TestProject
После запуска тестов они отображаются на панели TestProject. На ранней стадии разработки тестов возникло несколько сбоев, но после устранения неполадок тесты прогоняются стабильно.
Панель: прохождение одного теста
Панель отчетности позволяет находить отдельные тесты, которые были выполнены, выбирать их и просматривать отчеты по каждому тесту. Она автоматически преобразует шаги, выполненные драйвером, в понятную человеку схему процесса, при этом каждый шаг можно открыть и посмотреть значения, которые были получены драйвером от сайта.
Подробнее о панелях отчетности TestProject можно прочитать по следующим ссылкам:
Это пособие создал Уэйлон Уокер (Waylon Walker) на основе своей статьи «Интеграционное тестирование с помощью Python, TestProject.io и GitHub Actions».
Удачного тестирования!
- Узнать подробнее о курсе "Python QA Engineer".
- Записаться на открытый вебинар "Автоматизация тестирования API".