Pytest: протестируйте только один экземпляр параметризованного приспособления

#python #pytest

#python #pytest

Вопрос:

Настройка

Предположим, у меня есть conftest.py где я определил ресурсы для тестов

 class ClassifyRequest:
    pass

class OCRRequest:
    pass

@pytest.fixture(params=[ClassifyRequest, OCRRequest])
def api_request(request):
    return request.param()  # return one of the request instances
  

Затем, в test_api.py я использую параметризованное приспособление для тестирования сервиса:

 def test_service(api_request):
    response = send_request(api_request)
    assert response.ok()
  

Все хорошо, но затем я хочу протестировать специализированное приспособление api_request[ClassifyRequest] :

 @pytest.mark.usefixture("api_request[ClassifyRequest]")
def test_classification():
    # do something with the api_request fixture
  

Вопрос

Как можно специализировать параметризованное приспособление для тестовой функции? У меня есть две идеи:

  1. Просто используйте непараметризованное приспособление. Это неизбежно приводит к шаблонному коду.
  2. Удалите явную параметризацию из декоратора приспособления и используйте косвенную параметризацию, например
     @pytest.mark.parametrize("response_class", ["classify", "ocr"])
    def test_api(api_request):
        # do smth
      

    Замена параметров класса строками равносильна созданию двух источников конфигурации. Что, если я захочу добавить другой параметр? Должен ли я создавать для него новую строку, а также создавать экземпляр объекта внутри api_request приспособления косвенно?

  3. То же самое, за исключением сохранения параметризации класса и перемещения классов вовне conftest.py , поскольку pytest не может импортировать имена из этого модуля с помощью инструкций import.

Дополнительная информация

pytest ==6.0.1

Комментарии:

1. Вы имеете в виду, что вы параметризовали тесты с несколькими значениями, но хотите запускать только с подмножеством из них??

2. Я параметризовал приспособление, а не тесты. Некоторые тесты выполняются для всех экземпляров параметризованного устройства, а некоторые я хочу запустить только для определенного экземпляра.

3. хорошо. итак, по сути, это тестовый вариант фильтрации, который вы ищете .. вы хотите, чтобы это было похоже на передачу некоторых параметров, которые указывают, какие значения приспособления следует пропустить??

4. Я так думаю. Что-то столь же простое, как передача api_request[ClassifyRequest] в тестовую функцию.

Ответ №1:

Если все, что делает ваше устройство, — это создание экземпляра, я бы выбрал параметризацию теста, а не устройства. Но, что ж, если вам все равно придется специализировать это вручную, вы могли бы просто добавить оболочку, чтобы сделать устройство вызываемым:

 import pytest
import types

class ClassifyRequest:
    def __init__(self):
        print("ClassifyRequest ctor")

class OCRRequest:
    def __init__(self):
        print("OCRRequest ctor")

def api_request_wrapper(request):
    return request.param()  # return one of the request instances

@pytest.fixture(params=[ClassifyRequest, OCRRequest])
def api_request(request):
    return api_request_wrapper(request)

def test_classification():
    request = types.SimpleNamespace()
    request.param = ClassifyRequest
    instance = api_request_wrapper(request)
  

Хотя это кажется немного хакерским.

Комментарии:

1. Проблема в том, что сами тесты находятся в другом модуле, а не в conftest.py . Я не могу импортировать имена классов из conftest.py , чтобы pytest их видел.

2. Итак, поскольку вы не можете импортировать тип, вам нужно использовать строку, верно? Может быть, имело бы смысл переместить их в отдельный общий модуль?

3. Может быть, отдельный модуль, верно. Это даже избавило бы меня от необходимости помещать классы запросов в приспособления, при условии, что я могу создать экземпляр объекта запроса для каждой тестовой функции. Однако первоначальный вопрос о функциональности pytest остается. Могу ли я указать параметр приспособления для конкретного теста?

4. Фактически не импортируя типы? Итак, вы хотите, чтобы python автоматически привязывал строки к типам? Сомневаюсь в этом, но не могу сказать наверняка.

5. Нет-нет, если я помещу классы запросов в отдельный модуль, я смогу импортировать их в test_api.py , без проблем. Возможно, дизайн pytest заключается в объединении ресурсов в параметризованное устройство тогда и только тогда, когда вы планируете тестировать все ресурсы вместе, я не уверен.

Ответ №2:

Чего вы пытаетесь достичь, так это создать правила фильтрации тестовых примеров. Лучшее место для этого — через хук «pytest_collection_modifyitems» в Pytest.

Этот перехват будет вызван, как только все тесты будут собраны по пути, который вы указываете в качестве входных данных для Pytest.

Вы можете поместить свою логику в этот хук.

Всегда рекомендуется связывать идентификатор с вашими параметрами, как показано ниже:

 @pytest.fixture(params=[ClassifyRequest, OCRRequest], 
                ids=['ClassifyRequest', 'OCRRequest'])
def api_request(request):
    return request.param()  # return one of the request instances
  

Как только вы получите это, ваш тестовый набор будет сгенерирован как test_classification[ClassifyRequest] и test_classification[‘OCRRequest’]..

Если вы запускаете определенный набор тестов и хотите, чтобы фильтрация применялась ко всем тестам, то самой опции -k будет достаточно.

Но поскольку здесь это только для подмножества тестов, к которым вы хотите применить правило, определение перехвата может быть в модуле, где присутствует тест.

Комментарии:

1. Честно говоря, изменение привязки коллекции тестов для отмены выбора тестовых элементов похоже на введение другого источника управления. Если я хотел бы добавить другой класс запроса, чтобы протестировать его в test_api.py с другими, а затем какой-нибудь специализированный тест только для этого класса, я должен снова изменить этот хук. Я не хочу вводить дополнительные источники управления. В идеале я хотел бы объявить новый ресурс один раз и, возможно, добавить его в aggregate fixture без изменения какого-либо другого кода.

2. И да, спасибо! Это интересное понимание.

3. Допустим, у вас есть некоторое ‘N’ количество тестов для запуска, и вы не хотите запускать какой-либо тест, связанный с ‘OCRRequest’, вы всегда можете сделать -k ‘not OCRRequest’ в вызове pytest, который выполнит фильтрацию по умолчанию, как я уже упоминал.. Это будет самый простой способ выполнить ваши тесты только с ограниченным набором параметризованных.

4. Что, если я хочу запускаться test_service со всеми запросами и test_classification только с ClassificationRequest и test_ocr с OCRRequest ? Должен ли я указывать -k test_service or test_classification[ClassificationRequest] or test_ocr[OCRRequest] ? Мне кажется, это взлом, поскольку для этого требуется перечислить все сценарии, которые я хочу протестировать. К сожалению, между ними нет общего шаблона.

5. Верно, что … трудно поддерживать, когда у вас слишком много таких условий..

Ответ №3:

Примечание: это предварительный вывод, я создал проблему на pytest Github, чтобы узнать мнение сообщества по этой теме.


Я думаю, что самый чистый способ реализовать желаемую специализацию — переместить классы запросов в отдельный модуль и использовать conftest.py только для объявления функций приспособления. Согласно документации pytest,

Если во время реализации ваших тестов вы поймете, что хотите использовать функцию приспособления из нескольких тестовых файлов, вы можете переместить ее в conftest.py файл.

Если вы хотите сделать тестовые данные из файлов доступными для ваших тестов, хороший способ сделать это — загрузить эти данные в приспособление для использования вашими тестами.

Таким образом, я могу импортировать классы в тестовые модули и параметризовать приспособление косвенно через тестовые функции. В моем случае классы запросов очень легко создавать, поэтому нет необходимости в отдельном приспособлении для каждого класса, только агрегат. Но, как правило, можно определить приспособление для каждого ресурса и агрегированное параметризованное приспособление.

Ответ №4:

Вы также могли бы разделить приспособления для ClassifyRequest и использовать его в своем классификационном тесте и api_request приспособлении.

Предполагается, что создание экземпляров запросов обходится дешево. Если они дорогие, вы могли бы рассмотреть возможность изменения области применения приспособлений.

 @pytest.fixture
def classify_request():
    return ClassifyRequest()  # return ClassifyRequest instance

@pytest.fixture
def ocr_request():
    return OCRRequest()  # return OCRRequest instance

@pytest.fixture(params=["classify", "ocr"])
def api_request(request, classify_request, ocr_request):
    # return one of the request instances
    if request.param == "classify":
         return classify_request
    elif request.param == "ocr":
         return ocr_request
    assert False
  

или же (это небольшая модификация вашей идеи) внесите в белый список классы, из которых вы хотите test_classification

 @pytest.fixture(params=[ClassifyRequest, OCRRequest])
def clazz(request):
    return request.param

@pytest.fixture
def api_request(clazz):
    return clazz()

@pytest.mark.parametrize("clazz", [ClassifyRequest], indirect=True)
def test_classification(api_request):
    # do smth