Рекомендации по написанию unite тестов на Python (pytest)
1. Структура приложения
В обычном приложени (например FastApi), структура тестов повторяет структуру проекта, для Django проекта лучше располагать тесты в директориях django application
.
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. Нейминг для тест-кейсов.
Выберете и везде придерживайтесь одного-двух паттернов для наименования тест-кейсов.
Таких паттернов очень много, вы можете поискать в сети или придумать свой, главное быть последовательным при его применении.
Например:
-
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(): ...