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

    203,00

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

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

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

                                                      Это для теста, покрывающего несколько классов с 20 зависимостями в каждом? Позвольте вам не поверить.
                                                      Допустим, ваш тест показал, что сумма контракта считается неверно. За ее расчет отвечает около сотни классов. Тривиально?


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

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

                                                        0
                                                        > Допустим, ваш тест показал, что сумма контракта считается неверно. За ее расчет отвечает около сотни классов. Тривиально?

                                                        А юнит-тесты вам как помогут? Если какой-то из юнит-тестов падает на сломанной зависимости, то он и в интеграционном виде на ней упадет и вы увидите ошибку там же. Если же не падает — ну тогда в случае интеграционных тестов вы эту ошибку увидите, а в случае юнит-тестов — вообще нет, она будет пропущена.

                                                        > А еще лучше — обнаружить две хорошо локализованные ошибки, что наличие юнит-тестов и позволяет сделать.

                                                        Конечно, лучше. И еще лучше, если бы не надо было совершать кучу лишних телодвижений для моков. Но такую методологию тестирования еще не придумали. Либо мы пишем простые и качественные тесты, которые, однако, чуть менее точно локализуют ошибку, либо сложные и некачественные, которые локализуют ошибку лучше.
                                                0
                                                Тестирование только на моках может приводить к «тавтологическим тестам».
                                                Держал это в голове, но после прочтения habrahabr.ru/company/badoo/blog/336194 можно в терминах).
                                                  0

                                                  Тавтологичность тестов не зависит от использования моков.

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

                                                  3 правило. На *них-машине оба варианта путей приведут к ошибке. Внезапно, однако!
                                                  Если уж используется java, то, по-моему, будет уместным пользоваться File.separator или path.separator для формирования путей;
                                                  6 правило. Я думаю, будет уместным упомянуть про виртуалки/контейнеры с возможностью создания снимков ФС.

                                                    0

                                                    Да лучше везде использовать *ниховые разделители. Винда их вполне понимает.

                                                    +3
                                                    Тесты — это тоже код, и относиться к нему нужно как к рабочему коду.

                                                    То есть нужно писать тесты и на тесты тоже? :-)


                                                    image


                                                    И всё-равно оставили виндовые разделители путей :-D

                                                      +1
                                                      Тесты на тесты — это перебор.
                                                      Проверял только на винде и там действительно работало оба варианта. Сейчас проверил на маке и исправлю скрин. Спасибо за помощь.
                                                      0
                                                      1. Третье правило настоящего джентльмена — следи за путями.

                                                      Что за треш? Paths.get(".", "log", "service.log") позволит вам навсегда забыть о File.separator.

                                                        0
                                                        Спасибо за статью. Я с удовольствием посмеялся над пунктом три, и мне понравился последний пункт про запуск тестов не только локально.

                                                        Но в целом от такой статьи хотелось бы побольше примеров «из жизни» и, возможно, «вредных» примеров и антипаттернов для наглядности. Некоторые пункты показались довольно банальными.
                                                          –2
                                                          Как вам мое «тестирование» — syncProj — просто и элегантно? :)
                                                            +1

                                                            Отвратительно.

                                                              –1
                                                              Чем аргументируешь?
                                                                0

                                                                Тем, что (вообще) не понятно, как это работает и должно работать.


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

                                                                  –1
                                                                  Но думаю на эмоции можно отвечать эмоциями. :)

                                                                  Странные у вас комментарии. Т.е. скачать исходники проекта и запустить unit testing не умеем, но высказать эмоции умеем. Да… По меньшей мере странные… :)
                                                                    0

                                                                    Так я скачал и запустил. Вместо нормального зеленого списка тестов в раннере я получил низачем мне не нужное диалоговое окно. Мне страшно подумать, что будет, если я это в CI запущу.


                                                                    Так что нет, отвратительно.

                                                                      –1
                                                                      Тут дело тоже в требованиях к тестированию — моя изначальная цель была сделать не 5 ступенчатый шаттл, а простенький unit testing. Возможно что-то не закоммичено из-за чего тест не проходит, возможно у меня английский windows, а у тебя русский и regional settings не совпадают, и он плюется messageboxом в ответ — что мол будем делать с этим тестом.

                                                                      Если на работе я скажем коммичу изменения, то у меня и build компьютер проверяет что не работает, то с open source code проектом у меня нет build компьютера — все делается мануально.

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

                                                                      деньги + время + возможности влияют на требования, подход и код.

                                                                      и я считаю что прежде чем выражать свои эмоции надо бы понять почему это так сделано а не иначе.
                                                                        0
                                                                        Тут дело тоже в требованиях к тестированию — моя изначальная цель была сделать не 5 ступенчатый шаттл, а простенький unit testing.

                                                                        Ну так он уже есть в студии готовый. Бери и пользуйся.


                                                                        то с open source code проектом у меня нет build компьютер — все делается мануально.

                                                                        AppVeyor


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


                                                                        деньги + время + возможности влияют на требования, подход и код.

                                                                        Обычные юнит-тесты на банальном xUnit пишутся не дольше, чем вы писали свой собственный раннер, а денег не стоят нисколько.


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

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

                                                                          0
                                                                          > Ну так он уже есть в студии готовый. Бери и пользуйся.

                                                                          я видел много unit testов, и более менее представляю что это и с чем едят.
                                                                          Вы пишете unit testы. Я их записываю и повторяю («record & playback») — это быстрее чем написать их.

                                                                          И тут дело ещё не в том что написать

                                                                          assert( return == 25 )

                                                                          а в том что никому кроме программиста не понятно что такое 25 — тут идет заточка на описании ошибки, а также видимости всех параметров ошибки конечному пользователю или же пользователю API.
                                                                            0
                                                                            Я их записываю и повторяю («record & playback») — это быстрее чем написать их.

                                                                            Быстрее, но лучше ли?


                                                                            а в том что никому кроме программиста не понятно что такое 25

                                                                            Требований не существует?

                                                                              0
                                                                              > Быстрее, но лучше ли?

                                                                              Сэкономленное на тестировании время можно использовать на разработку новых фич? Да и refactorинг проще делать. Compare-yes, compare-yes, compare-yes.

                                                                              > Требований не существует?

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

                                                                              Я лично видел написанные функции, где вместо boolean превязывался код ошибки, он в конечном итоге он не использовался. (Правда без unit testa)
                                                                                0
                                                                                Да и refactorинг проще делать. Compare-yes, compare-yes, compare-yes.

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


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

                                                                                Ну так тесты — это и есть "отпечатывание всего текстом".


                                                                                Требования не всегда и не все покрывают.

                                                                                Тогда откуда вы знаете, что надо тестировать?

                                                                                  0
                                                                                  > Рефакторинг — это изменение структуры кода без изменения его поведения.

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

                                                                                  > Ну так тесты — это и есть «отпечатывание всего текстом».
                                                                                  Но заставляет задуматься что печатаешь что бы было понятно что функциональность делает.

                                                                                  Кстати — assert( result == 25 );
                                                                                  заменяется Console.WriteLine( result );
                                                                                  с последующим test result accept. Но естественно если напечаешь 25 — будет не понятно про что речь.

                                                                                  В итоге ты напечатаешь скажем так:
                                                                                  Console.WriteLine( "Port number: " + result );

                                                                                  И если в следующей итерации ты решишь поменять номер порта, то мой вариант проще и одобрить и понять что изменилось и почему.

                                                                                  > Тогда откуда вы знаете, что надо тестировать?

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

                                                                                    Это классическое определение. Если вы изменили поведение — это не рефакторинг.


                                                                                    Кстати — assert( result == 25 ); заменяется Console.WriteLine( result ); с последующим test result accept.

                                                                                    Конечно же, не заменяется.


                                                                                    Но естественно если напечаешь 25 — будет не понятно про что речь.

                                                                                    Ну так надо нормально переменные (и тесты) именовать.


                                                                                    И если в следующей итерации ты решишь поменять номер порта, то мой вариант проще и одобрить и понять что изменилось и почему.

                                                                                    О нет.


                                                                                    [Fact]
                                                                                    public void PortShouldBe25() => _sut.Port.Should().Be(25);

                                                                                    Вот это легко понять и поменять. И легко понять тестовый вывод (который будет "Test PortShouldBe25 failed: expected 25, got 20".


                                                                                    к чему клоните, не совсем понимаю.

                                                                                    К тому, что обычно необходимое поведение определяется требованиями.

                                                                                      0
                                                                                      Это классическое определение. Если вы изменили поведение — это не рефакторинг.


                                                                                      Да, вы кстати правы на wiki страничке так и написано. Но я теперь понял что мне не хватает термина refactoring with external changes.

                                                                                      [Fact]
                                                                                      public void PortShouldBe25() => _sut.Port.Should().Be(25);

                                                                                      Вот это легко понять и поменять. И легко понять тестовый вывод (который будет «Test PortShouldBe25 failed: expected 25, got 20».


                                                                                      Да, но фишка в том что бы пишете и тестовый код _sut.Port и результат тестирования Should().Be(25). Я пишу только тестовый код — т.е. делаю 50% меньше работы чем вы.

                                                                                        0
                                                                                        мне не хватает термина refactoring with external changes.

                                                                                        Чем это отличается от просто "доработки программы"?


                                                                                        Я пишу только тестовый код — т.е. делаю 50% меньше работы чем вы.

                                                                                        А если не писать тестовый код вообще, то будет на 100% меньше работы. Правда же, круто?


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


                                                                                        Как иначе вы предлагаете это сделать?

                                                                                          0
                                                                                          Чем это отличается от просто «доработки программы»?


                                                                                          В общем-то я хотел выразиться относительно большие изменения в программе — refactoring как раз подходит.

                                                                                          А если не писать тестовый код вообще, то будет на 100% меньше работы. Правда же, круто?


                                                                                          Да, до полного тестирования мне тоже ещё очень далеко. Дело в том что тестировать можно все связанное с файлами, базами данных, когда дело касается скажем 3d отрисовки или скорости выполнения, то это намного сложнее покрывать тестированием. Но syncProj позволяет тестирование — command line utility как никак.

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


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

                                                                                          Кстати — в syncProj идет сравнение не только по console output — там ещё идет сравнение по тому что сама программа генерирует. т.е. .sln (Solution file format) файлы и .vcxproj (xml, C++ project file format).

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

                                                                                            Рефакторинг — это очень конкретная вещь: изменение реализации без изменения поведения. Вы уж определитесь.


                                                                                            Когда я пишу тестирование, я сначала пишу что программа должна производить

                                                                                            Тогда вы тоже пишете "результат тестирования", и вашему "на 50% меньше" взяться неоткуда.


                                                                                            там ещё идет сравнение по тому что сама программа генерирует. т.е. .sln (Solution file format) файлы и .vcxproj (xml, C++ project file format).

                                                                                            И что? "Правильные" же все равно должны откуда-то взяться. А после того, как они есть, есть куча готовых модулей для юнит-тестирования, которые их сравнят.

                                                                                              0
                                                                                              И что? «Правильные» же все равно должны откуда-то взяться. А после того, как они есть, есть куча готовых модулей для юнит-тестирования, которые их сравнят.


                                                                                              Правильные результаты или результаты тестирования появляются тогда когда ты нажимаешь Yes в том Message boxе что ты получил и результаты спасаются отдельным файлом («одобрено программистом»).
                                                                                                0

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

                                                                                                  0
                                                                                                  Если это console output, то я, как программист, знаю что программа должна написать.

                                                                                                  Если это .sln или .vcxproj — то обычно я открываю проект Визуал Студией и проверяю что бы она не плювалась, а также при закрытии solution, Visual studio не пыталась бы спасти изменения. (Обычно такое просходит если .sln содержит ошинки, и Visual Studio пытается их исправить).
                                                                                                    0
                                                                                                    Если это console output, то я, как программист, знаю что программа должна написать.

                                                                                                    … и проверяете это глазами? Серьезно?


                                                                                                    Если это .sln или .vcxproj — то обычно я открываю проект Визуал Студией и проверяю что бы она не плювалась, а также при закрытии solution, Visual studio не пыталась бы спасти изменения.

                                                                                                    … и не сверяете с нужным набором файлов?


                                                                                                    Не, это плохое тестирование, и уж точно не автоматизированное. О чем изначально речь и шла.

                                                                                                      0
                                                                                                      … и проверяете это глазами? Серьезно?


                                                                                                      Да.
                                                                                                      Кстати можно сделать проще:

                                                                                                      Console.WriteLine( "Test " + result.ToString()


                                                                                                      Что даст Test True или Test False, можно проверить визуально тоже.
                                                                                                      Не, это плохое тестирование, и уж точно не автоматизированное. О чем изначально речь и шла.


                                                                                                      У меня была идея что автоматизировать это можно было бы просто без message box promptа — т.е. результат не совпадает — ошибка / майл, и так далее.
                                                                                                        0
                                                                                                        Да.

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


                                                                                                        У меня была идея что автоматизировать это можно было бы просто без message box promptа — т.е. результат не совпадает — ошибка / майл, и так далее.

                                                                                                        Серьезно, "была идея"? Это же все много раз реализовано.

                                                                                                          0
                                                                                                          Серьезно, «была идея»? Это же все много раз реализовано.


                                                                                                          Ну пока что не реализована, так как делал в свободное время, а не по работе. По работе ситуация с тестированием другая вообще.
                                                                                                            0

                                                                                                            Да у вас-то понятно, что не реализовано. Но есть же готовые решения.

                                                                                                              0
                                                                                                              Кстати, нашёл один random fail — уже профиксил:

                                                                                                              Revision: 130
                                                                                                              Author: tarmopikaro
                                                                                                              Date: 16. syyskuuta 2017 10:51:13
                                                                                                              Message:
                                                                                                              Bugfix: XmlSerializer was causing field reorder by itself, causing weird fails to appear
                                                                                                              — Modified: /ProjectTypes.cs
                                                                                                              Modified: /tests/ProjectsToCs/TestAllInOne/out_testproj.cs.accepted

                                                                                                              Удивительно почему XmlSerializer вообще задает fieldы случайным образом.

                                                                                                              Из следующего коммита — TestRunTime.cs — это тот тест что я написал, остальные 11 файлов сгенерированы и просто «одобрены» мною.

                                                                                                              Да, готовые есть, но если можно сделать проще — почему бы не сделать?
                                                                                                                0
                                                                                                                Да, готовые есть, но если можно сделать проще — почему бы не сделать?

                                                                                                                Потому что проще как раз взять готовое.

                                                                                                                  0
                                                                                                                  Мне все таки кажется что проще готовый заменить на мой — но тут это возможно мое субъективное мнение. Я думаю что если скажем писать unit test на библиотеку которая делать 3d подсчёты то наверное проще было бы использовать обычный unit test, но как только библиотека станет сложнее чем 2+2 — то уже проще перейдти на мой подход. Ну а для syncProj-утилиты проблема довольно специфическая — надо было бы проверять синтакс того что генерируется, можно сделать как это делается в premake5, но мне кажется это сложнее.

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

                                                                                                                  Возможно картинки он не сможет сравнивать, но это уже другой unit test домайн.
                                                                                                                    0
                                                                                                                    но как только библиотека станет сложнее чем 2+2 — то уже проще перейдти на мой подход.

                                                                                                                    Аргументируйте. Начать лучше с того, что же такое "ваш подход".


                                                                                                                    Ну а для syncProj-утилиты проблема довольно специфическая — надо было бы проверять синтакс того что генерируется

                                                                                                                    А что в этом специфического?

                                                                                                                      0
                                                                                                                      Хмм… Давайте начнем с начала.
                                                                                                                      Итак для того что бы запустить unit test

                                                                                                                      1) нам нужен некий unit test framework и начальный effort что бы его взять в использование

                                                                                                                      2) надо написать сами unit testы.

                                                                                                                      Я предполагаю что время ушедшее на развертку (1) пункта намного меньше чем (2). Т.е. если мы сделали (1) каким то образом, то он не съест у нас много времени в отличие от (2).

                                                                                                                      На этом этапе вы со мной согласны?
                                                                                                                        0

                                                                                                                        Время на п.1 меньше, чем п.2, только в том случае, если берется готовый и известный фреймворк. В этом случае есть готовый инструментарий (создать проект в студии — запустить студийными средствами/создать проект в студиии — добавить nuget — запустить студийными средствами; для подключения к CI есть готовые плагины и анализаторы), есть необходимая документация и примеры.

                                                                                                                          0
                                                                                                                          Ну мне кажется готовый framework или свой framework особо разницы не имеет? Ну т.е. (1) может съесть время, например на допиливание своего frameworkа, но и готовый framework тоже может съёсть время — например на конфигурацию и подключение к buildам.
                                                                                                                            0
                                                                                                                            Ну мне кажется готовый framework или свой framework особо разницы не имеет?

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


                                                                                                                            готовый framework тоже может съёсть время — например на конфигурацию и подключение к buildам.

                                                                                                                            Для всех мейнстрим-фреймворков это время меньше, чем время на написание того же для своего фреймворка.


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

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


                                                                                                                              Ну все при желании запиливается и документируется.
                                                                                                                              Думаю основой вопрос в каком направлении идти.

                                                                                                                              Для всех мейнстрим-фреймворков это время меньше, чем время на написание того же для своего фреймворка.


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

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

                                                                                                                                И сколько на это надо усилий?


                                                                                                                                Да, вот это мне нравится больше всего — для «всех». Я знаю что их вагон и маленькая тележка.

                                                                                                                                Для .net — три, из которых я один никогда в своей жизни не видел.


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

                                                                                                                                … а можно просто взять работающий и освоить минут за десять.


                                                                                                                                Собственно если вы говорите о мейнстрим-фрейворке, то можете дать линк на хорошую документацию, и как и что пишется?

                                                                                                                                https://xunit.github.io/docs/getting-started-desktop.html

                                                                                                                                  0
                                                                                                                                  … а можно просто взять работающий и освоить минут за десять.


                                                                                                                                  А за 5 можно и свой написать? :-)

                                                                                                                                  [Fact]
                                                                                                                                  public void PassingTest()
                                                                                                                                  {
                                                                                                                                  Assert.Equal(4, Add(2, 2));
                                                                                                                                  }


                                                                                                                                  Как я уже говорил — тут пишутся не только сам код, но и результат тестирования. а если я возьму более сложный пример — например 125374 + 832748 — будете на калькуляторе считать сколько получится? :-)
                                                                                                                                    0
                                                                                                                                    А за 5 можно и свой написать?

                                                                                                                                    С той же функциональностью? Нельзя.


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

                                                                                                                                    Ну да, это основа юнит-тестирования. А как, простите, иначе?..


                                                                                                                                    а если я возьму более сложный пример — например 125374 + 832748 — будете на калькуляторе считать сколько получится?

                                                                                                                                    Ну да, а в чем проблема? Или могу взять в экселе вбить, сделать колонку с суммами, выгрузить в csv, загрузить как теорию. Наконец, могу взять AutoFixture, генерить случайные, складывать и сравнивать.

                                                                                                                                      0
                                                                                                                                      Ну да, а в чем проблема? Или могу взять в экселе вбить, сделать колонку с суммами, выгрузить в csv, загрузить как теорию. Наконец, могу взять AutoFixture, генерить случайные, складывать и сравнивать.


                                                                                                                                      Хорошо. Я усложню задачу.

                                                                                                                                      sourceforge.net/p/syncproj/code/HEAD/tree/tests/CsToProjects/TestPerFileConfs

                                                                                                                                      для TestPerFileConfs.cs, я хочу проверить что при данной конфигурации код / скрипт сгенерирует те 12 *accepted* файлов что там лежат?

                                                                                                                                      Вобьешь каждую строчку Assert.Equal?
                                                                                                                                        0

                                                                                                                                        Во-первых, с хорошей вероятностью можно посмотреть на требования и понять, как их протестировать, не сравнивая 12 файлов как текст.


                                                                                                                                        Во-вторых, ничто не мешает прочитать референсные файлы и сравнить результат работы SUT с ними.

                                                                                                                                          0
                                                                                                                                          Вообще зачем я этот тест изменил — была изначально разница работы vs2010 и vs2013 — там xml format .vcxproj немного отличается — в vs2010, vs2012, нет тага <DebugInformationFormat>None</DebugInformationFormat>

                                                                                                                                          тут находится тот if который я покрыл тестированием

                                                                                                                                          и результат изменения можно просмотреть сравнив файлы out_TestWindows_symbols_off_vs2013.vcxproj.accepted и out_TestWindows_symbols_off_vs2010.vcxproj.accepted между собой.

                                                                                                                                          Во-вторых, ничто не мешает прочитать референсные файлы и сравнить результат работы SUT с ними.


                                                                                                                                          Да, но нужна логика для создания reference файлов?
                                                                                                                                          Или как вы думаете их создавать.

                                                                                                                                          Но вообще я понимаю что тестирование именно даного бага можно было бы сделать именно разбив оригинальный код помодульно, и протестировать конкретно каждый модуль в отдельности. Просто сейчас я имею один механизм тестирования, а так вместо одного я имел бы 3 или 4 разноуровневых unit testа.
                                                                                                                                          Как проще — не знаю — надо было бы пробовать оба метода. Но если честно одноуровневый unit test на данный момент меня устраивает.

                                                                                                                                          И да — тебя наверное интересует как определить результат тестирования — т.е. что я этим тест кайсом тестирую — и это не так ясно. Моя идея в том что я сам на момент разработки знаю где и что должно находится, а если кто и переберет эту утилиту, то можно по svn diff отслеживать что и в какую сторону именилось — а по коду выяснять, что и где не работает.
                                                                                                                                            0
                                                                                                                                            и результат изменения можно просмотреть сравнив файлы out_TestWindows_symbols_off_vs2013.vcxproj.accepted и out_TestWindows_symbols_off_vs2010.vcxproj.accepted между собой.

                                                                                                                                            А зачем их сравнивать? Если известно, что должно генериться с тегом или без тега, значит, надо проверять наличие и отсутствие тега в выводе.


                                                                                                                                            Да, но нужна логика для создания reference файлов? Или как вы думаете их создавать.

                                                                                                                                            Это же от требований зависит, а не от методики тестирования.


                                                                                                                                            Но если честно одноуровневый unit test на данный момент меня устраивает.

                                                                                                                                            … вот только это не unit test.


                                                                                                                                            Моя идея в том что я сам на момент разработки знаю где и что должно находится, а если кто и переберет эту утилиту, то можно по svn diff отслеживать что и в какую сторону именилось — а по коду выяснять, что и где не работает.

                                                                                                                                            А идея (одна из) тестов в том, чтобы по тестам можно было определить ожидаемое поведение SUT.

                                                                                                                                              0