7 правил хорошего тона при написании Unit-тестов


    “Хорошими манерами обладает тот,
    кто наименьшее количество людей
    ставит в неловкое положение.”
    Дж. Свифт


    Привет, коллеги! Сегодня я бы хотел поговорить о Unit-тестировании и некоторых “правилах” при их написании. Конечно, они неформальные и не обязательны к выполнению, но при их соблюдении всем будет приятно и легко читать и поддерживать тесты, которые вы написали. Мы в Wrike видели достаточно Unit-тестов, чтобы понять основные проблемы, которые возникают при их написании и поддержке, и сформулировать несколько правил для их предотвращения.

    1. Unit-тесты нужно писать. Да, как бы банально это не звучало, но писать их нужно. Каждый кусок логики приложения должен быть протестирован, чтобы в будущем избежать проблем. А они могут возникнуть при изменении логики, рефакторинге, или даже при обновлении версии зависимых библиотек. И чем больше покрытие кода тестами, тем быстрее проблема будет обнаружена и исправлена.

    2. Это правило очень актуально для тех, кого заставляют покрывать код тестами, и оно звучит так: Тесты — это тоже код, и относиться к нему нужно как к рабочему коду. Это касается и нейминга переменных, и форматирования кода внутри теста, и, особенно, названий тестовых методов. Конечно, написание адекватного имени переменной занимает немного больше времени и ударов по клавиатуре, чем “int i = 0;”, но это повышает читабельность тестов и легкость их поддержки.
    Угадайте, что проверяет упавший тестовый метод?)
    image

    3. Третье правило

    настоящего джентльмена — следи за путями. Конечно, всегда неприятно увидеть при запуске тестов, вот такую ошибку:

    И даже не потому, что тебя зовут не Andrey, а потому что у тебя мак. И как же быть в такой ситуации, спросите вы? Ответ прост — Относительные пути. Вот пример —

    Лучше всего использовать Unix разделитель (/). Это и гораздо лаконичнее, и меньше шансов получить непредвиденную ошибку.

    4. Чаще используйте заглушки (моки) вместо реальных объектов. Моки — это здорово! Ими можно управлять так, как нужно в конкретном тесте. Но, конечно, не стоит забывать сбрасывать состояние заглушек перед каждым тестовым методом. Использование заглушек повышает автономность теста и его гибкость. Не нужно подгонять состояние системы для конкретного случая, а просто настроил заглушку на возвращение нужного значения при вызове определенного метода и все. Хочется проверить другую ситуацию — исправил возвращаемое значение на другое. Легко и просто. И самое главное, что состояние всей системы при этом не изменяется — она ничего не записывает на диск, не передает по сети, не пересчитывает массивы данных, не лезет в другие сервисы. Просто заглушка и возвращаемое значение.
    Для использования заглушек в тестах я использую фреймворк Mockito. С его помощью создавать заглушки очень просто. Вот например:

    Здесь создается мок объекта calendar и передается в объект calendarService. Далее моки инициализируются в методе setUp. Затем непосредственно внутри теста мок настраивается и тест проверяет isModern, если тип календаря разный или не задан вовсе. При этом не пришлось пересоздавать CalendarService, а создание моков и генерация возвращаемых значений заняло всего несколько строк.

    5. Пишите осмысленные сообщения на случай падения теста. Самое часто встречающееся сообщение, которое я видел, разбирая упавшие тесты на TeamCity — это

    Ну сразу же все понятно! Но бывает, что сообщение об ошибки все-таки есть, но пользы от него…

    А вот уже хорошее, но еще не идеальное, сообщение, в котором сразу описано, что проверялось

    Идеальным можно считать сообщение, которое не только показывает что мы проверяем, но и почему мы это ожидаем

    Но здесь показана достаточно простая проверка, а если нужно сравнить пару массивов с данными в разном порядке? Какое сообщение нужно написать здесь? Для этого я советую воспользоваться фреймворком AssertJ. По ссылке много простых и понятных примеров использования, после которых вам захочется воспользоваться этим фреймворком! AssertJ позволяет меньше задумываться о написании сообщения в случае ошибки, а также проверить кучу всего одной строкой, экономя место. Например проверка:

    Выдаст нам замечательное сообщение об ошибке:

    И все понятно — что случилось и по какой причине! Можно идти и исправлять ошибки.
    И еще раз — Адекватные сообщения в ошибках тестов экономят время и нервы тех, кто будет эти тесты разбирать. Возможно, это будете вы сами через год.

    6. Убирайте за собой мусор (нет, это не про запуск GarbageCollector-a). Создали файл, сделали запись в базу или дернули ручку создания пользователя? Не поленитесь и почистите за собой после теста. Файлы копятся, база обрастает кучей мусора и в системе появляются толпы фейковых пользователей. Старайтесь сохраняйте в чистоте не только своё рабочее место, но и рабочее окружение. UPD Как правильно указали в комментариях, этот пункт относится только к интеграционному тестированию.

    7. Проверьте, что тест запускается где-то еще, помимо вашей локальной машины. Если у вас есть сервер CI или какое-то другое место, где вы прогоняете тесты, проверьте, что тест запустился и там. Например, тесты на сервере CI запускаются из определенного пакета, а вы положили свой в другой пакет. Или тесты запускаются по определенному имени, например *UTest, а вы назвали свой класс TestUid. Или тесты запускаются по группам, а вы забыли проставить определенную группу для своего теста. Или… Можно придумать много случаев, когда свеженаписанный тест так ниразу и не запустится где-то кроме вашей локальной машины. И тогда пользы от него не так уж и много!

    В результате получился небольшой список правил хорошего тона при написании Unit-тестов. Надеюсь эта статья поможет ещё немного поднять культуру тестов и тестирования в вашей команде. Если вам есть чем дополнить мой список, добро пожаловать в комментарии.

    Wrike 80,40
    Wrike делает совместную работу над проектами проще
    Поделиться публикацией

    Вакансии компании Wrike

    Комментарии 223
    • +8
      Годно. Про Атомарность тестов ещё не мешало бы упомянуть. И листинги кода картинкой это сильный изврат в то время, когда Хабр уже давно умеет нормально вставлять и подсвечивать код.
      • 0
        А какие есть хорошие практики рефакторинга моков? Когда, например, метод, который мокаем, меняет возвращаемое значение (для примера — возвышает не количество секунд, прошедших с какого-то события, а UNIX время события — сигнатура не меняется, но смысл меняется). В этом случае придется искать все места, где используется мок класса, что сильно повышает когнитивную нагрузку
        • +1
          Я таких практик не знаю. Кажется вы правы, и придется искать вызовы этого метода у моков и руками менять возвращаемое значение.
          А вообще логичнее было бы в таком случае сделать новый метод, а не менять возвращаемое значение в старом.
          • +1
            Когда, например, метод, который мокаем, меняет возвращаемое значение (для примера — возвышает не количество секунд, прошедших с какого-то события, а UNIX время события — сигнатура не меняется, но смысл меняется).

            Это антипаттерн. В таких случаях лучше форсировать изменение сигнатуры:


            1. Завести специальный тип для даты-времени (в идеале)
            2. Добавить новый метод
            3. Пометить старый как deprecated (если больше не нужен)
            4. Исправить все предупреждения компилятора
            5. Выпилить старый метод (не раньше чем через релиз)
            • 0

              Использование в качестве моков анонимных классов не может помочь? Они должны быть более восприимчивы к такого рода рефакторингу, хоть и не во всем так удобны как моки.

              • 0
                Если вы в курсе правила Open/Closed и следуете ему, то такие ситуации будут редки.
              • +5
                Файлы копятся, база обрастает кучей мусора и в системе появляются толпы фейковых пользователей. Старайтесь сохраняйте в чистоте не только своё рабочее место, но и рабочее окружение.

                Неужели вы работаете с БД, файловой системой напрямую в своем тестируемом модуле? Непонятна ситуация, когда в базе появляются толпы фейковых юзеров. Если они появляются, значит вы тестируете модуль создания/регистрации пользователей, а не модуль для работы с базой данных. Почему тогда не использовать мок низкоуровнего модуля?
                • 0
                  Неужели вы работаете с БД

                  А почему бы и нет? Поднять прямо из теста H2, и в путь. Очень удобно, итог работы можно вынимать обычным селектом, что наиболее близко к боевой работе.
                  • 0
                    Я про работу с инфраструктурным уровнем не в тесте, а в самом тестируемом модуле. Если модуль регистрации должен отправлять нотификацию в виде email`а, то не думаю, что при запуске вашего теста должны уходить письма, скорее вы используете в своем модуле сервис из инфраструктуры, который при unit тестировании надо замокать и ожидать, что метод отправки будет вызван. Так же схема и для базы данных.
                    • 0
                      Для тестирования «близко к боевой работе» используются другие тесты. Unit тесты тестируют unit и только его в отрыве от всего остального. Разделяй и властвуй.
                  • +6
                    7 правил хорошего тона при написании Unit-тестов

                    Создали файл, сделали запись в базу или дернули ручку создания пользователя?

                    Что???
                    • –1
                      Если не заткнуть моками со всех сторон, то именно так все и происходит.
                      • +4
                        Тогда это не Unit тесты
                        • +2
                          Каюсь Допишу в статью, что этот пункт относится только к интеграционному тестированию.
                    • 0
                      Спасибо за пример с AssertJ
                      • 0
                        8) Не используйте тестируемый код (SUT) частично или полностью в самих тестах для вычисления «правильного» ответа насколько это возможно.
                        • +4
                          Пишите осмысленные сообщения на случай падения теста.

                          Для юнит-теста это лишнее. Необходимо и достаточно иметь название метода и результат (прошел-упал). Если для вашего теста этого мало, то ваш тест либо проверяет слишком много и должен быть разбит на несколько тестов, либо не является юнит-тестом и не относится к заявленной теме.


                          Чаще используйте заглушки (моки) вместо реальных объектов

                          А где про разницу между стабами и моками и правило "моков не должно быть больше одного"?


                          Создали файл, сделали запись в базу или дернули ручку создания пользователя?

                          Это не юнит-тесты по определению


                          При этом не пришлось пересоздавать CalendarService,

                          Это грубое нарушение автономности тестов, что для юнит-тестирования недопустимо.

                          • +3
                            > 4. Чаще используйте заглушки (моки) вместо реальных объектов.

                            Весьма спорно. Есть мнение, что моки/стабы надо использовать только в тех случаях, когда без них не обойтись, так как они ведут к резкому росту сложности тестового кода и снижению качества самих тестов.
                            • +1

                              Я бы добавил, что для сущностей и объектов-значений и разного рода моделей скорее не стоит использовать моки.
                              Для сервисов наоборот, инфраструктурных вещей наоборот, лучше мочить.

                              • 0
                                > Для сервисов наоборот, инфраструктурных вещей наоборот, лучше мочить.

                                Безусловно. Сложность настройки внешнего тестового окружения обычно не то что сравнима — превышает сложность настройки моков. А по-этому в данном случае моки вполне полезный инструмент — то есть это как раз тот случай, когда «не обойтись».
                              • 0
                                они ведут к резкому росту сложности тестового кода и снижению качества самих тестов

                                Мнение, агрументированное… другие мнением.
                                Между тем, о каком качестве юнит-тестов может идти речь, если тестируемый объект не будет изолирован от остального приложения?

                                • 0
                                  > Мнение, агрументированное… другие мнением.

                                  Да все абсолютно, кто когда-либо писал юнит-тесты, встречались с адом, когда настройка моков занимает 3/4 самих тестов. При этом без моков этого кода бы просто не было.

                                  > Между тем, о каком качестве юнит-тестов может идти речь, если тестируемый объект не будет изолирован от остального приложения?

                                  Назначение тестов — поиск ошибок. Количество ошибок, которые отловил ваш тест, деленное на затраты для написания теста — это и есть прямая оценка качества данного теста.

                                  Тесты с моками ловят строго меньше ошибок (так как снижают покрытие, для сравнимого покрытия тестов с моками требуется обычно в разы больше), при этом они ведут к более частому рефакторингу (а тест до и после рефакторинга — это разные тесты), и увеличивают затраты на написание теста (так как тесты становятся сложнее). Так что, да, повсеместное мокирование в итоге снижает качество тестов.
                                  • 0
                                    Да все абсолютно, кто когда-либо писал юнит-тесты, встречались с адом, когда настройка моков занимает 3/4 самих тестов. При этом без моков этого кода бы просто не было.

                                    Если у класса плохая тестируемость, это проблема не моков, а проектирования. Обычно один из двух вариантов: или тестируемый объект имеет более одной ответственности (и кучу зависимостей как результат) или не имеет определенной ответственности вообще (видимо, ваш случай — хорошо тестируемый класс разбит на несколько плохо тестируемых).
                                    Фейки в данном случае полезны, так как выявляют плохой запах кода.
                                    В любом случае, если ваш тест проверяет более одного объекта — это НЕ юнит-тест по определению.

                                    • +1
                                      > Если у класса плохая тестируемость, это проблема не моков, а проектирования.

                                      Да нет никакой такой проблемы. Если у вас есть пара десятков зависимостей — вам их надо настраивать. Тот факт, что вы раскидаете условные 100 строк кода по 10 методам — никак не отменяет того, что это сумме те же сто строк.

                                      > В любом случае, если ваш тест проверяет более одного объекта — это НЕ юнит-тест по определению.

                                      Тогда юнит-тесты в вашем определении — не нужны. Зачем использовать заведомо более плохой инструмент? В этом же нету смысла.
                                      • 0
                                        Если у вас есть пара десятков зависимостей

                                        Если у вас у одного класса есть пара десятков зависимостей — у него многовато ответственностей.

                                        • +1
                                          Если вы разобьете класс и снизите число зависимостей, то это никакого влияния на результат не окажет, т.к. эти зависимости просто будут устанавливаться _в других_ тестах. Вы перенесли код из одного места в другое. Да, это может в итоге привести к некоему упрощению (хоть и не всегда), но проблемы не решает — как куча лишнего кода была, так и осталась.

                                          Ну и, да, нет ничего хуже, чем портить архитектуру приложения ради того, чтобы оно было «тестируемей». Сам факт того, что это приходится делать, уже говорит о том, что что-=то пошло не так.
                                          • 0
                                            Если вы разобьете класс и снизите число зависимостей, то это никакого влияния на результат не окажет, т.к. эти зависимости просто будут устанавливаться в других тестах.

                                            Окажет. Проблема-то возникает только тогда, когда мы вынуждены для теста сетапить те зависимости, которые к тесту отношения не имеют. Если у вас везде соблюдается принцип единой ответственности, то зависимости, которые вы сетапите, (почти) всегда будут описаны как входные параметры теста — то есть вам все равно надо их сетапить.


                                            как куча лишнего кода была, так и осталась.

                                            Как вы делите код на "лишний" и "нелишний"?

                                            • 0
                                              > Как вы делите код на «лишний» и «нелишний»?

                                              Тот код, который не решает каких-то конкретных задач (добавляет новый функционал, улучшает качество архитектуры, увеличивает простоту поддержки) — лишний.

                                              > Окажет. Проблема-то возникает только тогда, когда мы вынуждены для теста сетапить те зависимости, которые к тесту отношения не имеют.

                                              Проблема возникает, когда много зависимостей. Единственный способ ощутимо снизить количество зависимостей — это просто перебором построить такой граф, чтобы их было минимум. Но перетасовывать функционал модулей, нарушая их семантику, ради снижения количества зависимостей, чтобы было проще потестировать, хотя можно это не делать — весьма странное решение.
                                              • 0
                                                Тот код, который не решает каких-то конкретный задач (добавляет новый функционал, улучшает качество архитектуры, увеличивает простоту поддержки) — лишний.

                                                Ну так моки упрощают поддержку тестов. По этому критерию они, очевидно, не лишние.


                                                Проблема возникает, когда много зависимостей.

                                                А много зависимостей (обычно) возникает тогда, когда много ответственностей. О чем и речь.


                                                Я в своем опыте пока не видел класса, у которого реально была бы одна ответственность и при этом много зависимостей. Исключение — фасады и роутеры, но их, будем честными, юнит-тестировать можно в последнюю очередь (а composition root вообще можно не юнит-тестировать).

                                                • 0
                                                  > Ну так моки упрощают поддержку тестов.

                                                  Каким образом? Надо тратить лишнее время на поддержку моков и их синхронизацию с реальным поведением зависимостей. Чем это проще?

                                                  > А много зависимостей (обычно) возникает тогда, когда много ответственностей. О чем и речь.

                                                  Вообще-то все наоборот. Количество зависимостей растет при декомпозиции. Чем меньше делает каждый отдельный класс — тем больше у него зависимостей, т.к. вместо выполнения работы самостоятельно, класс ее делегирует зависимости.

                                                  И, наоборот, чем больше ответственности — тем меньше зависимостей, с god-object в пределе, который делает все и у которого практически нет зависимостей.
                                                  • 0
                                                    Каким образом?

                                                    Таким, что в каждом тесте сетапится только то, что нужно для его выполнения.


                                                    Чем меньше делает каждый отдельный класс — тем больше у него зависимостей.

                                                    Это, простите, как? У класса, который не делает ничего, осмысленных зависимостей быть не может.

                                                    • 0
                                                      > Это, простите, как? У класса, который не делает ничего, осмысленных зависимостей быть не может.

                                                      Если он сам не делает ничего, то он делегирует кому-то всю полезную работу.

                                                      > Таким, что в каждом тесте сетапится только то, что нужно для его выполнения.

                                                      А в случае интеграционных — вообще обычно не сетапится. Сетап зависимостей для интеграционного теста — исключительная ситуация. Для сьюитов — ну да, бывает, хоть и не слишком часто.
                                                      • 0
                                                        Если он сам не делает ничего, то он делегирует кому-то всю полезную работу.

                                                        Нет, если он не делает ничего, то он и не делегирует. Делегирование — тоже работа (в программировании, по крайней мере).


                                                        А в случае интеграционных — вообще обычно не сетапится.

                                                        Это очень опасная иллюзия. Вы просто переиспользуете какой-то существующий сетап, со всеми опасностями антипаттерна shared fixture.


                                                        Сетап зависимостей для интеграционного теста — исключительная ситуация.

                                                        Ну да, БД с правильными данными, или мок этой БД — они сами собой возникают.

                                                        • 0
                                                          > Ну да, БД с правильными данными, или мок этой БД — они сами собой возникают.

                                                          Еще раз, _сетап для теста_. Глобальный сетап возникает не сам, он пишется. Иногда он правится под тест-сьюиты — и очень редко под конкретные тесты.
                                                          • 0
                                                            Глобальный сетап возникает не сам, он пишется.

                                                            И на его поддержку тоже нужны усилия.


                                                            и очень редко под конкретные тесты.

                                                            Значит, ваш сетап должен (заранее) содержать кейсы под все тесты. И чем это лучше "давайте зададим свой кейс в каждом тесте"?

                                                            • 0
                                                              > И на его поддержку тоже нужны усилия.

                                                              конечно же нужны. Но благодаря тому, что нет многократного дублирования, их на порядок меньше.

                                                              > Значит, ваш сетап должен (заранее) содержать кейсы под все тесты. И чем это лучше «давайте зададим свой кейс в каждом тесте»?

                                                              Да нет, конечно. Я понял проблему, у вас, видимо, поведение зависимых объектов существенно зависит от поведения зависимостей? Тогда, конечно, надо делать много сетапов. Если же это не так, вам одного сетапа хватит на много-много тестов.
                                                              • 0
                                                                Но благодаря тому, что нет многократного дублирования, их на порядок меньше.

                                                                Так не надо же (беспорядочно) дублировать код — тесты тоже прекрасно рефакторятся.


                                                                Я понял проблему, у вас, видимо, поведение зависимых объектов существенно зависит от поведения зависимостей?

                                                                У меня тестовая проверка зависит от поведения зависимостей (самый тривиальный пример — API, читающий объект. Очевидно, он зависит от того, что в хранилище).

                                                                • 0
                                                                  > У меня тестовая проверка зависит от поведения зависимостей

                                                                  Так если поведение метода не меняется из-за работы зависимостей, то вы и не сможете выделить тесткейз.

                                                                  > (самый тривиальный пример — API, читающий объект. Очевидно, он зависит от того, что в хранилище).

                                                                  Результат зависит, а поведение? Нам же требуются разные тесткейзы с разными данными в хранилище. Почему вы не можете написать все тесты с теми же данными?
                                                                  • 0
                                                                    Так если поведение метода не меняется из-за работы зависимостей, то вы и не сможете выделить тесткейз.

                                                                    "Поведение метода" всегда одинаковое — "правильно смапить".


                                                                    Результат зависит, а поведение?

                                                                    А поведение всегда одинаковое — "правильно смапить".


                                                                    Вот только критерии "правильно" — они сложные. Где-то объект плоский, где-то многоуровневый, где-то ссылки, где-то включения. Для всего этого в БД надо построить исходные данные.


                                                                    Тоже банальный пример: у объекта есть коллекция вложенных объектов (например, строчки в заказе). Мы должны проверить, что если в заказе нет ни одной строчки, возвращается именно пустая коллекция, а не null (ну нет у нас null-safety в языке). Вот, надо уже два заказа в БД завести.

                                                                    • 0
                                                                      > Где-то объект плоский, где-то многоуровневый, где-то ссылки, где-то включения. Для всего этого в БД надо построить исходные данные.

                                                                      Для всего этого достаточно одного набора тестовых данных. В котором будет и простой объект и многоуровневый, и что вам там надо еще, это-во-первых. Во-вторых — мапингом ваше «читающее апи» вообще заниматься не должно, это ответственность, как минимум, другого метода, как максимум — другого модуля, у которого данные для маппинга будут просто в аргументах. Вот в нем-то и будут тесты на логику маппинга.
                                                                      • 0
                                                                        Для всего этого достаточно одного набора тестовых данных. В котором будет и простой объект и многоуровневый, и что вам там надо еще, это-во-первых.

                                                                        Все равно там будет столько объектов, сколько у меня тестовых сценариев. И выгоды по сравнению с "объяви объект прямо в тесте" нет.


                                                                        Во-вторых — мапингом ваше «читающее апи» вообще заниматься не должно, это ответственность, как минимум, другого метода, как максимум — другого модуля, у которого данные для маппинга будут просто в аргументах.

                                                                        Просто считайте, что я тестирую не "читающее апи", а "маппящий метод". Аргументация не меняется.

                                                                        • 0
                                                                          > Все равно там будет столько объектов, сколько у меня тестовых сценариев. И выгоды по сравнению с «объяви объект прямо в тесте» нет.

                                                                          Конечно же, есть. Вы объявляете все это только один раз (а не в каждом тесте).

                                                                          > Просто считайте, что я тестирую не «читающее апи», а «маппящий метод».

                                                                          Мапящий метод в бд не лезет, он о ней не в курсе. Он принимает какие-то данные и возвращает смапленные данные. Пишется он одинаково, что с моками, что без моков, просто потому что зависимостей не имеет (по крайней мере от бд, остальные мы на данный момент не обсуждаем).
                                                                          • +1
                                                                            Вы объявляете все это только один раз (а не в каждом тесте).

                                                                            Еще раз, по буквам. В каждом тесте я объявляю тот объект, который ему нужен. Один объект на тест. Суммарно k объектов. Я не объявляю все остальные объекты они мне не интересны.


                                                                            В случае глобального сетапа я объявляю те же k объектов, но в одном месте. Кода ровно столько же.


                                                                            Мапящий метод в бд не лезет, он о ней не в курсе.

                                                                            Извините, это какая-то ваша декомпозиция кода, не имеющая никакого отношения к тому коду, который конкретно я тестирую. В моем случае мапящий модуль, как уже говорилось, принимает данные во внешнем представлении на вход, отдает данные во внутреннем представлении в выход, а метаинформацию о том, как именно делается маппинг, берет из хранилища. (типичный реквест-фильтр)

                                                                            • 0
                                                                              > Еще раз, по буквам. В каждом тесте я объявляю тот объект, который ему нужен. Один объект на тест. Суммарно k объектов. Я не объявляю все остальные объекты они мне не интересны.

                                                                              Давайте подведем некоторый итог:

                                                                              1. В вашем коде неким образом получается избегать большого количества зависимостей, не нарушая SRP, в моем — нет (например — у вас бд, бизнес-логика содержится в сервисах, их много, они разделены согласно доменной логике, какой-нибудь метод-двустрочник может использовать полдесятка разных зависимостей, как вы тут будете количество зависимостей снижать?)
                                                                              2. У вас есть всякий indirect output, я его по возможности избегаю и стараюсь тестировать при помощи черного ящика.

                                                                              Возможно, в _вашей_ ситуации юнит-тесты имеют смысл, в моей — очевидно, не совсем так.
                                                                              • 0
                                                                                какой-нибудь метод-двустрочник может использовать полдесятка разных зависимостей, как вы тут будете количество зависимостей снижать?

                                                                                Что-то не так с таким методом-двухстрочником. Выделять логику дальше.


                                                                                Возможно, в вашей ситуации юнит-тесты имеют смысл

                                                                                В моей и аналогичных, да. Это как раз те ситуации, на которые ориентированы (и которые порождают и порождаются) юнит-тесты.

                                                                                • 0
                                                                                  > Выделять логику дальше.

                                                                                  А некуда выделять. Все, что делает этот метод — дергает 5 других методов. Конечно, можно, например, разбить его так, чтобы было 2 метода по два вызова и один скомбинированный с тремя. Причем каждый из новых этих методов будет полностью бессмысленным, да и количество зависимостей тут не изменится.
                                                                                  • 0
                                                                                    Все, что делает этот метод — дергает 5 других методов.

                                                                                    И все пять из них нужны для выполнения одной ответственности? Значит, у вас типичный координатор. Координаторов в системе мало.

                                                                                    • 0
                                                                                      > И все пять из них нужны для выполнения одной ответственности?

                                                                                      Да, для вполне конкретной и определенной задачи.

                                                                                      > Координаторов в системе мало.

                                                                                      Ну вот в моей практике все строго наоборот.
                                                                              • 0
                                                                                > а метаинформацию о том, как именно делается маппинг, берет из хранилища.

                                                                                Ну вот у вас и нарушение SRP — вы в одном методе и в хранилище лезете, и какую-то логику мапинга реализуете. Кто еще этот мапинг разбирает и смотрит, как по нему, с-но, мапить?
                                                                                • 0
                                                                                  Ну вот у вас и нарушение SRP — вы в одном методе и в хранилище лезете

                                                                                  Нет, не нарушение. Я всего лишь вызываю _metadataProvider.GetMappingMetadataFor(someId), а за общение с "реальным" хранилищем отвечает провайдер.

                                                                                  • 0
                                                                                    > Нет, не нарушение.

                                                                                    С моей точки зрения — нарушение. У вас в одном методе смешана логика и доступ к хранилищу.
                                                                                    • 0

                                                                                      Если _metadataProvider.GetMappingMetadataFor(someId) — это доступ к хранилищу, то _mapper.Map(request, mappingMetadata) — это логика маппинга, тогда метод


                                                                                      Request Map(Request request)
                                                                                      {
                                                                                        return _mapper.Map(request, _metadataProvider.GetMappingMetadataFor(request.Type));
                                                                                      }

                                                                                      все равно имеет две ответственности, и так далее вверх по стеку.


                                                                                      (Вообще, для проверки SRP иногда удобнее считать "причины для изменения", а не "ответственности".)

                                                                                      • 0
                                                                                        Ну вот у вас метод _mapper.Map, его и тестируйте для проверки логики маппинга. Зачем вам при этом мокать _metadataProvider?
                                                                                        • 0

                                                                                          То есть метод Request Map(Request), приведенный мной выше, тестировать не надо?

                                                                                          • 0
                                                                                            Конечно, надо — достаточно одного единственного теста, который вытягивает некоторые данные из бд и применяет некоторый маппинг. Если получилось — метод работает правильно. В итоге вам одного тестового сетапа и достаточно (о чем я выше говорил). А если вы хотите проверить именно правильность мапинга (или правильность взаимодействия с БД) — вы проверяете маппер (или провайдер), потому что это их ответственность, а не ответственность Request Map(Request).

                                                                                            Можно сказать тут, что любой тест для Request Map(Request) будет автоматически интеграционным, потому что сам метод единственное что делает — это обеспечивает интеграцию между двумя модулями, это метод-клей. Если вы в этом методе изолируете зависимости — то вы просто ничего не протестируете, за отсутствием какой-либо логики в данном методе.
                                                                                            • 0
                                                                                              Конечно, надо — достаточно одного единственного теста, который вытягивает некоторые данные из бд и применяет некоторый маппинг.

                                                                                              Только вам для этого надо знать, что БД — это неявный вход для этого метода. А вы об этом, по вашему утверждению, знать не можете.


                                                                                              Если вы в этом методе изолируете зависимости — то вы просто ничего не протестируете, за отсутствием какой-либо логики в данном методе.

                                                                                              Вот только логика там есть даже в том рудиментарном варианте, который мной написан: из реквеста достается конкретное свойство, возвращается обработанный реквест, а не исходный.


                                                                                              А еще есть обработка ошибок, которую мы традиционно в примере пропустили, но которая есть — и за которую потом отрывают руки. Вот банальный пример: IMapper.Map ожидает, что переданный на вход маппинг — не null (повторюсь, я исхожу из того, что у нас язык без управления nullability; в противном случае замените все на Option[T]). А IMetadataProvider.GetMappingMetadataFor может вернуть null (он используется другими местами, которые на это опираются). Чья ответственность проверить этот null? Точно не маппера, он только guard на входе может поставить (и если мы эту ошибку прокинем вверх, получим нечитаемое сообщение). Значит, нам нужен либо (еще один) враппер вокруг IMetadataProvider.GetMappingMetadataFor, либо проверка внутри нашего SUT — и в любом случае этот кейс надо покрыть.

                                                                                              • 0
                                                                                                > Только вам для этого надо знать, что БД — это неявный вход для этого метода.

                                                                                                Как я могу этого не знать, если согласно спецификации этот метод использует мапинг из бд?

                                                                                                > Вот только логика там есть даже в том рудиментарном варианте, который мной написан: из реквеста достается конкретное свойство, возвращается обработанный реквест, а не исходный.

                                                                                                И один единственный тест эту логику проверит. Вы же выше речь вели о том, что вам надо много сетапов, т.к. разные мапинги. Но в данном случае валидность метода от того, какой мапинг лежит в бд, вообще никак не отличается, если вы и напишете кучу тестов с разными мапингами (которые будут возвращаться из моков), то если один тест прошел — остальные практически гарантированно пройдут. Зачем тогда эти тесты нужны, и почему бы не перенести их в тестирование маппера, где это будет проще (т.к. туда все что надо передается рпотсо аргументом, это всегда удобнее чем настраивать моки, согласитесь)?

                                                                                                > Значит, нам нужен либо (еще один) враппер вокруг IMetadataProvider.GetMappingMetadataFor

                                                                                                Вроде, вы сами ответили на свой вопрос?
                                                                                                • 0
                                                                                                  Как я могу этого не знать, если согласно спецификации этот метод использует мапинг из бд?

                                                                                                  Нет, согласно спецификации этот метод использует маппинг, предоставленный провайдером (если мы говорим о спецификации на компонент, а не на приложение). И это легко понять, если представить себе, что провайдер грузит маппинги из БД в память на старте приложения.


                                                                                                  Зачем тогда эти тесты нужны, и почему бы не перенести их в тестирование маппера, где это будет проще (т.к. туда все что надо передается рпотсо аргументом, это всегда удобнее чем настраивать моки, согласитесь)?

                                                                                                  Это если вы можете переписать все приложение таким образом, что у вас данные везде передаются аргументами — иными словами, перейдете на функциональную парадигму. Ну так да, известно, что в функциональной парадигме к зависимостям отношение другое.


                                                                                                  Но, еще раз обращу ваше внимание: в итоге от "не надо мокать зависимости" вы перешли к "давайте делать как можно меньше зависимостей в том коде, который нуждается в обширном тестировании".


                                                                                                  Вроде, вы сами ответили на свой вопрос?

                                                                                                  Там не было вопроса (кроме риторического), там был пойнт, что эти кейсы тоже надо тестировать (и они снова требуют знания indirect inputs).

                                                                                                  • 0
                                                                                                    > Нет, согласно спецификации этот метод использует маппинг, предоставленный провайдером

                                                                                                    Я не могу понять, откуда у вас спецификация знает про провайдер? Почему в спецификацию протекают детали реализации метода? Может быть, я вообще не буду использовать в этом методе указанный провайдер? Собственно, это и есть одна из основных претензий к мокам — они сильно завязаны на реализацию и заставляют часто переписывать тесты (когда реализация поменялась, а спецификация — нет).

                                                                                                    > Но, еще раз обращу ваше внимание: в итоге от «не надо мокать зависимости» вы перешли к «давайте делать как можно меньше зависимостей в том коде, который нуждается в обширном тестировании».

                                                                                                    Ну так вы тоже перешли от «надо мокать все зависимости» к «функциональные можно и не мокать» :)

                                                                                                    > Там не было вопроса (кроме риторического), там был пойнт, что эти кейсы тоже надо тестировать (и они снова требуют знания indirect inputs).

                                                                                                    Так если обработать ошибку и выкинуть нужное исключение — это ответственность обертки (или и вовсе провайдера, почему нет? он же с хранилищем взаимодействует), то почему надо делать эти тесты на Map? Тестируйте обертку.
                                                                                                    • 0
                                                                                                      Я не могу понять, откуда у вас спецификация знает про провайдер?

                                                                                                      Спецификация на компоненты знает компонентную структуру приложения (по крайней мере, в части тех компонентов, которые надо использовать).


                                                                                                      Может быть, я вообще не буду использовать в этом методе указанный провайдер?

                                                                                                      Архитектор против.


                                                                                                      Так если обработать ошибку и выкинуть нужное исключение — это ответственность обертки (или и вовсе провайдера, почему нет? он же с хранилищем взаимодействует), то почему надо делать эти тесты на Map? Тестируйте обертку.

                                                                                                      Вот только надо протестировать, что SUT использует обертку, а не провайдер, да же?


                                                                                                      (точнее, надо протестировать, что SUT в ответ на реквест, для которого не определен маппинг, бросает конкретную строго определенную ошибку, а вот остальное — детали реализации)

                                                                                                      • 0
                                                                                                        > Архитектор против.

                                                                                                        Я думаю, в таком ключе смысла обсуждать, что и как тестировать, нет, т.к. архитектор вам также скажет и моки, или не моки. Безусловно, что выбор оптимальной методологии тестирования будет зависеть от архитектуры, это факт самоочевидный. Если архитектура фиксируется неким «архитектором», то это и на способ тестирования автоматически наложит ограничения.

                                                                                                        > Вот только надо протестировать, что SUT использует обертку, а не провайдер, да же?

                                                                                                        SUT не сможет использовать провайдер, если не содержит зависимостей от провайдера :)
                                                                                                        • 0
                                                                                                          Безусловно, что выбор оптимальной методологии тестирования будет зависеть от архитектуры, это факт самоочевидный.

                                                                                                          Ура. Так вот, есть архитектуры, которые удобно и выгодно тестировать с моками.


                                                                                                          SUT не сможет использовать провайдер, если не содержит зависимостей от провайдера

                                                                                                          Муа-ха-ха. Надо протестировать, что он не получает их скрытым образом. Черный ящик же, да?

                                                                                                          • 0
                                                                                                            > Муа-ха-ха. Надо протестировать, что он не получает их скрытым образом.

                                                                                                            Это тестировать не надо. Это ограничение архитектуры :)
                                                                                                            • 0

                                                                                                              Ограничения архитектуры тоже надо тестировать (до тех пор пока они не гарантируются чем-то, что вам неподконтрольно, конечно).

                                                                                                              • 0
                                                                                                                > Ограничения архитектуры тоже надо тестировать (до тех пор пока они не гарантируются чем-то, что вам неподконтрольно, конечно).

                                                                                                                Есть же вполне себе языковые средства, с-но все ООП как раз про ограничение видимости и изоляции. Естественно, все подобные вещи _можно_ обойти, но если считать, что кто-то это зачем-то будет делать (не имея на то разумной причины) — то тут явно что-то не так. И наличие какой-то зависимости где-то, где она не нужна — будет наименьшей из проблем.
                                                                                                                • 0
                                                                                                                  Естественно, все подобные вещи можно обойти, но если считать, что кто-то это зачем-то будет делать (не имея на то разумной причины) — то тут явно что-то не так.

                                                                                                                  Проблема в том, что в достаточно большом проекте у разработчика всегда найдется "разумная причина" сделать не так, как от него ожидают. В том числе — нарушить изоляцию.

                                                                                                                  • 0
                                                                                                                    > Проблема в том, что в достаточно большом проекте у разработчика всегда найдется «разумная причина» сделать не так, как от него ожидают.

                                                                                                                    Если разумная причина есть (без кавычек), то это вполне можно сделать.
                                                                                                                    • 0

                                                                                                                      … вот только тест про это ничего не знает (черный ящик же), и все посыпалось.

                                                                                                                      • 0
                                                                                                                        > … вот только тест про это ничего не знает (черный ящик же), и все посыпалось.

                                                                                                                        Вроде, причина была разумной, а это предполагает, что должен получиться рабочий код, а не тот, что все сломает.
                                                                                                                        • 0

                                                                                                                          Он рабочий (в том смысле, что система в целом работает). Просто тесты упали.

                                                                                                                          • 0
                                                                                                                            > Он рабочий (в том смысле, что система в целом работает). Просто тесты упали.

                                                                                                                            Так это же плохо, если тесты падают, когда не надо?
                                                                                                                            • 0

                                                                                                                              Конечно, плохо. Осталось понять, как отделить "надо" от "не надо".

                                                                                                                              • 0
                                                                                                                                > «не надо»

                                                                                                                                Когда нет ошибок (работа программы соответствует функциональным требованиям).
                                                                                                                                • 0

                                                                                                                                  Люди, отвечающие за дизайн, в печали.

                                                                                                              • 0
                                                                                                                > Ура. Так вот, есть архитектуры, которые удобно и выгодно тестировать с моками.

                                                                                                                Да с этим вобщем-то никто и не спорил. Обычно под рекомендацию можно почти всегда построить ситуацию, в которой она неверна (или верна), максим в программировании практически не существует.
                                        • +1
                                          Да все абсолютно, кто когда-либо писал юнит-тесты, встречались с адом, когда настройка моков занимает 3/4 самих тестов.

                                          Все, кто когда-либо писал код, встречался с адом. Это повод код не писать?


                                          При этом без моков этого кода бы просто не было.

                                          А протестировать при этом все еще можно?

                                          • +1
                                            > А протестировать при этом все еще можно?

                                            А почему бы нельзя? Пишете тот же самый тест, что с моками, только без моков. В итоге затрат меньше (т.к. не надо тратить время на моки), а результат — лучше (т.к. поймано больше багов).
                                            • +1
                                              А почему бы нельзя? Пишете тот же самый тест, что с моками, только без моков.

                                              Вот у меня есть простенькая преобразовалка: вход — конверсия по метаданным из БД — выход. И штук сорок текст-кейсов вида "вход — метаданные — ожидаемый вход". Как мне это удобно сделать без моков?


                                              Заодно как мне проверить без моков, что мой код ведет себя верно, когда зависимость (а) отдает неверные данные (б) отдает ошибку (ц) зависает?


                                              а результат — лучше (т.к. поймано больше багов).

                                              А почему поймано больше багов?..

                                              • 0
                                                > вход — конверсия по метаданным из БД

                                                Давайте сразу определимся — внешнее окружение (бд, удаленные сервисы и т.д.), естественно следует мокировать, это попадает под «использовать только в тех случаях, когда без них не обойтись».

                                                > А почему поймано больше багов?..

                                                Простой пример, у вас есть один и тот же тест на некоторый модуль, вы его запускаете в двух форматах — либо заменив зависимости моками, либо не заменив. В первом случае выполняется только код непосредственно тестируемого модуля, а во втором случае — и код всех используемых зависимостей, то есть один интеграционный тест заменяет 1+количество_используемых_зависимостей юнит-тестов. Кроме того, вы просто можете забыть обновить моки, в итоге с реальными зависимостями тест падает (т.к. их поведение изменилось), а с моками — проходит. Естественно, может быть обратная ситуация — когда несколько багов, взаимонакладываясь друг на друга, в итоге дают правильное поведение — но это очень большая редкость по сравнению с предыдущими двумя пунктами
                                                • +1
                                                  Давайте сразу определимся — внешнее окружение (бд, удаленные сервисы и т.д.), естественно следует мокировать

                                                  Вот только преобразовалка смотрит не в БД, а в сервис, поставляющий метаданные в удобном ей формате. Мне мокировать БД или этот сервис? И почему?


                                                  И еще — локальные зависимости ошибаться не могут?


                                                  В первом случае выполняется только код непосредственно тестируемого модуля, а во втором случае — и код всех используемых зависимостей, то есть один интеграционный тест заменяет 1+количество_используемых_зависимостей юнит-тестов.

                                                  А вот теперь смотрите: у вас есть модули А и Б, оба зависят от Ц и Д. Мы решили, по вашей методике, сэкономить тесты, и написали только интеграционные тесты на А и Б (тем самым Ц и Д тестируются имплицитно). Мы поменяли Ц. Какие тесты нам надо запустить, чтобы быть уверенными, что мы ничего не сломали? А теперь представьте, что наш сосед написал модуль Г, зависящий от Ц, на который он решил тесты не писать, потому что "модуль тривиальный".


                                                  Или вот наоборот: модуль Х зависит от У. У, в свою очередь, зависит от внешних зависимостей З1, З2 и З3. Согласно вашему же правилу, эти зависимости надо мокировать. Значит, в вашем тесте на Х есть три мока (З1, З2, З3) вместо одного (У). Теперь наш сосед добавляет в У зависимость З4. Что происходит? Правильно, падают тесты на Х, хотя казалось бы.

                                                  • 0
                                                    > Мне мокировать БД или этот сервис? И почему?

                                                    Бд. Потому что затрат меньше, чем в случае использование тестовой бд. Если в вашем случае тестовая бд дает меньше затрат (что бывает редко, но вдруг) — ну тогда, конечно, можно и тестовую бд. Чем ближе тест к реальности — тем он, конечно же, лучше.

                                                    > И еще — локальные зависимости ошибаться не могут?

                                                    Могут, конечно же, но вероятность того, что два модуля согласованно ошибутся так, чтобы съесть ошибку, весьма мала.

                                                    > А вот теперь смотрите: у вас есть модули А и Б, оба зависят от Ц и Д. Мы решили, по вашей методике, сэкономить тесты, и написали только интеграционные тесты на А и Б (тем самым Ц и Д тестируются имплицитно).

                                                    Конечно же, мы не пишем тесты только на А и Б, мы пишем все те же самые тесты, что писали бы и в случае использования моков. Просто мы «искаробки» получаем некоторое дополнительное, как вы выразились, «имплицитное», тестирование зависимых модулей, что позволит поймать больше багов в этих модулях. То есть не «меньше тестов при том же результате», а «выше результат при тех же тестах».

                                                    > Значит, в вашем тесте на Х есть три мока (З1, З2, З3) вместо одного (У).

                                                    Так это глобальные моки. Они настраиваются раз и для всех тестов.
                                                    • +1
                                                      Бд. Потому что затрат меньше, чем в случае использование тестовой бд

                                                      Стоп-стоп. Вы сравниваете затраты с использованием тестовой БД, а я спрашиваю, мне мокать БД или сервис. Речи о тестовой БД не идет.


                                                      Могут, конечно же, но вероятность того, что два модуля согласованно ошибутся так, чтобы съесть ошибку, весьма мала.

                                                      Вы снова отвечаете не на тот вопрос. Мне надо проверить, что SUT корректно себя ведет, если его зависимость (внутренняя!) ведет себя некорректно. Как это сделать без мока?


                                                      Конечно же, мы не пишем тесты только на А и Б, мы пишем все те же самые тесты, что писали бы и в случае использования моков.

                                                      То есть вы предлагаете писать тесты на А, Б, Ц и Д?


                                                      Тогда у вас получается больше кода, а не меньше, потому что тесты те же, только еще и для А и Б надо засетапить все, что нужно для Ц и Д.


                                                      Так это глобальные моки. Они настраиваются раз и для всех тестов.

                                                      Так в разных тестах разное поведение нужно, вообще-то. Какие тут глобальные моки?


                                                      Хуже того, если у вас общий сетап, то он отнесен от теста, и прочитать, что же делает тест — и почему — становится намного сложнее.

                                                      • 0
                                                        > Стоп-стоп. Вы сравниваете затраты с использованием тестовой БД, а я спрашиваю, мне мокать БД или сервис.

                                                        Я ответил. Мокать БД. Просто сделал на всякий случай пояснение — если по каким-то исключительным причинам вы можете легко поднять и без проблем использовать полноценную тестовую БД — то мокать и вовсе ничего не надо (но это чисто умозрительная ситуация, понятно, что в реальности такое представить сложно).

                                                        > Мне надо проверить, что SUT корректно себя ведет, если его зависимость (внутренняя!) ведет себя некорректно. Как это сделать без мока?

                                                        Никак. А зачем решать бесполезные задачи?

                                                        > То есть вы предлагаете писать тесты на А, Б, Ц и Д?

                                                        Те же самые тесты, я же указал.

                                                        > Тогда у вас получается больше кода, а не меньше, потому что тесты те же, только еще и для А и Б надо засетапить все, что нужно для Ц и Д.

                                                        Сетапить вообще ничего не надо кроме глобального сетапа.

                                                        > Так в разных тестах разное поведение нужно, вообще-то. Какие тут глобальные моки?

                                                        Если надо подменить какую-то конкретную часть сетапа — ну не проблема, подменяйте. В любом случае, это обычно требует в десятки меньше кода, чем на полноценный сетап всех моков.
                                                        • +1
                                                          Я ответил. Мокать БД.

                                                          Так почему же не сервис?


                                                          Никак. А зачем решать бесполезные задачи?

                                                          То есть тоже нужен мок?


                                                          Сетапить вообще ничего не надо кроме глобального сетапа.

                                                          Глобальный сетап — зло.


                                                          Если надо подменить какую-то конкретную часть сетапа — ну не проблема, подменяйте.

                                                          Ну то есть нужно сделать целую отдельную инфраструктуру, которая будет позволять делать глобальный сетап (см. выше), и подменять любую его часть.


                                                          В любом случае, это обычно требует в десятки меньше кода, чем на полноценный сетап всех моков.

                                                          … если зависимостей мало, то и моков мало. А если моков мало, то их сетап — это ровно то же самое, что подмена в глобальном. Так что никакого "в десятки меньше кода".

                                                          • 0
                                                            > Так почему же не сервис?

                                                            Потому что лучше мокать только БД, чем и сервис и БД (вы же сам сервис тоже тестировать будете, это ваш код? или я неверно понял вопрос?)

                                                            > То есть тоже нужен мок?

                                                            Зачем?

                                                            > Глобальный сетап — зло.

                                                            В чем? Я вижу только добро — экономию при прочих равных.

                                                            > Ну то есть нужно сделать целую отдельную инфраструктуру, которая будет позволять делать глобальный сетап (см. выше), и подменять любую его часть.

                                                            Зачем для этого какая-то особая инфраструктура? Подмена куска сетапа не сложнее, чем его определение.

                                                            > … если зависимостей мало, то и моков мало. А если моков мало, то их сетап — это ровно то же самое, что подмена в глобальном. Так что никакого «в десятки меньше кода».

                                                            Но на практике их много, либо у вас у god-object'ы вместо классов.
                                                            • +1
                                                              Потому что лучше мокать только БД, чем и сервис и БД

                                                              А почему лучше?


                                                              Зачем?

                                                              Чтобы протестировать описанные кейсы.


                                                              В чем?

                                                              Антипаттерн shared fixture. Сетап не виден в тесте, возникают неявные зависимости между тестами, могут быть проблемы при конкурентном выполнении и так далее.


                                                              Зачем для этого какая-то особая инфраструктура? Подмена куска сетапа не сложнее, чем его определение.

                                                              Покажите пример, пожалуйста.


                                                              Но на практике их много, либо у вас у god-object'ы вместо классов.

                                                              Или нет. У меня на практике мало зависимостей (кроме тех классов, где я знаю, что нарушен SRP, и которые стоят в очереди на рефакторинг).

                                                              • 0
                                                                > А почему лучше?

                                                                Потому что нет кода, который не выполняет никакой задачи (мок сервиса в данном случае).

                                                                > Чтобы протестировать описанные кейсы.

                                                                Ну так тестируйте с моком БД.

                                                                > Антипаттерн shared fixture.

                                                                Почему это антипаттерн?

                                                                > Или нет. У меня на практике мало зависимостей (кроме тех классов, где я знаю, что нарушен SRP, и которые стоят в очереди на рефакторинг).

                                                                Если у вас весь весь функционал в куче, то, конечно, зависимостей мало. Но мы же о качественном коде говорим?

                                                                > Покажите пример, пожалуйста.

                                                                Пример чего? Использования операции присваивания? Или вызова методов фреймворка для создания моков?
                                                                • +1
                                                                  Потому что нет кода, который не выполняет никакой задачи (мок сервиса в данном случае).

                                                                  Почему же не выполняет? Он выполняет задачу "подать на вход тестируемому объекту ровно те данные, которые описаны в тесткейсе".


                                                                  Ну так тестируйте с моком БД.

                                                                  Мок БД не позволяет протестировать, как поведет себя SUT, зависящий от сервиса, зависящего от БД, при зависании кода сервиса.


                                                                  Почему это антипаттерн?

                                                                  В следующем предложении было написано. Ну и у Мезароса тоже.


                                                                  Если у вас весь весь функционал в куче, то, конечно, зависимостей мало. Но мы же о качественном коде говорим?

                                                                  Да, о качественном. И вот у качественного кода в моей практике мало зависимостей (кроме уже описанных исключений, которые все равно юнит-тестировать не обязательно).


                                                                  Пример чего?

                                                                  Пример того, как вы получаете SUT с глобальным сетапом, в котором заменено поведение одной зависимости.

                                                                  • 0
                                                                    > Он выполняет задачу «подать на вход тестируемому объекту ровно те данные, которые описаны в тесткейсе».

                                                                    Это аргументы метода и внешнее состояние (сервисов, бд, етц.). То, что возвращают те или иные зависимости, данными, конечно, не является, так как мы даже не знаем (и не должны) о том, какие зависимости данный метод тянет (черный ящик же).

                                                                    > В следующем предложении было написано. Ну и у Мезароса тоже.

                                                                    То, что там написано — по-просту неверно.

                                                                    > Да, о качественном. И вот у качественного кода в моей практике мало зависимостей

                                                                    Тогда либо у вас нарушается SRP, либо вам везет.

                                                                    > Пример того, как вы получаете SUT с глобальным сетапом, в котором заменено поведение одной зависимости.

                                                                    Я не понимаю что конкретно вы хотите. Чем этот код, по-вашему, должен отличаться от построения сетапа с нуля, кроме того, что часть сетапа оказывается опущена?
                                                                    • +1
                                                                      Это аргументы метода и внешнее состояние (сервисов, бд, етц.). То, что возвращают те или иные зависимости, данными, конечно, не является, так как мы даже не знаем (и не должны) о том, какие зависимости данный метод тянет (черный ящик же).

                                                                      А вот и нет. В тесткейсе описана доменная сущность (метаданные Х), а не состояние БД. Соответственно, если у меня есть сервис, который мне эту доменную сущность возвращает, мне проще как раз его замокать, чем думать, как же эта сущность отображается в БД.


                                                                      То, что там написано — по-просту неверно.

                                                                      Аргументируйте.


                                                                      Тогда либо у вас нарушается SRP, либо вам везет.

                                                                      Видимо, я очень везуч.


                                                                      Я не понимаю что конкретно вы хотите.

                                                                      Пример кода.

                                                                      • –1
                                                                        > А вот и нет. В тесткейсе описана доменная сущность (метаданные Х), а не состояние БД. С

                                                                        Если это доменная сущность, то она тогда в аргументах. В промежуточном слое ее быть не может, т.к. я просто не знаю ничего об этом промежуточном слое. Я знаю, что мой метод принимает определенные аргументы, лезет в бд, и что-то там возвращает. Я ничего не знаю о существовании (и особенностях работы) каких-либо промежуточных слоев.
                                                                        То есть если я знаю о том, что метод работает с доменной сущностью — то значит этот метод в базу не лезет, в базу за него полез (и построил потом доменную сущность) кто-то другой и в аргументах передал.

                                                                        > Пример кода.

                                                                        Вы можете его увидеть в описании любого фреймворка для моков.
                                                                        • 0
                                                                          Если это доменная сущность, то она тогда в аргументах.

                                                                          Нет, это indirect input.


                                                                          В промежуточном слое ее быть не может, т.к. я просто не знаю ничего об этом промежуточном слое. Я знаю, что мой метод принимает определенные аргументы, лезет в бд, и что-то там возвращает.

                                                                          Тоже нет. Метод принимает определенные аргументы, и, опираясь на определенные где-то метаданные, что-то возвращает. В тесткейсе метаданные сформулированы в виде доменной сущности, не ее представления в БД.


                                                                          Я ничего не знаю о существовании (и особенностях работы) каких-либо промежуточных слоев.

                                                                          Значит, вы тестируете не свой метод. Не мой случай.


                                                                          Вы можете его увидеть в описании любого фреймворка для моков.

                                                                          Фреймворки для моков

                                                                          • 0
                                                                            Фреймворки для моков

                                                                            Оу, плохо отвлекаться. Читать "фреймворки для моков редко показывают примеры глобальных повторно используемых сетапов". Я, по крайней мере, не видел ни одного. Можно ссылочку?

                                                                            • 0
                                                                              > Читать «фреймворки для моков редко показывают примеры глобальных повторно используемых сетапов».

                                                                              Я не могу понять, чем,, по-вашему, настройка глобального сетапа отличается от любого другого. Почему вы решили, что там есть какие-то особенности? Вызываются те же методы, с тем же результатом.
                                                                              • 0
                                                                                Вызываются те же методы, с тем же результатом.

                                                                                "Те же методы" в мок-фреймворках, которыми я пользуюсь, просто возвращают правильным образом сконфигуренные моки. А вот как мне сделать, чтобы SUT использовал зависимости, которые используют другие зависимости, которые где-то потом неизвестно где используют эти моки?

                                                                                • 0
                                                                                  > А вот как мне сделать, чтобы SUT использовал зависимости, которые используют другие зависимости, которые где-то потом неизвестно где используют эти моки?

                                                                                  Эм… Вам надо конфигурировать только те, последние моки. Точно так же, как вы все моки конфигурируете.
                                                                                  • 0

                                                                                    Моки надо не только конфигурировать (т.е., определять их поведение), но еще и передать пользователям (т.е., сказать, что их надо использовать).


                                                                                    Когда у меня моки конфигурятся под тест, я прямо в тесте создаю SUT и в качестве зависимостей передаю ему моки. А если у меня глобальный сетап, то мне нужно то ли взять откуда-то готовый SUT, то ли взять откуда-то зависимости, которые ему передать. Откуда?

                                                                                    • 0
                                                                                      > А если у меня глобальный сетап, то мне нужно то ли взять откуда-то готовый SUT, то ли взять откуда-то зависимости, которые ему передать. Откуда?

                                                                                      Ну а как вы это делаете обычно? Через IoC-контейнер, я полагаю. Зарегистрировать/заменить моки для теста в IoC-контейнере вы можете откуда угодно.
                                                                                      • 0
                                                                                        Ну а как вы это делаете обычно? Через IoC-контейнер, я полагаю.

                                                                                        В тестах? Конечно, нет: просто создаю SUT напрямую, передавая зависимости параметрами.


                                                                                        Зарегистрировать/заменить моки для теста в IoC-контейнере вы можете откуда угодно.

                                                                                        Вот этот IoC контейнер, со всей его настройкой, и есть "инфраструктура для глобального сетапа", о которой я говорил.

                                                                            • 0
                                                                              > Нет, это indirect input.

                                                                              Тогда я о ней не могу знать.

                                                                              > Значит, вы тестируете не свой метод.

                                                                              Нет, просто я не хочу привязывать тесты к реализации. Тесты проверяют спецификацию, спецификация не зависит от реализации.
                                                                              • 0
                                                                                Тогда я о ней не могу знать

                                                                                Тогда вы не можете протестировать.


                                                                                Нет, просто я не хочу привязывать тесты к реализации. Тесты проверяют спецификацию, спецификация не зависит от реализации.

                                                                                В спецификации написано "given mapping defined as follows", и дальше домен. Куда бы вы это ни записали — все равно будет привязка к реализации, просто в одном случае — к доменной модели, а в другом — к БД.

                                                                                • 0
                                                                                  > В спецификации написано «given mapping defined as follows»

                                                                                  Я буду передавать эти мапинги аргументом в данном случае.
                                                                                  • 0

                                                                                    Технические ограничения не позволяют (я не зря про request filter написал).

                                                                                    • 0
                                                                                      > Технические ограничения не позволяют (я не зря про request filter написал).

                                                                                      Это как может быть? Один метод достает мапинг из хранилища и возвращает, второй — принимает то, что вернул предыдущий, и применяет.
                                                                                      • 0

                                                                                        А вот так. SUT — это компонент, находящийся в request pipeline (например, WebAPI), у которого на входе запрос (и только запрос), и на выходе — тоже запрос (преобразованный как хочется). Соответственно, эти ваши два метода — это хорошо, но они окажутся внутри SUT.


                                                                                        (это кстати, иллюстрация к вашему "тестировать как черный ящик")

                                                                                        • 0
                                                                                          > Соответственно, эти ваши два метода — это хорошо, но они окажутся внутри SUT.

                                                                                          Если по смыслу они должны быть снаружи — то кто вам мешает их вынести?
                                                                                          • 0

                                                                                            Контракт SUT мне мешает. Используемый фреймворк требует, чтобы фильтры в конвеере имели сигнатуру Request ProcessRequest(Request) — соответственно, у SUT сигнатура строго такая же.


                                                                                            Мы, конечно, можем вынести оба этих метода в какой-то другой класс, но тестируем-то мы этот. Если мы остаемся в парадигме интеграционного тестирования/черного ящика, то для нас нет разницы, вынесли мы их или нет (именно потому, что это черный ящик, мы не знаем, что он вызывает). Если мы в парадигме юнит-теста/прозрачного ящика, то при тестировании такого SUT нам придется проверить, что он вызывает то, куда мы вынесли эти операции (то есть, использовать моки).


                                                                                            Собственно, когда у вас контракт SUT строго определен требованиями, и логика завязана на информацию, не поступающую во входных данных, вам придется иметь indirect inputs, вы никуда не можете от этого деться.

                                                                                            • 0
                                                                                              > Контракт SUT мне мешает.

                                                                                              Как он может мешать, если мы говорим о внутренней реализации функции? Сигнатура у нее та же самая, данные она принимает и возвращает те же. Просто логика мапинга выделена в метод, который мы и тестируем. Тестировать же надо логику, а не БД или работу моков.
                                                                                              • 0
                                                                                                Просто логика мапинга выделена в метод, который мы и тестируем.

                                                                                                Вот так взяли и заменили один SUT на другой. Не надо так, первый SUT остался непротестированным.

                                                                              • 0

                                                                                "Indirect input" Явное всегда лучше неявного в отрефакторном коде их быть не должно

                                                                                • 0

                                                                                  Если в отрефакторенном коде нет неявных входов и выходов, то в нем нет и зависимостей; а если в нем нет зависимостей, то он за пределами этого обсуждения.


                                                                                  (ну то есть за исключением "чистых" функциональных зависимостей, конечно, но их тоже на моей памяти не мокают, ибо незачем, а значит, тоже за пределами обсуждения)

                                                                                  • 0
                                                                                    > (ну то есть за исключением «чистых» функциональных зависимостей, конечно, но их тоже на моей памяти не мокают, ибо незачем, а значит, тоже за пределами обсуждения)

                                                                                    Когда я выше говорил про сервисы к БД с кучей зависимостей и методе-двустрочнике, который все что делает — это дергает пяток функций из них — то я как раз про функциональные зависимости и говорил. Оказывается, их «можно» и не мокать? Ну тогда в итоге кроме внешних сервисов и нечего мокать-то, как я и предложил изначально.
                                                                                    • 0
                                                                                      Когда я выше говорил про сервисы к БД с кучей зависимостей и методе-двустрочнике, который все что делает — это дергает пяток функций из них — то я как раз про функциональные зависимости и говорил.

                                                                                      А эти ваши зависимости — "чистые" функциональные? Никаких побочных эффектов и полная детерминистичность?

                                                                                      • 0
                                                                                        Там методы генерируют спецификации. Исполняются спецификации уже отдельно.
                                                                                        • 0

                                                                                          Я, если честно, уже запутался, что у вас что и что что генерирует, поэтому просто повторю свою мысль целиком: обычно нет смысла мокать те зависимости, которые ведут себя как чистая функция — не имеют побочных эффектов и строго детерминистичны (т.е. для одного и того же входа всегда дадут один и тот же выход).

                                                                                          • –1
                                                                                            Так мы в итоге и пришли к предлагаемому мной изначально варианту — мокать только внешние зависимости, т.к. все остальные — могут (и должны) быть функциональными.
                                                                                            • 0

                                                                                              Ну вот в моем опыте внутренних зависимостей с поведением чистой функции — подавляющее меньшинство (наверное, следствие ООП?). Поэтому ваш вариант получается неприменим.

                                                                                              • 0
                                                                                                > Ну вот в моем опыте внутренних зависимостей с поведением чистой функции — подавляющее меньшинство

                                                                                                Так это уже вопрос исключительно вашей архитектуры. Никто же вам не мешает максимально выделять логику в чистые функции.

                                                                                                Кроме того, не совсем понятно, почему чистые зависимости мокать не надо, а «грязные» — надо. Почему такое разделение?
                                                                                                • 0
                                                                                                  Так это уже вопрос исключительно вашей архитектуры. Никто же вам не мешает максимально выделять логику в чистые функции.

                                                                                                  "Мешает" используемая парадигма и принятые архитектурные соглашения.


                                                                                                  Кроме того, не совсем понятно, почему чистые зависимости мокать не надо, а «грязные» — надо. Почему такое разделение?

                                                                                                  Не "не надо", а незачем. Их поведение (если они покрыты тестами) предсказуемо, и они чаще всего не вносят собственный эффект в тест. Грубо говоря, никто же не мокает string.Split? (ну, я надеюсь)


                                                                                                  Когда я понимаю, что зависимость — не важно уже, грязная она или чистая, — мешает мне корректно сформулировать тесты, я ее мокаю. Так уж вышло, что — в моем опыте — чистые зависимости мокать обычно не приходится.

                                                                                                  • 0
                                                                                                    > Не «не надо», а незачем. Их поведение (если они покрыты тестами) предсказуемо, и они чаще всего не вносят собственный эффект в тест.

                                                                                                    Давайте немного уточним, вот есть метод, у него нет зависимостей. Теперь мы какой-то из аргументов перенесли в поле класса (сделав зависимостью). В каком случае эта зависимость будет «функциональной», а в каком — нет?
                                                                                                    • 0

                                                                                                      Ну то есть было void Do(x arg), стало void Do() и x _dependency? Первый и самый важный вопрос — а какого типа arg?

                                                                                                      • 0
                                                                                                        > Ну то есть было void Do(x arg), стало void Do() и x

                                                                                                        Ну не обязательно void, может и что-то другое возвращать.

                                                                                                        > Первый и самый важный вопрос — а какого типа arg?

                                                                                                        Вот я это и хочу узнать, при аргументах с какими свойствами (тип, то как мы работаем со значением внутри метода, еще какие особенности) зависимость будет функциональной, а при какой — не будет. Просто уточниться, чтобы друг друга верно понимать.
                                                                                                        • 0

                                                                                                          … а что вообще в вашем примере зависимость?


                                                                                                          Потому что изначально я подумал, что "зависимость" — это то, что было arg, а стало _dependency. И в этом случае то, зависимость ли оно, зависит (извините) не от того, где оно (в параметре или в филде), а от того, какого оно типа. Если это сервис, то оно всегда зависимость (не важно, как она вбрасывается), если это значение, то оно никогда не зависимость.


                                                                                                          Так что давайте на примере хотя бы двух компонентов:


                                                                                                          IMetadataProvider {Mapping GetMappingFor(string requestType)}
                                                                                                          IRequestMapper {Request MapRequest(Request request, ?)}

                                                                                                          Вопросительный знак оставлен как раз для того, чтобы вы выбрали, что же туда передается, и что является зависимостью чего, если является.


                                                                                                          IMetadataProvider.GetMappingFor, очевидно, не чистая зависимость.

                                                                                                          • 0
                                                                                                            > IMetadataProvider.GetMappingFor, очевидно, не чистая зависимость.

                                                                                                            Потому что он тянет значение из какого-то хранилища, верно (этого в типе нет, но по смыслу, вроде, ясно)? А если будет так — GetMappingFor возвращает не сам маппинг, а то, как его достать (ну пусть для простоты и конкретики, это просто строка с sql запросом, чисто для примера). Запускать полученные запросы будет какой-то третий сервис, естественно, на него добавиться зависимость (очевидно, нефункциональная) в маппер. Тогда зависимость на провайдер будет функциональной или нет?
                                                                                                            • 0
                                                                                                              Тогда зависимость на провайдер будет функциональной или нет?

                                                                                                              Если то, что возвращает провайдер, всегда зависит только и исключительно от того, что в него передано, провайдер, как зависимость, можно считать чистой функцией.

                                                                                                              • 0
                                                                                                                > Если то, что возвращает провайдер, всегда зависит только и исключительно от того, что в него передано, провайдер, как зависимость, можно считать чистой функцией.

                                                                                                                Я бы еще добавил, что это «что-то» надо суметь провалидировать внутренними средствами (а то если лямбду возвращать всегда — то у нас тоже получатся везде формально «чистые» функции, но от самой что ни на есть грязной грязи по факту это отличаться не будет, ведь вы не можете проверить свою лямбду, не запустив ее, со всей грязью).

                                                                                                                А по примеру — в итоге получается, что мокать вам придется только «запускатор запросов», то есть это, вобщем-то, и есть БД.
                                                                                                                • 0
                                                                                                                  А по примеру — в итоге получается, что мокать вам придется только «запускатор запросов», то есть это, вобщем-то, и есть БД.

                                                                                                                  ...если вам удастся построить систему так, что у вас все грязные эффекты находятся в паре модулей, то у вас получится хаскель которые легко замокать.

                                                                                                                  • 0
                                                                                                                    > то у вас получится хаскель

                                                                                                                    Хаскель, кстати, проблемы не решит, т.к. действия в ИО-монаде как раз нарушают мое примечание из предыдущего поста (они не валидируются). То есть, вы не можете проверить свое ИО, не запустив его, в итоге для валидации генерящих ИО ф-й вы вынуждены будете мокать зависимости (по вашему подходу с моками нефункциональных зависимостей).
                                                                                                                    • 0

                                                                                                                      У чисто функциональных зависимостей в хаскеле никакой монады IO нет и не будет по определению.

                                                                                                                      • 0
                                                                                                                        В хаскеле все зависимости функционально чистые. Грязных функций там в принципе не бывает, их нельзя написать :)
                                                                                                                        • 0
                                                                                                                          Грязных функций там в принципе не бывает, их нельзя написать

                                                                                                                          Да ладно?
                                                                                                                          А как же UnsafePerformIO?
                                                                                                                          Но и без таких фокусов монада IO позволяет сохранить язык чистым, но явно обозначив зависимость от состояния окружения, совместив "чистую" программу с "грязным" результатом выполнения.
                                                                                                                          Как результат в хаскеле вопрос с моками решается тривиально — там зависимость от внешнего мира без адских хаков не спрячешь.

                                                                                                                          • 0
                                                                                                                            > А как же UnsafePerformIO?

                                                                                                                            А это и не хаскель, unsafePerformIO ломает семантику. Это особенность конкретной реализации.

                                                                                                                            > совместив «чистую» программу с «грязным» результатом выполнения.

                                                                                                                            Нету в хаскеле никакого грязного результата выполнения. Когда вы в хаскеле возвращаете IO, то никаких сайд-эффектов не происходит, они происходят при запуске ИО, которое уже к хаскелю не относится, изнутри хаскеля запустить ИО невозможно.
                                                                                                                            • 0
                                                                                                                              А это и не хаскель, unsafePerformIO ломает семантику. Это особенность конкретной реализации.

                                                                                                                              Это GHC, стандарт хаскеля де-факто, про него говорить "особенности конкретной реализации" скорее вредно чем бесполезно.


                                                                                                                              они происходят при запуске ИО

                                                                                                                              Ну вот мы и пришли к тому с чего начали — IO оказывается отличным несмываемым маркером зависимости результата работы программы от внешних данных. У шарпа такого маркера нет.

                                                                                                                              • 0
                                                                                                                                > Это GHC

                                                                                                                                А это компилятор. У языка есть определенная семантика, unsafePErformIO в нее не входит. С точки зрения хаскеля эта ф-я вообще ничего не делает.

                                                                                                                                > Ну вот мы и пришли к тому с чего начали — IO оказывается отличным несмываемым маркером зависимости результата работы программы от внешних данных.

                                                                                                                                Так результат у программы один и тот же — одно и то же ИО. А вот результат ИО — уже другое дело. Но к программе на хаскеле этот результат отношения не имеет :)
                                                      • 0
                                                        Простой пример, у вас есть один и тот же тест на некоторый модуль, вы его запускаете в двух форматах — либо заменив зависимости моками, либо не заменив.

                                                        И каким образом у вас вариант с моками оказался больше?
                                                        С моками досточно имитировать непосредственные зависимости, без моков — надо построить все зависимости в графе. Похоже, вы нам что-то недоговариваете.

                                                        • 0
                                                          > И каким образом у вас вариант с моками оказался больше?

                                                          С моками вам надо настраивать моки, без моков — соответственно, не надо.

                                                          > С моками досточно имитировать непосредственные зависимости, без моков — надо построить все зависимости в графе.

                                                          Надо мокировать только что, что надо мокировать в итоге (какие-то внешние зависимости), а не все в графе. И делается это один раз. Под какие-то тесть-сьюиты могут вноситься, конечно, какие-то необходимые изменения, но в самих тестах уже ничего дополнительно делать практически никогда не надо. В итоге лишнего кода в разы меньше.
                                                • 0
                                                  Назначение тестов — поиск ошибок. Количество ошибок, которые отловил ваш тест, деленное на затраты для написания теста — это и есть прямая оценка качества данного теста

                                                  Например, у вас есть простой однострочный тест, который ловит абсолютно все ошибки (идеал по вашим же критериям). Вот только искать, где именно ошибка, вам придется самому и тест, несмотря на высочайшую "оценку качества", не сможет здесь помочь.

                                                  • 0
                                                    > Например, у вас есть простой однострочный тест, который ловит абсолютно все ошибки (идеал по вашим же критериям). Вот только искать, где именно ошибка, вам придется самому и тест, несмотря на высочайшую «оценку качества», не сможет здесь помочь.

                                                    Тест укажет, в чем состоит ошибка (какое именно требование нарушено), а если известно, в чем ошибка, то ее локализация (в тех рамках, в которых она может быть выполнена за счет изоляции тестов) — тривиальная задача, которая даже в самых сложных случаях решается за время порядка единиц минут, обычно же — секунды.

                                                    И, да, почти всегда лучше обнаружить две плохо локализованные ошибки, чем одну — хорошо локализованную.