Рекомендации по написанию unite тестов на Python (pytest)
1. Структура приложения
- Для джанго проекта лучше располагать тесты в директориях
django applicationиз принципа "наименьшего удивления", т.к. большинство проектов на django используют подобный подход. Но внутри тестов повторяем структуруdjango application. - Для FastApi приложений, выносим директорию тестов на внешний уровень.

2. Использование Mock + DI предпочтительнее, чем monkeypatch.
Monkeypatch стоит применять в end-to-end (допустим тестирование Api endpoint).
Для всех остальных случаев стоит применять unittest.Mock, т.к. его поведение более контролируемое и прогнозируемое.
Желательно ознакомиться с достаточно интересным обсуждением на тему когда необходимо использовать monkeypatch, а когда Mock по ссылке
Пример использования 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. Стараемся использовать обычный Mock, вместо AsyncMock & MagicMock.
Mock - подходит почти под все ситуации, если ему передать в качестве спецификации тип, который мы хотим замокать, то Mock сам создаст все необходимые методы, даже если они асинхронные.
mock = Mock(BonusesPaymentValidator) await mock.some_async_method() # отработает как и ожидалось.
Mock vs AsyncMock
AsyncMock - отличается наличием асинхронным режимом, стоит применять в том случае, если мокается отдельный async callable, например:
service = SomeService() service.some_async_method = Mock() await service.some_async_method() # будет ошибка при вызове. service.some_async_method = AsyncMock() await service.some_async_method() # отработает как и ожидалось.
Mock vs MagicMock
MagicMock - отличается наличием дандер (магических) методов, например
__iter__ || __getitem__. Следует учитывать, что __call__ есть и в обычном Mock().
mock = Mock() iter(mock) # будет ошибка. magic_mock = MagicMock() iter(magic_mock) # вернёт итератор. magic_mock[0] # вернёт MagicMock
Вследствие такого поведения, чаще "стреляет в ногу", чем помогает.
Его стоит использовать только в случае, если вы полностью уверены в том, что делаете, на практике подобных кейсов почти не встречается в корпоративных приложения.
4. Используйте 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]
5. Для каждого метода желательно описывать отдельный класс и в нём уже список тестов.
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, ): ...
6. Нейминг для тест-кейсов.
Наименование теста должно максимально однозначно указывать на то, что тестируется, тут не стоит гоняться за короткими названиями и лаконичностью.
Выберите и везде придерживайтесь одного-двух паттернов для наименования тест-кейсов.
Таких паттернов очень много, вы можете поискать в сети или придумать свой, главное быть последовательным при его применении.
Вот несколько примеров:
-
test_when_<тестируемое условие>_then_<ожидаемый результат>-
Пример:
def test_when_items_contain_special_characters_then_this_remove_from_group_title(): ... def test_when_invalid_promocode_then_order_canceled(): ...
-
-
test_<тестируемый функционал>_<ожидаемый результат>-
Пример:
def test_get_user_success(): ... def test_get_user_raise_value_error(): ...
-