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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions