Обновить

Комментарии 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, наоборот, требует знать внутреннее устройство модуля: где именно импортирована зависимость, под каким именем она лежит, в каком месте её нужно подменить.

Приведите, пожалуйста, пример конкретной проблемы. Забегая вперёд скажу: тесты - это не продуктовый код. Причины, по которым что-то плохо работает в продуктовом коде, могут быть совершенно нерелевантны для тестов. Поэтому важно докопаться до сути, а не опираться на чьи-то представления о хорошем и плохом. И я, честно, это и пытаюсь сделать.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации