Комментарии 5
Вся статья опирается на бездоказательные тезисы. Чем плоха "перевёрнутая" пирамида тестирования? Чем плох манки-патчинг? Что мешает сделать высокоуровневые абстракции и не использовать инверсию контроля?
Рекомендую особенно крепко задуматься про манки-патчинг. Использование его в тестах действительно пораждает какие-то проблемы или это просто догматизм?
Это не догматизм, а вопрос цены.
Перевёрнутая пирамида тестирования плоха тем, что медленные и дорогие тесты начинают проверять то, что можно было бы проверить быстро и изолированно.
Monkey patching - это сигнал плохого дизайна. Это обходной путь, чтобы протестировать код изолировано. Ещё он плох тем, что часто прибивает тест к внутренностям реализации. Переименовал импорт, перенёс объект, изменил способ сборки зависимости - тест сломался, хотя поведение не изменилось.
Высокоуровневые абстракции без инверсии зависимостей возможны. Но такой код всё равно связан с низкоуровневыми деталями. Это и есть проблема, которую DIP пытается решить.
Я сфокусируюсь на манки-патчинге.
Monkey patching - это сигнал плохого дизайна. Это обходной путь, чтобы протестировать код изолировано.
Пожалуйста, отбросьте все догмы и чужие мнения и вдумайтесь в логику того, что вы говорите. Цель - "протестировать код изолированно". Под эту цель вы предлагаете переписать продуктовый код. И это - не обходной путь? Ещё раз. Вам нужно протестировать код. Чтобы это сделать, код нужно переписать. Это точно хороший дизайн или всё-таки недостаток инфраструктуры?
Ещё он плох тем, что часто прибивает тест к внутренностям реализации. Переименовал импорт, перенёс объект, изменил способ сборки зависимости - тест сломался, хотя поведение не изменилось.
Простите, я не понял, какие изменения имеются в виду. Вы не могли бы привести короткие примеры? Сразу скажу следующий вопрос: а во всех этих случаях, что инжектируемые моки исправлять разве не придётся? Мне не понятно, в каких ситуациях тест с подменой через манки-патчинг может сломаться, а подмена через DI при этом не сломается. Они же подменяют одни и те же интерфейсы. Только манки-патчинг выражает это явно, а использование DI заставляет нарушать связи в коде.
Мне не понятно, в каких ситуациях тест с подменой через манки-патчинг может сломаться, а подмена через DI при этом не сломается.
Например, тест с monkey patching часто выглядит так:
with patch("tickets.repo.save", AsyncMock()) as save:
await submit_ticket(...)
save.assert_awaited_once_with(Ticket(...))
Такой тест проверяет только факт вызова save(). Но если в коде появится баг:
await repo.save(ticket)
await repo.delete(ticket.id) # баг: удаляем сразу после сохранения
тест всё равно останется зелёным. Почему? Потому что он проверял вызов метода, а не результат операции.
В варианте с тестовым дублёром можно проверить состояние системы после выполнения:
ticket = await submit_ticket(...)
assert await repo.get(ticket.id) == ticket
Проверяем финальное состояние системы, а не вызовы методов, как это обычно делается с моками и monkey patching.
Только манки-патчинг выражает это явно, а использование DI заставляет нарушать связи в коде.
На мой взгляд, как раз наоборот. В случае DI всё происходит явно, все зависимости всегда видны в списке параметров. Monkey patching, наоборот, требует знать внутреннее устройство модуля: где именно импортирована зависимость, под каким именем она лежит, в каком месте её нужно подменить.
тест всё равно останется зелёным
Во-первых, не останется, потому что delete же не замокан. Но важнее другое: ничто не мешает подменить весь repo на тот же самый тестовый дублёр. Вам не нужен DI для этого.
В случае DI всё происходит явно, все зависимости всегда видны в списке параметров.
Тут мы говорим о разном. Я имел ввиду явность самой подмены кода. Давайте коротко опишу картину целиком.
У вас есть что-то такое:
def a(x: int): return external.b(x)Вам нужно подменить b в тестах. Для этого вы переписываете код на:
def a(b: Callable[[int], int], x: int): return b(x)Во-первых, код просто стал сложнее. Во-вторых, сразу появляется вопрос: а какие именно значения у b могут быть в продакшене? Сюда действительно приходят разные функции или это сделано только для тестирования? В-третьих, вместо явного `with patch`, в тесте моки видны только по неймингу. Сравните:
with patch("external.b", side_effect=lambda x: x):
a(1)И
b_mock = lambda x: x
a(b_mock, 1)Сразу подчеркну, что сам по себе DI никак не влияет на связность между a и b. То, что a получает b при вызове снаружи, не значит, что они не приколочены гвоздями друг к другу. На любое изменение a придётся менять b и на любое изменение b придётся менять a. Это может быть нормально, а может быть и плохо. Но DI на это никак не влияет, и при этом обманывает читателя, якобы a может работать с чем-то, кроме b.
Monkey patching, наоборот, требует знать внутреннее устройство модуля: где именно импортирована зависимость, под каким именем она лежит, в каком месте её нужно подменить.
Приведите, пожалуйста, пример конкретной проблемы. Забегая вперёд скажу: тесты - это не продуктовый код. Причины, по которым что-то плохо работает в продуктовом коде, могут быть совершенно нерелевантны для тестов. Поэтому важно докопаться до сути, а не опираться на чьи-то представления о хорошем и плохом. И я, честно, это и пытаюсь сделать.

Инвертируем зависимости одного FastAPI-эндпоинта