> Все равно там будет столько объектов, сколько у меня тестовых сценариев. И выгоды по сравнению с «объяви объект прямо в тесте» нет.
Конечно же, есть. Вы объявляете все это только один раз (а не в каждом тесте).
> Просто считайте, что я тестирую не «читающее апи», а «маппящий метод».
Мапящий метод в бд не лезет, он о ней не в курсе. Он принимает какие-то данные и возвращает смапленные данные. Пишется он одинаково, что с моками, что без моков, просто потому что зависимостей не имеет (по крайней мере от бд, остальные мы на данный момент не обсуждаем).
> А вот и нет. В тесткейсе описана доменная сущность (метаданные Х), а не состояние БД. С
Если это доменная сущность, то она тогда в аргументах. В промежуточном слое ее быть не может, т.к. я просто не знаю ничего об этом промежуточном слое. Я знаю, что мой метод принимает определенные аргументы, лезет в бд, и что-то там возвращает. Я ничего не знаю о существовании (и особенностях работы) каких-либо промежуточных слоев.
То есть если я знаю о том, что метод работает с доменной сущностью — то значит этот метод в базу не лезет, в базу за него полез (и построил потом доменную сущность) кто-то другой и в аргументах передал.
> Пример кода.
Вы можете его увидеть в описании любого фреймворка для моков.
> Где-то объект плоский, где-то многоуровневый, где-то ссылки, где-то включения. Для всего этого в БД надо построить исходные данные.
Для всего этого достаточно одного набора тестовых данных. В котором будет и простой объект и многоуровневый, и что вам там надо еще, это-во-первых. Во-вторых — мапингом ваше «читающее апи» вообще заниматься не должно, это ответственность, как минимум, другого метода, как максимум — другого модуля, у которого данные для маппинга будут просто в аргументах. Вот в нем-то и будут тесты на логику маппинга.
> У меня тестовая проверка зависит от поведения зависимостей
Так если поведение метода не меняется из-за работы зависимостей, то вы и не сможете выделить тесткейз.
> (самый тривиальный пример — API, читающий объект. Очевидно, он зависит от того, что в хранилище).
Результат зависит, а поведение? Нам же требуются разные тесткейзы с разными данными в хранилище. Почему вы не можете написать все тесты с теми же данными?
> Он выполняет задачу «подать на вход тестируемому объекту ровно те данные, которые описаны в тесткейсе».
Это аргументы метода и внешнее состояние (сервисов, бд, етц.). То, что возвращают те или иные зависимости, данными, конечно, не является, так как мы даже не знаем (и не должны) о том, какие зависимости данный метод тянет (черный ящик же).
> В следующем предложении было написано. Ну и у Мезароса тоже.
То, что там написано — по-просту неверно.
> Да, о качественном. И вот у качественного кода в моей практике мало зависимостей
Тогда либо у вас нарушается SRP, либо вам везет.
> Пример того, как вы получаете SUT с глобальным сетапом, в котором заменено поведение одной зависимости.
Я не понимаю что конкретно вы хотите. Чем этот код, по-вашему, должен отличаться от построения сетапа с нуля, кроме того, что часть сетапа оказывается опущена?
конечно же нужны. Но благодаря тому, что нет многократного дублирования, их на порядок меньше.
> Значит, ваш сетап должен (заранее) содержать кейсы под все тесты. И чем это лучше «давайте зададим свой кейс в каждом тесте»?
Да нет, конечно. Я понял проблему, у вас, видимо, поведение зависимых объектов существенно зависит от поведения зависимостей? Тогда, конечно, надо делать много сетапов. Если же это не так, вам одного сетапа хватит на много-много тестов.
Потому что лучше мокать только БД, чем и сервис и БД (вы же сам сервис тоже тестировать будете, это ваш код? или я неверно понял вопрос?)
> То есть тоже нужен мок?
Зачем?
> Глобальный сетап — зло.
В чем? Я вижу только добро — экономию при прочих равных.
> Ну то есть нужно сделать целую отдельную инфраструктуру, которая будет позволять делать глобальный сетап (см. выше), и подменять любую его часть.
Зачем для этого какая-то особая инфраструктура? Подмена куска сетапа не сложнее, чем его определение.
> … если зависимостей мало, то и моков мало. А если моков мало, то их сетап — это ровно то же самое, что подмена в глобальном. Так что никакого «в десятки меньше кода».
Но на практике их много, либо у вас у god-object'ы вместо классов.
> Это, простите, как? У класса, который не делает ничего, осмысленных зависимостей быть не может.
Если он сам не делает ничего, то он делегирует кому-то всю полезную работу.
> Таким, что в каждом тесте сетапится только то, что нужно для его выполнения.
А в случае интеграционных — вообще обычно не сетапится. Сетап зависимостей для интеграционного теста — исключительная ситуация. Для сьюитов — ну да, бывает, хоть и не слишком часто.
> Стоп-стоп. Вы сравниваете затраты с использованием тестовой БД, а я спрашиваю, мне мокать БД или сервис.
Я ответил. Мокать БД. Просто сделал на всякий случай пояснение — если по каким-то исключительным причинам вы можете легко поднять и без проблем использовать полноценную тестовую БД — то мокать и вовсе ничего не надо (но это чисто умозрительная ситуация, понятно, что в реальности такое представить сложно).
> Мне надо проверить, что SUT корректно себя ведет, если его зависимость (внутренняя!) ведет себя некорректно. Как это сделать без мока?
Никак. А зачем решать бесполезные задачи?
> То есть вы предлагаете писать тесты на А, Б, Ц и Д?
Те же самые тесты, я же указал.
> Тогда у вас получается больше кода, а не меньше, потому что тесты те же, только еще и для А и Б надо засетапить все, что нужно для Ц и Д.
Сетапить вообще ничего не надо кроме глобального сетапа.
> Так в разных тестах разное поведение нужно, вообще-то. Какие тут глобальные моки?
Если надо подменить какую-то конкретную часть сетапа — ну не проблема, подменяйте. В любом случае, это обычно требует в десятки меньше кода, чем на полноценный сетап всех моков.
Каким образом? Надо тратить лишнее время на поддержку моков и их синхронизацию с реальным поведением зависимостей. Чем это проще?
> А много зависимостей (обычно) возникает тогда, когда много ответственностей. О чем и речь.
Вообще-то все наоборот. Количество зависимостей растет при декомпозиции. Чем меньше делает каждый отдельный класс — тем больше у него зависимостей, т.к. вместо выполнения работы самостоятельно, класс ее делегирует зависимости.
И, наоборот, чем больше ответственности — тем меньше зависимостей, с god-object в пределе, который делает все и у которого практически нет зависимостей.
> Допустим, ваш тест показал, что сумма контракта считается неверно. За ее расчет отвечает около сотни классов. Тривиально?
А юнит-тесты вам как помогут? Если какой-то из юнит-тестов падает на сломанной зависимости, то он и в интеграционном виде на ней упадет и вы увидите ошибку там же. Если же не падает — ну тогда в случае интеграционных тестов вы эту ошибку увидите, а в случае юнит-тестов — вообще нет, она будет пропущена.
> А еще лучше — обнаружить две хорошо локализованные ошибки, что наличие юнит-тестов и позволяет сделать.
Конечно, лучше. И еще лучше, если бы не надо было совершать кучу лишних телодвижений для моков. Но такую методологию тестирования еще не придумали. Либо мы пишем простые и качественные тесты, которые, однако, чуть менее точно локализуют ошибку, либо сложные и некачественные, которые локализуют ошибку лучше.
Тот код, который не решает каких-то конкретных задач (добавляет новый функционал, улучшает качество архитектуры, увеличивает простоту поддержки) — лишний.
> Окажет. Проблема-то возникает только тогда, когда мы вынуждены для теста сетапить те зависимости, которые к тесту отношения не имеют.
Проблема возникает, когда много зависимостей. Единственный способ ощутимо снизить количество зависимостей — это просто перебором построить такой граф, чтобы их было минимум. Но перетасовывать функционал модулей, нарушая их семантику, ради снижения количества зависимостей, чтобы было проще потестировать, хотя можно это не делать — весьма странное решение.
> И каким образом у вас вариант с моками оказался больше?
С моками вам надо настраивать моки, без моков — соответственно, не надо.
> С моками досточно имитировать непосредственные зависимости, без моков — надо построить все зависимости в графе.
Надо мокировать только что, что надо мокировать в итоге (какие-то внешние зависимости), а не все в графе. И делается это один раз. Под какие-то тесть-сьюиты могут вноситься, конечно, какие-то необходимые изменения, но в самих тестах уже ничего дополнительно делать практически никогда не надо. В итоге лишнего кода в разы меньше.
Бд. Потому что затрат меньше, чем в случае использование тестовой бд. Если в вашем случае тестовая бд дает меньше затрат (что бывает редко, но вдруг) — ну тогда, конечно, можно и тестовую бд. Чем ближе тест к реальности — тем он, конечно же, лучше.
> И еще — локальные зависимости ошибаться не могут?
Могут, конечно же, но вероятность того, что два модуля согласованно ошибутся так, чтобы съесть ошибку, весьма мала.
> А вот теперь смотрите: у вас есть модули А и Б, оба зависят от Ц и Д. Мы решили, по вашей методике, сэкономить тесты, и написали только интеграционные тесты на А и Б (тем самым Ц и Д тестируются имплицитно).
Конечно же, мы не пишем тесты только на А и Б, мы пишем все те же самые тесты, что писали бы и в случае использования моков. Просто мы «искаробки» получаем некоторое дополнительное, как вы выразились, «имплицитное», тестирование зависимых модулей, что позволит поймать больше багов в этих модулях. То есть не «меньше тестов при том же результате», а «выше результат при тех же тестах».
> Значит, в вашем тесте на Х есть три мока (З1, З2, З3) вместо одного (У).
Так это глобальные моки. Они настраиваются раз и для всех тестов.
Если вы разобьете класс и снизите число зависимостей, то это никакого влияния на результат не окажет, т.к. эти зависимости просто будут устанавливаться _в других_ тестах. Вы перенесли код из одного места в другое. Да, это может в итоге привести к некоему упрощению (хоть и не всегда), но проблемы не решает — как куча лишнего кода была, так и осталась.
Ну и, да, нет ничего хуже, чем портить архитектуру приложения ради того, чтобы оно было «тестируемей». Сам факт того, что это приходится делать, уже говорит о том, что что-=то пошло не так.
Давайте сразу определимся — внешнее окружение (бд, удаленные сервисы и т.д.), естественно следует мокировать, это попадает под «использовать только в тех случаях, когда без них не обойтись».
> А почему поймано больше багов?..
Простой пример, у вас есть один и тот же тест на некоторый модуль, вы его запускаете в двух форматах — либо заменив зависимости моками, либо не заменив. В первом случае выполняется только код непосредственно тестируемого модуля, а во втором случае — и код всех используемых зависимостей, то есть один интеграционный тест заменяет 1+количество_используемых_зависимостей юнит-тестов. Кроме того, вы просто можете забыть обновить моки, в итоге с реальными зависимостями тест падает (т.к. их поведение изменилось), а с моками — проходит. Естественно, может быть обратная ситуация — когда несколько багов, взаимонакладываясь друг на друга, в итоге дают правильное поведение — но это очень большая редкость по сравнению с предыдущими двумя пунктами
> Например, у вас есть простой однострочный тест, который ловит абсолютно все ошибки (идеал по вашим же критериям). Вот только искать, где именно ошибка, вам придется самому и тест, несмотря на высочайшую «оценку качества», не сможет здесь помочь.
Тест укажет, в чем состоит ошибка (какое именно требование нарушено), а если известно, в чем ошибка, то ее локализация (в тех рамках, в которых она может быть выполнена за счет изоляции тестов) — тривиальная задача, которая даже в самых сложных случаях решается за время порядка единиц минут, обычно же — секунды.
И, да, почти всегда лучше обнаружить две плохо локализованные ошибки, чем одну — хорошо локализованную.
Тогда я о ней не могу знать.
> Значит, вы тестируете не свой метод.
Нет, просто я не хочу привязывать тесты к реализации. Тесты проверяют спецификацию, спецификация не зависит от реализации.
Конечно же, есть. Вы объявляете все это только один раз (а не в каждом тесте).
> Просто считайте, что я тестирую не «читающее апи», а «маппящий метод».
Мапящий метод в бд не лезет, он о ней не в курсе. Он принимает какие-то данные и возвращает смапленные данные. Пишется он одинаково, что с моками, что без моков, просто потому что зависимостей не имеет (по крайней мере от бд, остальные мы на данный момент не обсуждаем).
Если это доменная сущность, то она тогда в аргументах. В промежуточном слое ее быть не может, т.к. я просто не знаю ничего об этом промежуточном слое. Я знаю, что мой метод принимает определенные аргументы, лезет в бд, и что-то там возвращает. Я ничего не знаю о существовании (и особенностях работы) каких-либо промежуточных слоев.
То есть если я знаю о том, что метод работает с доменной сущностью — то значит этот метод в базу не лезет, в базу за него полез (и построил потом доменную сущность) кто-то другой и в аргументах передал.
> Пример кода.
Вы можете его увидеть в описании любого фреймворка для моков.
Для всего этого достаточно одного набора тестовых данных. В котором будет и простой объект и многоуровневый, и что вам там надо еще, это-во-первых. Во-вторых — мапингом ваше «читающее апи» вообще заниматься не должно, это ответственность, как минимум, другого метода, как максимум — другого модуля, у которого данные для маппинга будут просто в аргументах. Вот в нем-то и будут тесты на логику маппинга.
Так если поведение метода не меняется из-за работы зависимостей, то вы и не сможете выделить тесткейз.
> (самый тривиальный пример — API, читающий объект. Очевидно, он зависит от того, что в хранилище).
Результат зависит, а поведение? Нам же требуются разные тесткейзы с разными данными в хранилище. Почему вы не можете написать все тесты с теми же данными?
Это аргументы метода и внешнее состояние (сервисов, бд, етц.). То, что возвращают те или иные зависимости, данными, конечно, не является, так как мы даже не знаем (и не должны) о том, какие зависимости данный метод тянет (черный ящик же).
> В следующем предложении было написано. Ну и у Мезароса тоже.
То, что там написано — по-просту неверно.
> Да, о качественном. И вот у качественного кода в моей практике мало зависимостей
Тогда либо у вас нарушается SRP, либо вам везет.
> Пример того, как вы получаете SUT с глобальным сетапом, в котором заменено поведение одной зависимости.
Я не понимаю что конкретно вы хотите. Чем этот код, по-вашему, должен отличаться от построения сетапа с нуля, кроме того, что часть сетапа оказывается опущена?
конечно же нужны. Но благодаря тому, что нет многократного дублирования, их на порядок меньше.
> Значит, ваш сетап должен (заранее) содержать кейсы под все тесты. И чем это лучше «давайте зададим свой кейс в каждом тесте»?
Да нет, конечно. Я понял проблему, у вас, видимо, поведение зависимых объектов существенно зависит от поведения зависимостей? Тогда, конечно, надо делать много сетапов. Если же это не так, вам одного сетапа хватит на много-много тестов.
Потому что нет кода, который не выполняет никакой задачи (мок сервиса в данном случае).
> Чтобы протестировать описанные кейсы.
Ну так тестируйте с моком БД.
> Антипаттерн shared fixture.
Почему это антипаттерн?
> Или нет. У меня на практике мало зависимостей (кроме тех классов, где я знаю, что нарушен SRP, и которые стоят в очереди на рефакторинг).
Если у вас весь весь функционал в куче, то, конечно, зависимостей мало. Но мы же о качественном коде говорим?
> Покажите пример, пожалуйста.
Пример чего? Использования операции присваивания? Или вызова методов фреймворка для создания моков?
Еще раз, _сетап для теста_. Глобальный сетап возникает не сам, он пишется. Иногда он правится под тест-сьюиты — и очень редко под конкретные тесты.
Потому что лучше мокать только БД, чем и сервис и БД (вы же сам сервис тоже тестировать будете, это ваш код? или я неверно понял вопрос?)
> То есть тоже нужен мок?
Зачем?
> Глобальный сетап — зло.
В чем? Я вижу только добро — экономию при прочих равных.
> Ну то есть нужно сделать целую отдельную инфраструктуру, которая будет позволять делать глобальный сетап (см. выше), и подменять любую его часть.
Зачем для этого какая-то особая инфраструктура? Подмена куска сетапа не сложнее, чем его определение.
> … если зависимостей мало, то и моков мало. А если моков мало, то их сетап — это ровно то же самое, что подмена в глобальном. Так что никакого «в десятки меньше кода».
Но на практике их много, либо у вас у god-object'ы вместо классов.
Если он сам не делает ничего, то он делегирует кому-то всю полезную работу.
> Таким, что в каждом тесте сетапится только то, что нужно для его выполнения.
А в случае интеграционных — вообще обычно не сетапится. Сетап зависимостей для интеграционного теста — исключительная ситуация. Для сьюитов — ну да, бывает, хоть и не слишком часто.
Я ответил. Мокать БД. Просто сделал на всякий случай пояснение — если по каким-то исключительным причинам вы можете легко поднять и без проблем использовать полноценную тестовую БД — то мокать и вовсе ничего не надо (но это чисто умозрительная ситуация, понятно, что в реальности такое представить сложно).
> Мне надо проверить, что SUT корректно себя ведет, если его зависимость (внутренняя!) ведет себя некорректно. Как это сделать без мока?
Никак. А зачем решать бесполезные задачи?
> То есть вы предлагаете писать тесты на А, Б, Ц и Д?
Те же самые тесты, я же указал.
> Тогда у вас получается больше кода, а не меньше, потому что тесты те же, только еще и для А и Б надо засетапить все, что нужно для Ц и Д.
Сетапить вообще ничего не надо кроме глобального сетапа.
> Так в разных тестах разное поведение нужно, вообще-то. Какие тут глобальные моки?
Если надо подменить какую-то конкретную часть сетапа — ну не проблема, подменяйте. В любом случае, это обычно требует в десятки меньше кода, чем на полноценный сетап всех моков.
Каким образом? Надо тратить лишнее время на поддержку моков и их синхронизацию с реальным поведением зависимостей. Чем это проще?
> А много зависимостей (обычно) возникает тогда, когда много ответственностей. О чем и речь.
Вообще-то все наоборот. Количество зависимостей растет при декомпозиции. Чем меньше делает каждый отдельный класс — тем больше у него зависимостей, т.к. вместо выполнения работы самостоятельно, класс ее делегирует зависимости.
И, наоборот, чем больше ответственности — тем меньше зависимостей, с god-object в пределе, который делает все и у которого практически нет зависимостей.
А юнит-тесты вам как помогут? Если какой-то из юнит-тестов падает на сломанной зависимости, то он и в интеграционном виде на ней упадет и вы увидите ошибку там же. Если же не падает — ну тогда в случае интеграционных тестов вы эту ошибку увидите, а в случае юнит-тестов — вообще нет, она будет пропущена.
> А еще лучше — обнаружить две хорошо локализованные ошибки, что наличие юнит-тестов и позволяет сделать.
Конечно, лучше. И еще лучше, если бы не надо было совершать кучу лишних телодвижений для моков. Но такую методологию тестирования еще не придумали. Либо мы пишем простые и качественные тесты, которые, однако, чуть менее точно локализуют ошибку, либо сложные и некачественные, которые локализуют ошибку лучше.
Тот код, который не решает каких-то конкретных задач (добавляет новый функционал, улучшает качество архитектуры, увеличивает простоту поддержки) — лишний.
> Окажет. Проблема-то возникает только тогда, когда мы вынуждены для теста сетапить те зависимости, которые к тесту отношения не имеют.
Проблема возникает, когда много зависимостей. Единственный способ ощутимо снизить количество зависимостей — это просто перебором построить такой граф, чтобы их было минимум. Но перетасовывать функционал модулей, нарушая их семантику, ради снижения количества зависимостей, чтобы было проще потестировать, хотя можно это не делать — весьма странное решение.
С моками вам надо настраивать моки, без моков — соответственно, не надо.
> С моками досточно имитировать непосредственные зависимости, без моков — надо построить все зависимости в графе.
Надо мокировать только что, что надо мокировать в итоге (какие-то внешние зависимости), а не все в графе. И делается это один раз. Под какие-то тесть-сьюиты могут вноситься, конечно, какие-то необходимые изменения, но в самих тестах уже ничего дополнительно делать практически никогда не надо. В итоге лишнего кода в разы меньше.
Бд. Потому что затрат меньше, чем в случае использование тестовой бд. Если в вашем случае тестовая бд дает меньше затрат (что бывает редко, но вдруг) — ну тогда, конечно, можно и тестовую бд. Чем ближе тест к реальности — тем он, конечно же, лучше.
> И еще — локальные зависимости ошибаться не могут?
Могут, конечно же, но вероятность того, что два модуля согласованно ошибутся так, чтобы съесть ошибку, весьма мала.
> А вот теперь смотрите: у вас есть модули А и Б, оба зависят от Ц и Д. Мы решили, по вашей методике, сэкономить тесты, и написали только интеграционные тесты на А и Б (тем самым Ц и Д тестируются имплицитно).
Конечно же, мы не пишем тесты только на А и Б, мы пишем все те же самые тесты, что писали бы и в случае использования моков. Просто мы «искаробки» получаем некоторое дополнительное, как вы выразились, «имплицитное», тестирование зависимых модулей, что позволит поймать больше багов в этих модулях. То есть не «меньше тестов при том же результате», а «выше результат при тех же тестах».
> Значит, в вашем тесте на Х есть три мока (З1, З2, З3) вместо одного (У).
Так это глобальные моки. Они настраиваются раз и для всех тестов.
Ну и, да, нет ничего хуже, чем портить архитектуру приложения ради того, чтобы оно было «тестируемей». Сам факт того, что это приходится делать, уже говорит о том, что что-=то пошло не так.
Давайте сразу определимся — внешнее окружение (бд, удаленные сервисы и т.д.), естественно следует мокировать, это попадает под «использовать только в тех случаях, когда без них не обойтись».
> А почему поймано больше багов?..
Простой пример, у вас есть один и тот же тест на некоторый модуль, вы его запускаете в двух форматах — либо заменив зависимости моками, либо не заменив. В первом случае выполняется только код непосредственно тестируемого модуля, а во втором случае — и код всех используемых зависимостей, то есть один интеграционный тест заменяет 1+количество_используемых_зависимостей юнит-тестов. Кроме того, вы просто можете забыть обновить моки, в итоге с реальными зависимостями тест падает (т.к. их поведение изменилось), а с моками — проходит. Естественно, может быть обратная ситуация — когда несколько багов, взаимонакладываясь друг на друга, в итоге дают правильное поведение — но это очень большая редкость по сравнению с предыдущими двумя пунктами
Тест укажет, в чем состоит ошибка (какое именно требование нарушено), а если известно, в чем ошибка, то ее локализация (в тех рамках, в которых она может быть выполнена за счет изоляции тестов) — тривиальная задача, которая даже в самых сложных случаях решается за время порядка единиц минут, обычно же — секунды.
И, да, почти всегда лучше обнаружить две плохо локализованные ошибки, чем одну — хорошо локализованную.