Как известно, основной проблемой в тестировании является отчетность по прогонам. Некоторые компании собирают данные в отдельном хранилище. Вместо того, чтобы вручную организовывать хранение, было решено сохранять их в Test IT. Такие данные как: исход, время выполнения и количество автоматизированных кейсов позволяют разделить тесты на выборки и дать оценку покрытия автотестами.


Это практично, поскольку в Test IT всё сделали до нас и не придется заново изобретать велосипед. Объединив Test IT и pytest мы сможем выборочно запускать авто тесты. В итоге должна получиться мастер система, из которой будут запускаться тестовые выборки и собираться статистика.


Несмотря на то, что существует множество СУТ (Allure TestOps, TestLink и т.п.), решили остановится на Test IT, поскольку это быстро растущий проект по доступной цене с поддержкой на русском языке, а лицензия на TestRail уже закончилась.


Техническое задание


В рамках задачи следует:


  • Настроить проект в Test IT
  • Настроить прохождение pytest-тестов в GitLab CI
  • Написать плагин для pytest который:
    • создает автоматизированный кейс в Test IT
    • связывает автоматизированный и ручной кейс
    • запускает только те автоматизированные кейсы, которые были выбраны в прогоне
    • отмечает результаты в режиме реального времени

Замечания



Реализация


1. Создание и связывание автоматизированных кейсов в Test IT


В Test IT существует два вида кейсов: ручной и автоматизированный



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



В таком случае напротив ручного кейса появится символ ракеты



Создание и связывание автоматизированного кейса происходит через endpoint AutoTests, в который следует передать два параметра:


  • testit_case_title — название автоматизированного кейса;
  • testit_case_id — идентификатор ручного кейса для привязки.

@pytest.mark.testit_case_id(29035)
@putest.mark.testit_case_title("Проверка первой успешной загрузки файла")
def test_download_first(test_it_configuration):
    """
        Автотест прилинкованный к ручному тесту
    """
    from pprint import pprint
    pprint(test_it_configuration)
    sleep(randint(a=0, b=8))

Для запуска GitLab CI агента через Test IT следует создать WebHook со следующими параметрами:



Параметры для GitLab CI:


  • URL — ссылка на trigger от GIT-проекта;
  • ref — ветка, в которой запускаются тесты;
  • token — токин от GitLab CI триггера.

Параметры для запуска PYTEST:


  • TEST_IT — флаг сообщающий, что тесты запущены из Test IT;
  • TESTIT_URL — ссылка на Test IT;
  • API_KEY — ключ для Test IT Swagger API;
  • PROJECT_ID — идентификатор проекта в котором вы работаете;
  • TEST_IT_MODE — режим работы с Test IT.

TEST_IT_MODE принимает следующие значения:


  • DELETE_AUTOTESTS
  • CREAT_AUTOTESTS
  • LINK_AUTOTESTS
  • RUN

Эти параметры будут переданы в GitLab CI и сохранены в переменных окружения агента



Тесты получают значения из переменных окружения агента при помощи модуля getenv


from os import getenv

class TestItParams:
    """
        Параметры из Test IT
    """
    is_test_it = getenv('TEST_IT', 'False').lower().capitalize() == 'True'
    mode = getenv('TEST_IT_MODE', 'RUN')
    url = getenv('TESTIT_URL', 'http://tmslt-1.video.rt.ru/')
    key = getenv('API_KEY', None)
    project_id = getenv('PROJECT_ID', None)
    run_id = getenv('TEST_RUN_ID', None)
    configuration_id = getenv('CONFIG_ID', None)

Класс TestItParams следует сохранить в объекте session


def pytest_sessionstart(session):
    """
        SetUP тестовый сессии: собрать env-параметры из TestIT
    """

    session.test_it = TestItParams

Для создания автоматизированного кейса во время запуска pytest, нам нужно достать два параметра testit_case_title и testit_case_id.


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


Для этого напишем plugin в файле conftest.py


def pytest_collection_modifyitems(session, items):
    """
        Перехват состояния перед публикацией списка кейсов
    """

    if session.test_it.is_test_it is True:
        print('Тесты запущены для TEST_IT')

        if 'DELETE_AUTOTESTS' in session.test_it.mode:
            delete_all_auto_test_from_project(pytest_session=session)
        if 'CREAT_AUTOTESTS' in session.test_it.mode:
            create_autotest_in_project(pytest_session=session, pytest_items=items)
        if 'LINK_AUTOTESTS' in session.test_it.mode:
            link_autotests_to_testcases(pytest_session=session, pytest_items=items)
        if 'RUN' in session.test_it.mode:
            select_autotest_from_testrun_only(pytest_session=session)
    else:
        print('\nПроходит штатный запуск')

Функции create_autotest_in_project и link_autotests_to_testcases это обычные API запросы через endpoint AutoTests.


