Skip to content

Некорректная привязка автотестов к тесткейсам #167

@iromanchenko-cyrm

Description

@iromanchenko-cyrm

Описываемый в данном баг репорте недостаток системы и вытекающие из него эксплуатационные сложности будут продемонстрированы на примере использования адаптера к пайтест https://github.com/testit-tms/adapters-python/tree/main/testit-adapter-pytest. При этом следует учитывать, что проблема затрагивает как бекэнд, так и все имеющиеся адаптеры.

В общем случае связь автотестов с тесткейсами (далее ТК) соответствует соотношению многие к многим. Соотвественно для реализации такой схемы используются два признака, уникальный для ТК workitemId и уникальный для автотеста externalId. У одного автотеста может быть указан список из произвольного количества workitemId, у ТК - список из externalId. При создании нового автотеста, покрывающего проверки уже имеющегося в системе ТК с известным значением workitemId, необходимо руками навесить этот признак на автотест для определения желаемой связи с ТК, то есть никакой подкапотной машинерии в данном случае адаптером не предусмотрено. Далее чтобы позволить системе отличать один автотест от других, одновременно прилинкованных к конкретному ТК, необходимо чтобы на каждом автотесте присутствовал признак externalId с уникальным значением Аналогично workitemId имеется возможно навесить произвольный externalId на автотест руками, а так же в адаптере имеется машинерия автоматически расставляющая externalId на те автотесты, где данный признак не установлен вручную. Данная функция реализована следующим образом:

https://github.com/testit-tms/adapters-python/blob/main/testit-adapter-pytest/src/testit_adapter_pytest/listener.py#L135

    for item in items:
            if hasattr(item.function, 'test_external_id'):
                item.test_external_id = item.function.test_external_id
            else:
                item.test_external_id = utils.get_hash(item.parent.nodeid + item.function.__name__)

Из данного кода видно, что для системы ТестИТ финальной атомарной сущностью автотеста является тестовая функция. Я считаю, что это неверно, потому как тестовая функция - это всего лишь синтаксическая конструкция языка. Пайтест никак не ограничивает тестописателя соотношением 1 тестовая функция <-> 1 автотест. Более того, пайтест мотивирует создавать параметризованные автотесты с помощью встроенного и чрезвычайно популярного декоратора @pytest.mark.parametrize https://docs.pytest.org/en/stable/how-to/parametrize.html. То есть совершенно нормально, обыденно и даже желательно описывать близкую по смыслу группу автотестов одной тестовой функцией.

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

class TestDebug:
    @workItemIds("228185")
    @pytest.mark.parametrize(
        "param1, param2, display_name",
        [
            (
                "value1_1",
                "value2_1",
                "Отображаемое имя 1",
            ),
            (
                "value1_2",
                "value2_2",
                "Отображаемое имя 2",
            ),
        ],
    )
    @displayName("{display_name}")
    def test_debug(
            self,
            param1,
            param2,
            display_name
    ):
        with step("тестовый шаг 1"):
            pass

С точки зрения пайтеста, это 2 отдельных автотеста, которые при запуске будут выполнены порознь. На этапе сбора {коллектинга} айтемов (автотестов в терминологии пайтеста) в список items будут помещены два дубликата тестовой функции test_debug c различными значениями аргументов, согласно наборам тестовых данных из агрументов @pytest.mark.parametrize.
При этом обоим айтемам адаптер ТестИТ навесит один и тот же test_external_id, в следствии чего мы можем наблюдать следующие побочные эффекты:

  1. В прогоне у всех автотестов будет один и тот же display_name, в частности соотвествующий последнему выполненному автотесту, то есть в данном примере "Отображаемое имя 2".

Image
(Влияет на читабельность репорта, уже были жалобы от ручников)

  1. На странице "Автотесты" (/autotests) будет неправильное количество автотестов (1 вместо 2)

Image
(Влияет на всю статистику самого тест айти и запусков)

Мы попробовали исправить ситуацию на своей стороне. Навесим на каждый айтем отдельный test_external_id в обход адаптера. Для этого в конфтесте определим хук-функцию:

@hookimpl(tryfirst=True)
def pytest_collection_modifyitems(session, config, items):

    for item in items:
        __hash = get_sha256(item.nodeid)
        # parameterized tests
        if hasattr(item, "callspec"):
            if not hasattr(item.function, "test_external_id"):
                item.function.test_external_id = "{testit_external_id}"
            item.callspec.params["testit_external_id"] = __hash

        # non-parameterized tests
        else:
            if not hasattr(item.function, "test_external_id"):
                item.function.test_external_id = __hash
        logger.debug('externalId applied for node "%s": "%s".', item.nodeid, __hash)
  • пояснение, данный хук вместо хеша от item.parent.nodeid + item.function.__name__, как делает под капотом адаптер, навестит на айтемы хеш от item.nodeid, в который входит постфиксом набор уникальных параметров, указанных в декораторе параметрайз.

Удаляем автотест привязанный к ТК на прошлом запуске и запускаем автотесты с проливкой репорта заново:

Image

Получаем в результате пофишеными обе проблемы, приведенные выше.

  1. В прогоне у автотестов правильные отображаемые имена

Image

  1. На станице автотестов имеем в списке 2 автотеста и соотвественно правильное количество в счетчике

Image

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

Рассмотрим такой момент времени в процессе тестирования, когда тесткейсы для проекта уже написаны, а часть ТК даже автоматизирована и тест-менеджер приступает к планирования предстоящего регресса с помощью ТМС ТестИТ. На этом этапе он создает новый тесплан в разделе "Тест-планы" (/test-plans).

Image

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

Image

На этом месте следует сделать оговорку, что для правильной работы данного функционала, как задумано проектировщиками ТестИТ, мы предварительно внесли в ТК информацию о его наборах параметров. (Кстати, чтобы избавиться от ручной рутины, можно заполнять тесткейсы в ТМС параметрами из автотестов с помощью плагина https://pypi.org/project/pytest-testit-parametrize/)

Image

Благодаря таблице параметров в ТК с двумя наборами тестовых данных, после добавления одного ТК (с номером 228185) в набор, там оказалось 2 теста.

Image

Оба теста автоматизированные (помеченные иконкой ракеты), поэтому тест-менеджер решает во вкладке "Выполнение" не подключать для их прогона ручных тестировщиков, а выполнить соотвествующие им автотесты, для чего выделяет оба теста и нажимает кнопку "Запустить автотесты".

Image

После запуска ТестИТ создал прогон

Image

Перейдя в прогон, мы можем наблюдать, что произошло на беке:

Image
Их стало 4. Ожидаемое количество автотестов размножилось.
Откроим каждый из автотестов, чтобы сверить значения параметров:

Image

Image

Image

Image

Во вкладке "Запуски автотестов" раздела "Тест-планы":

Image

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

Image

После выполнения прогона только часть, в данном случае половина, ожидаемых тестов получит результат прохождения. Остальные останутся "крутиться". Соответственно все параметризованные тесты в наборе остануться в статусе результата "В процессе" навечно, равно как и сам запуск. Запуск можно принудительно завершить. Но тогда при большом количестве тестов в наборе, как обычно бывает в реальной жизни, получается такая мешанина дублирующихся результатов, что подобная попытка использовать функционал ТМС не валидна.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions