SQ Blog— заметки разработчика.

Рекомендации по написанию unite тестов на Python (pytest)

03.01.2024Pythonpytest

1. Структура приложения

В обычном приложени (например FastApi), структура тестов повторяет структуру проекта, для Django проекта лучше располагать тесты в директориях django application.

django_and_fastapi_project_structure

2. Использование Mock предпочтительнее monkeypatch

Для всех случаев лучше применять unittest.Mock или свои релаизации для тестов.

Monkeypatch стоит применять только при невозможности применить Mock, из-за отсутствия DI контейнера (или Depends из fastapi), в тестах внешних интерфейсах, таких как api endpoint.

Пример использования Mock

@pytest.fixture
def bonus_calculator_mock() -> CalculateBonusesService:
    # Применение `Mock` в качестве фикстуры
    service = Mock(CalculateBonusesService)
    service.calculate.return_value = CalculateBonusesResult(
        mobile_phone="79999999999",
        amount=100,
        expiration=TimeProvider.now(),
    )

    return service


class TestAcceptOrders:

    async def test_incorrect_bonus_payment_failure(
            self,
            db, 
            bonus_calculator_mock,
    ):
        items = OrderItemFactory.batch(3, price=1000, quantity=1)
        order = OrderFactory.build(items=items, bonus_payment=200)

        # Если необходимо переопределить `Mock` для конкретного тест кейса.
        raises_validator = Mock(BonusesPaymentValidator)
        raises_validator.validate.side_effect = OrderConstraintError()

        with pytest.raises(OrderConstraintError):
            await AcceptOrders(
                db=db,
                calculate_service=bonus_calculator_mock,
                validate_service=raises_validator,
            ).execute(order=order)

Обсуждение на тему когда необходимо использовать monkeypatch, а когда Mock по ссылке

3. Используйте parametrize

Для устранения избыточности тест-кейсов, лучше всего использовать parametrize.

Например этот тест-кейс создаст 12 тестов, с декартовым произведением параметров (это похоже на itertools.product(...)).

class TestQueryBuilderFilters:

    @pytest.mark.parametrize(
        "filter_value, exp_query_name",
        (
                ("foo", "term"),
                (1, "term"),
                (5.0, "term"),
                (["foo", "bar"], "terms"),
                ([1, 13], "terms"),
                ([5.0, 7.7], "terms"),
        ),
    )
    @pytest.mark.parametrize(
        "filter_is_nested, filter_key",
        (
                (False, "test_key"),
                (True, "test.nested_key"),
        ),
    )
    def test_when_filter_exact_and_value_is_base_or_list_then_query_name_choices_correct(
        self,
        filter_value,
        exp_query_name,
        filter_is_nested,
        filter_key,
    ):
        filter_item = FilterItem(
            key=filter_key,
            filter_type=EXACT,
            is_nested=filter_is_nested,
            value=filter_value,
        )

        result = Builder.build({"filters": [filter_item]})

        assert exp_query_name in result
        assert filter_value == result[exp_query_name][filter_item.key]

4. Для каждого метода желательно описывать отдельный класс и в нём уже список тестов.

class TestCategoriesRepositoryAddMany:
    async def test_add_many_success(
        self,
        category_repo,
        category_collection,
    ):
        ...

    @pytest.mark.parametrize(
        "name, exp_slug",
        (
            ("Some Name", "some-name"),
            ("Категория для теста", "kategoriya-dlya-testa"),
        ),
    )
    async def test_when_category_created_then_slug_autogenerated(
        self,
        category_repo,
        name,
        exp_slug,
    ):
        ...


class TestCategoriesRepositoryGetFiltered:
    @pytest.mark.parametrize(
        "count",
        (0, 4, 13),
    )
    async def test_when_parent_not_set_in_filter_then_query_not_filtered_by_parent(
        self,
        category_repo,
        count,
    ):
       ...

    async def test_when_parent_is_str_null_then_only_root_categories_in_result(
        self,
        category_repo,
    ):
        ...

    async def test_when_parent_is_set_then_only_with_this_parent_categories_in_result(
        self,
        category_repo,
    ):
        ...

5. Нейминг для тест-кейсов.

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

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

Например:

  1. test_when_<тестируемое условие>_then_<ожидаемый результат>

    def test_when_items_contain_special_characters_then_this_remove_from_group_title():
        ...
    
    def test_when_invalid_promocode_then_order_canceled():
        ...
    
  2. test_<тестируемый функционал>_<ожидаемый результат>

    def test_get_user_success():
        ...
    
    def test_get_user_raise_value_error():
        ...
    

Links

Github

© 2025 Dmitrii Kulakov

|

Privacy Policy