Параметр test_it_mode используется для разделения задач: создание, привязка, удаление или запуск.


2. Выборочный запуск pytest-тестов


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


Поскольку pytest ничего не знает о тестовом плане, то он должен запрашивать его самостоятельно.


Для этого по значению параметра TEST_RUN_ID через endpoint TestRunes мы получаем список тестов в текущем прогоне.



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


def pytest_collection_modifyitems(session, items):
    """
        Перехват состояния перед публикацией списка кейсов: создание, удаление, линковка, выборка
    """

    if session.test_it.is_test_it is True:
        print('Тесты запущены для TEST_IT')

        if 'DELETE_AUTOTESTS' in session.test_it.mode:
            delete_all_auto_test_from_project(pytest_session=session)
        if 'CREAT_AUTOTESTS' in session.test_it.mode:
            create_autotest_in_project(pytest_session=session, pytest_items=items)
        if 'LINK_AUTOTESTS' in session.test_it.mode:
            link_autotests_to_testcases(pytest_session=session, pytest_items=items)
        if 'RUN' in session.test_it.mode:
            select_autotest_from_testrun_only(pytest_session=session)
    else:
        print('\nПроходит штатный запуск')

Фильтрация происходит в объекте session.items в момент составления списка на запуск.


def select_autotest_from_testrun_only(pytest_session):
    """
        Выбрать для запуска только те кейсы, которые содержатся в тестовом прогоне
    """

    run_api = TestRunes(pytest_session.test_it.url, pytest_session.test_it.key)
    current_test_run = run_api.get_testrun_by_id(pytest_session.test_it.run_id)

    external_ids = []
    for case in current_test_run['testResults']:
        if case['startedOn'] is None:
            external_ids.append((case['autoTest']['externalId'],
                                 case['configuration']))

    edited_session_items = []
    for case in pytest_session.items:
        for external_id in external_ids:
            if case.name in external_id[0]:
                case.test_it_configuration = external_id[1]
                edited_session_items.append(case)
    pytest_session.items = edited_session_items

3. Возврат результата прохождения pytest-тестов


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


Для возвращения статуса в реальном времени нам следует воспользоваться хуком pytest_runtest_makereport с декоратором @pytest.hookimpl(hookwrapper=True).


Он позволяет перехватывать выполнение ОДНОГО теста ДО и ПОСЛЕ прохождения.


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item):
    """
        Перехватывает состояние каждого теста во время: setup и done
    """

    outcome = yield

    if 'RUN' in item.session.test_it.mode:
        provide_test_results_into_run(pytest_session=item.session,
                                      pytest_outcome=outcome,
                                      pytest_item=item)

Этот хук возвращает объект outcome, в котором содержится информация о прогоне текущего теста.


Передача результатов прогона в Test IT происходит через endpoint TestRunes.


def provide_test_results_into_run(pytest_session, pytest_outcome, pytest_item):
    """
        Предоставить результат выполнения теста в TestIT
    """
    test_run_api = TestRunes(pytest_session.test_it.url, pytest_session.test_it.key)
    res = pytest_outcome.get_result()
    if res.when == "call":
        autotest_id = pytest_item.name
        outcome = res.outcome
        duration = res.duration
        message = ''
        traceback = ''

        if res.outcome == 'failed':
            message = res.longrepr.reprcrash.message
            traceback = res.longreprtext

        test_run_api.set_auto_test_results_for_test_run(run_id=pytest_session.test_it.run_id,
                                                        autoTestExternalId=autotest_id,
                                                        outcome=humanize(outcome),
                                                        message=message,
                                                        traces=traceback,
                                                        duration=duration,
                                                        configurationId=pytest_item.test_it_configuration['id'])

Вывод


Объединив Test IT и pytest мы собрали мастер систему которая умеет:


  • запускать автотесты в GitLab CI;
  • получать результат в режиме реального времени;
  • собирать статистику по запускам;
  • запускать выборки тестов.

Test IT имеет красивый интерфейс, предоставляющий кольцевые-диаграммы, различные таблицы и графики. Можно узнать, в каком прогоне упал конкретный тест и сколько времени он выполнялся.


Демонстрация



Ссылки


Комментарии (4)


  1. hatman
    31.10.2022 12:05

    Импортзамещение в действии? Я так полагаю, ТестИт - это аналог тестрейла?


    1. CloseGB Автор
      31.10.2022 12:08

      Да, это отечественная система управления тестированием


  1. GnuriaN
    31.10.2022 14:26

    @CloseGB
    is_test_it = eval(getenv('TEST_IT', 'False'))

    Вот прям вот так вот?


    1. CloseGB Автор
      31.10.2022 14:53
      +1

      Действительно, так делать небезопасно.
      В ближайшее время исправлю.
      Спасибо!