“Хорошими манерами обладает тот,
кто наименьшее количество людей
ставит в неловкое положение.”
Дж. Свифт
Привет, коллеги! Сегодня я бы хотел поговорить о Unit-тестировании и некоторых “правилах” при их написании. Конечно, они неформальные и не обязательны к выполнению, но при их соблюдении всем будет приятно и легко читать и поддерживать тесты, которые вы написали. Мы в Wrike видели достаточно Unit-тестов, чтобы понять основные проблемы, которые возникают при их написании и поддержке, и сформулировать несколько правил для их предотвращения.
1. Unit-тесты нужно писать. Да, как бы банально это не звучало, но писать их нужно. Каждый кусок логики приложения должен быть протестирован, чтобы в будущем избежать проблем. А они могут возникнуть при изменении логики, рефакторинге, или даже при обновлении версии зависимых библиотек. И чем больше покрытие кода тестами, тем быстрее проблема будет обнаружена и исправлена.
2. Это правило очень актуально для тех, кого заставляют покрывать код тестами, и оно звучит так: Тесты — это тоже код, и относиться к нему нужно как к рабочему коду. Это касается и нейминга переменных, и форматирования кода внутри теста, и, особенно, названий тестовых методов. Конечно, написание адекватного имени переменной занимает немного больше времени и ударов по клавиатуре, чем “int i = 0;”, но это повышает читабельность тестов и легкость их поддержки.
Угадайте, что проверяет упавший тестовый метод?)
3. Третье правило
И даже не потому, что тебя зовут не Andrey, а потому что у тебя мак. И как же быть в такой ситуации, спросите вы? Ответ прост — Относительные пути. Вот пример —
Лучше всего использовать Unix разделитель (/). Это и гораздо лаконичнее, и меньше шансов получить непредвиденную ошибку.
4. Чаще используйте заглушки (моки) вместо реальных объектов. Моки — это здорово! Ими можно управлять так, как нужно в конкретном тесте. Но, конечно, не стоит забывать сбрасывать состояние заглушек перед каждым тестовым методом. Использование заглушек повышает автономность теста и его гибкость. Не нужно подгонять состояние системы для конкретного случая, а просто настроил заглушку на возвращение нужного значения при вызове определенного метода и все. Хочется проверить другую ситуацию — исправил возвращаемое значение на другое. Легко и просто. И самое главное, что состояние всей системы при этом не изменяется — она ничего не записывает на диск, не передает по сети, не пересчитывает массивы данных, не лезет в другие сервисы. Просто заглушка и возвращаемое значение.
Для использования заглушек в тестах я использую фреймворк Mockito. С его помощью создавать заглушки очень просто. Вот например:
Здесь создается мок объекта calendar и передается в объект calendarService. Далее моки инициализируются в методе setUp. Затем непосредственно внутри теста мок настраивается и тест проверяет isModern, если тип календаря разный или не задан вовсе. При этом не пришлось пересоздавать CalendarService, а создание моков и генерация возвращаемых значений заняло всего несколько строк.
5. Пишите осмысленные сообщения на случай падения теста. Самое часто встречающееся сообщение, которое я видел, разбирая упавшие тесты на TeamCity — это
Ну сразу же все понятно! Но бывает, что сообщение об ошибки все-таки есть, но пользы от него…
А вот уже хорошее, но еще не идеальное, сообщение, в котором сразу описано, что проверялось
Идеальным можно считать сообщение, которое не только показывает что мы проверяем, но и почему мы это ожидаем
Но здесь показана достаточно простая проверка, а если нужно сравнить пару массивов с данными в разном порядке? Какое сообщение нужно написать здесь? Для этого я советую воспользоваться фреймворком AssertJ. По ссылке много простых и понятных примеров использования, после которых вам захочется воспользоваться этим фреймворком! AssertJ позволяет меньше задумываться о написании сообщения в случае ошибки, а также проверить кучу всего одной строкой, экономя место. Например проверка:
Выдаст нам замечательное сообщение об ошибке:
И все понятно — что случилось и по какой причине! Можно идти и исправлять ошибки.
И еще раз — Адекватные сообщения в ошибках тестов экономят время и нервы тех, кто будет эти тесты разбирать. Возможно, это будете вы сами через год.
6. Убирайте за собой мусор (нет, это не про запуск GarbageCollector-a). Создали файл, сделали запись в базу или дернули ручку создания пользователя? Не поленитесь и почистите за собой после теста. Файлы копятся, база обрастает кучей мусора и в системе появляются толпы фейковых пользователей. Старайтесь сохраняйте в чистоте не только своё рабочее место, но и рабочее окружение. UPD Как правильно указали в комментариях, этот пункт относится только к интеграционному тестированию.
7. Проверьте, что тест запускается где-то еще, помимо вашей локальной машины. Если у вас есть сервер CI или какое-то другое место, где вы прогоняете тесты, проверьте, что тест запустился и там. Например, тесты на сервере CI запускаются из определенного пакета, а вы положили свой в другой пакет. Или тесты запускаются по определенному имени, например *UTest, а вы назвали свой класс TestUid. Или тесты запускаются по группам, а вы забыли проставить определенную группу для своего теста. Или… Можно придумать много случаев, когда свеженаписанный тест так ниразу и не запустится где-то кроме вашей локальной машины. И тогда пользы от него не так уж и много!
В результате получился небольшой список правил хорошего тона при написании Unit-тестов. Надеюсь эта статья поможет ещё немного поднять культуру тестов и тестирования в вашей команде. Если вам есть чем дополнить мой список, добро пожаловать в комментарии.
Комментарии (183)
drakmail
06.09.2017 10:26А какие есть хорошие практики рефакторинга моков? Когда, например, метод, который мокаем, меняет возвращаемое значение (для примера — возвышает не количество секунд, прошедших с какого-то события, а UNIX время события — сигнатура не меняется, но смысл меняется). В этом случае придется искать все места, где используется мок класса, что сильно повышает когнитивную нагрузку
at_wrike Автор
06.09.2017 10:49+1Я таких практик не знаю. Кажется вы правы, и придется искать вызовы этого метода у моков и руками менять возвращаемое значение.
А вообще логичнее было бы в таком случае сделать новый метод, а не менять возвращаемое значение в старом.
Bonart
06.09.2017 12:26+1Когда, например, метод, который мокаем, меняет возвращаемое значение (для примера — возвышает не количество секунд, прошедших с какого-то события, а UNIX время события — сигнатура не меняется, но смысл меняется).
Это антипаттерн. В таких случаях лучше форсировать изменение сигнатуры:
- Завести специальный тип для даты-времени (в идеале)
- Добавить новый метод
- Пометить старый как deprecated (если больше не нужен)
- Исправить все предупреждения компилятора
- Выпилить старый метод (не раньше чем через релиз)
samizdam
06.09.2017 13:11Использование в качестве моков анонимных классов не может помочь? Они должны быть более восприимчивы к такого рода рефакторингу, хоть и не во всем так удобны как моки.
vryashentsev
08.09.2017 06:13Если вы в курсе правила Open/Closed и следуете ему, то такие ситуации будут редки.
isxam
06.09.2017 10:34+5Файлы копятся, база обрастает кучей мусора и в системе появляются толпы фейковых пользователей. Старайтесь сохраняйте в чистоте не только своё рабочее место, но и рабочее окружение.
Неужели вы работаете с БД, файловой системой напрямую в своем тестируемом модуле? Непонятна ситуация, когда в базе появляются толпы фейковых юзеров. Если они появляются, значит вы тестируете модуль создания/регистрации пользователей, а не модуль для работы с базой данных. Почему тогда не использовать мок низкоуровнего модуля?AstarothAst
06.09.2017 11:50Неужели вы работаете с БД
А почему бы и нет? Поднять прямо из теста H2, и в путь. Очень удобно, итог работы можно вынимать обычным селектом, что наиболее близко к боевой работе.isxam
06.09.2017 11:59Я про работу с инфраструктурным уровнем не в тесте, а в самом тестируемом модуле. Если модуль регистрации должен отправлять нотификацию в виде email`а, то не думаю, что при запуске вашего теста должны уходить письма, скорее вы используете в своем модуле сервис из инфраструктуры, который при unit тестировании надо замокать и ожидать, что метод отправки будет вызван. Так же схема и для базы данных.
vryashentsev
08.09.2017 06:14Для тестирования «близко к боевой работе» используются другие тесты. Unit тесты тестируют unit и только его в отрыве от всего остального. Разделяй и властвуй.
voff
06.09.2017 11:52+67 правил хорошего тона при написании Unit-тестов
Создали файл, сделали запись в базу или дернули ручку создания пользователя?
Что???
unabl4
06.09.2017 12:038) Не используйте тестируемый код (SUT) частично или полностью в самих тестах для вычисления «правильного» ответа насколько это возможно.
Bonart
06.09.2017 12:35+4Пишите осмысленные сообщения на случай падения теста.
Для юнит-теста это лишнее. Необходимо и достаточно иметь название метода и результат (прошел-упал). Если для вашего теста этого мало, то ваш тест либо проверяет слишком много и должен быть разбит на несколько тестов, либо не является юнит-тестом и не относится к заявленной теме.
Чаще используйте заглушки (моки) вместо реальных объектов
А где про разницу между стабами и моками и правило "моков не должно быть больше одного"?
Создали файл, сделали запись в базу или дернули ручку создания пользователя?
Это не юнит-тесты по определению
При этом не пришлось пересоздавать CalendarService,
Это грубое нарушение автономности тестов, что для юнит-тестирования недопустимо.
Druu
06.09.2017 13:05+3> 4. Чаще используйте заглушки (моки) вместо реальных объектов.
Весьма спорно. Есть мнение, что моки/стабы надо использовать только в тех случаях, когда без них не обойтись, так как они ведут к резкому росту сложности тестового кода и снижению качества самих тестов.samizdam
06.09.2017 13:15+1Я бы добавил, что для сущностей и объектов-значений и разного рода моделей скорее не стоит использовать моки.
Для сервисов наоборот, инфраструктурных вещей наоборот, лучше мочить.Druu
06.09.2017 14:04> Для сервисов наоборот, инфраструктурных вещей наоборот, лучше мочить.
Безусловно. Сложность настройки внешнего тестового окружения обычно не то что сравнима — превышает сложность настройки моков. А по-этому в данном случае моки вполне полезный инструмент — то есть это как раз тот случай, когда «не обойтись».
Bonart
06.09.2017 13:42они ведут к резкому росту сложности тестового кода и снижению качества самих тестов
Мнение, агрументированное… другие мнением.
Между тем, о каком качестве юнит-тестов может идти речь, если тестируемый объект не будет изолирован от остального приложения?Druu
06.09.2017 14:02> Мнение, агрументированное… другие мнением.
Да все абсолютно, кто когда-либо писал юнит-тесты, встречались с адом, когда настройка моков занимает 3/4 самих тестов. При этом без моков этого кода бы просто не было.
> Между тем, о каком качестве юнит-тестов может идти речь, если тестируемый объект не будет изолирован от остального приложения?
Назначение тестов — поиск ошибок. Количество ошибок, которые отловил ваш тест, деленное на затраты для написания теста — это и есть прямая оценка качества данного теста.
Тесты с моками ловят строго меньше ошибок (так как снижают покрытие, для сравнимого покрытия тестов с моками требуется обычно в разы больше), при этом они ведут к более частому рефакторингу (а тест до и после рефакторинга — это разные тесты), и увеличивают затраты на написание теста (так как тесты становятся сложнее). Так что, да, повсеместное мокирование в итоге снижает качество тестов.Bonart
06.09.2017 14:19Да все абсолютно, кто когда-либо писал юнит-тесты, встречались с адом, когда настройка моков занимает 3/4 самих тестов. При этом без моков этого кода бы просто не было.
Если у класса плохая тестируемость, это проблема не моков, а проектирования. Обычно один из двух вариантов: или тестируемый объект имеет более одной ответственности (и кучу зависимостей как результат) или не имеет определенной ответственности вообще (видимо, ваш случай — хорошо тестируемый класс разбит на несколько плохо тестируемых).
Фейки в данном случае полезны, так как выявляют плохой запах кода.
В любом случае, если ваш тест проверяет более одного объекта — это НЕ юнит-тест по определению.Druu
06.09.2017 16:32+1> Если у класса плохая тестируемость, это проблема не моков, а проектирования.
Да нет никакой такой проблемы. Если у вас есть пара десятков зависимостей — вам их надо настраивать. Тот факт, что вы раскидаете условные 100 строк кода по 10 методам — никак не отменяет того, что это сумме те же сто строк.
> В любом случае, если ваш тест проверяет более одного объекта — это НЕ юнит-тест по определению.
Тогда юнит-тесты в вашем определении — не нужны. Зачем использовать заведомо более плохой инструмент? В этом же нету смысла.lair
06.09.2017 16:37Если у вас есть пара десятков зависимостей
Если у вас у одного класса есть пара десятков зависимостей — у него многовато ответственностей.
Druu
06.09.2017 16:50+1Если вы разобьете класс и снизите число зависимостей, то это никакого влияния на результат не окажет, т.к. эти зависимости просто будут устанавливаться _в других_ тестах. Вы перенесли код из одного места в другое. Да, это может в итоге привести к некоему упрощению (хоть и не всегда), но проблемы не решает — как куча лишнего кода была, так и осталась.
Ну и, да, нет ничего хуже, чем портить архитектуру приложения ради того, чтобы оно было «тестируемей». Сам факт того, что это приходится делать, уже говорит о том, что что-=то пошло не так.lair
06.09.2017 16:52Если вы разобьете класс и снизите число зависимостей, то это никакого влияния на результат не окажет, т.к. эти зависимости просто будут устанавливаться в других тестах.
Окажет. Проблема-то возникает только тогда, когда мы вынуждены для теста сетапить те зависимости, которые к тесту отношения не имеют. Если у вас везде соблюдается принцип единой ответственности, то зависимости, которые вы сетапите, (почти) всегда будут описаны как входные параметры теста — то есть вам все равно надо их сетапить.
как куча лишнего кода была, так и осталась.
Как вы делите код на "лишний" и "нелишний"?
Druu
06.09.2017 17:08> Как вы делите код на «лишний» и «нелишний»?
Тот код, который не решает каких-то конкретных задач (добавляет новый функционал, улучшает качество архитектуры, увеличивает простоту поддержки) — лишний.
> Окажет. Проблема-то возникает только тогда, когда мы вынуждены для теста сетапить те зависимости, которые к тесту отношения не имеют.
Проблема возникает, когда много зависимостей. Единственный способ ощутимо снизить количество зависимостей — это просто перебором построить такой граф, чтобы их было минимум. Но перетасовывать функционал модулей, нарушая их семантику, ради снижения количества зависимостей, чтобы было проще потестировать, хотя можно это не делать — весьма странное решение.lair
06.09.2017 17:11Тот код, который не решает каких-то конкретный задач (добавляет новый функционал, улучшает качество архитектуры, увеличивает простоту поддержки) — лишний.
Ну так моки упрощают поддержку тестов. По этому критерию они, очевидно, не лишние.
Проблема возникает, когда много зависимостей.
А много зависимостей (обычно) возникает тогда, когда много ответственностей. О чем и речь.
Я в своем опыте пока не видел класса, у которого реально была бы одна ответственность и при этом много зависимостей. Исключение — фасады и роутеры, но их, будем честными, юнит-тестировать можно в последнюю очередь (а composition root вообще можно не юнит-тестировать).
Druu
06.09.2017 17:16> Ну так моки упрощают поддержку тестов.
Каким образом? Надо тратить лишнее время на поддержку моков и их синхронизацию с реальным поведением зависимостей. Чем это проще?
> А много зависимостей (обычно) возникает тогда, когда много ответственностей. О чем и речь.
Вообще-то все наоборот. Количество зависимостей растет при декомпозиции. Чем меньше делает каждый отдельный класс — тем больше у него зависимостей, т.к. вместо выполнения работы самостоятельно, класс ее делегирует зависимости.
И, наоборот, чем больше ответственности — тем меньше зависимостей, с god-object в пределе, который делает все и у которого практически нет зависимостей.lair
06.09.2017 17:18Каким образом?
Таким, что в каждом тесте сетапится только то, что нужно для его выполнения.
Чем меньше делает каждый отдельный класс — тем больше у него зависимостей.
Это, простите, как? У класса, который не делает ничего, осмысленных зависимостей быть не может.
Druu
06.09.2017 17:23> Это, простите, как? У класса, который не делает ничего, осмысленных зависимостей быть не может.
Если он сам не делает ничего, то он делегирует кому-то всю полезную работу.
> Таким, что в каждом тесте сетапится только то, что нужно для его выполнения.
А в случае интеграционных — вообще обычно не сетапится. Сетап зависимостей для интеграционного теста — исключительная ситуация. Для сьюитов — ну да, бывает, хоть и не слишком часто.lair
06.09.2017 17:27Если он сам не делает ничего, то он делегирует кому-то всю полезную работу.
Нет, если он не делает ничего, то он и не делегирует. Делегирование — тоже работа (в программировании, по крайней мере).
А в случае интеграционных — вообще обычно не сетапится.
Это очень опасная иллюзия. Вы просто переиспользуете какой-то существующий сетап, со всеми опасностями антипаттерна shared fixture.
Сетап зависимостей для интеграционного теста — исключительная ситуация.
Ну да, БД с правильными данными, или мок этой БД — они сами собой возникают.
Druu
06.09.2017 17:30> Ну да, БД с правильными данными, или мок этой БД — они сами собой возникают.
Еще раз, _сетап для теста_. Глобальный сетап возникает не сам, он пишется. Иногда он правится под тест-сьюиты — и очень редко под конкретные тесты.lair
06.09.2017 17:33Глобальный сетап возникает не сам, он пишется.
И на его поддержку тоже нужны усилия.
и очень редко под конкретные тесты.
Значит, ваш сетап должен (заранее) содержать кейсы под все тесты. И чем это лучше "давайте зададим свой кейс в каждом тесте"?
Druu
06.09.2017 17:38> И на его поддержку тоже нужны усилия.
конечно же нужны. Но благодаря тому, что нет многократного дублирования, их на порядок меньше.
> Значит, ваш сетап должен (заранее) содержать кейсы под все тесты. И чем это лучше «давайте зададим свой кейс в каждом тесте»?
Да нет, конечно. Я понял проблему, у вас, видимо, поведение зависимых объектов существенно зависит от поведения зависимостей? Тогда, конечно, надо делать много сетапов. Если же это не так, вам одного сетапа хватит на много-много тестов.lair
06.09.2017 17:43Но благодаря тому, что нет многократного дублирования, их на порядок меньше.
Так не надо же (беспорядочно) дублировать код — тесты тоже прекрасно рефакторятся.
Я понял проблему, у вас, видимо, поведение зависимых объектов существенно зависит от поведения зависимостей?
У меня тестовая проверка зависит от поведения зависимостей (самый тривиальный пример — API, читающий объект. Очевидно, он зависит от того, что в хранилище).
Druu
06.09.2017 17:54> У меня тестовая проверка зависит от поведения зависимостей
Так если поведение метода не меняется из-за работы зависимостей, то вы и не сможете выделить тесткейз.
> (самый тривиальный пример — API, читающий объект. Очевидно, он зависит от того, что в хранилище).
Результат зависит, а поведение? Нам же требуются разные тесткейзы с разными данными в хранилище. Почему вы не можете написать все тесты с теми же данными?lair
06.09.2017 18:01Так если поведение метода не меняется из-за работы зависимостей, то вы и не сможете выделить тесткейз.
"Поведение метода" всегда одинаковое — "правильно смапить".
Результат зависит, а поведение?
А поведение всегда одинаковое — "правильно смапить".
Вот только критерии "правильно" — они сложные. Где-то объект плоский, где-то многоуровневый, где-то ссылки, где-то включения. Для всего этого в БД надо построить исходные данные.
Тоже банальный пример: у объекта есть коллекция вложенных объектов (например, строчки в заказе). Мы должны проверить, что если в заказе нет ни одной строчки, возвращается именно пустая коллекция, а не
null
(ну нет у насnull
-safety в языке). Вот, надо уже два заказа в БД завести.Druu
06.09.2017 18:05> Где-то объект плоский, где-то многоуровневый, где-то ссылки, где-то включения. Для всего этого в БД надо построить исходные данные.
Для всего этого достаточно одного набора тестовых данных. В котором будет и простой объект и многоуровневый, и что вам там надо еще, это-во-первых. Во-вторых — мапингом ваше «читающее апи» вообще заниматься не должно, это ответственность, как минимум, другого метода, как максимум — другого модуля, у которого данные для маппинга будут просто в аргументах. Вот в нем-то и будут тесты на логику маппинга.lair
06.09.2017 18:07Для всего этого достаточно одного набора тестовых данных. В котором будет и простой объект и многоуровневый, и что вам там надо еще, это-во-первых.
Все равно там будет столько объектов, сколько у меня тестовых сценариев. И выгоды по сравнению с "объяви объект прямо в тесте" нет.
Во-вторых — мапингом ваше «читающее апи» вообще заниматься не должно, это ответственность, как минимум, другого метода, как максимум — другого модуля, у которого данные для маппинга будут просто в аргументах.
Просто считайте, что я тестирую не "читающее апи", а "маппящий метод". Аргументация не меняется.
Druu
06.09.2017 18:10> Все равно там будет столько объектов, сколько у меня тестовых сценариев. И выгоды по сравнению с «объяви объект прямо в тесте» нет.
Конечно же, есть. Вы объявляете все это только один раз (а не в каждом тесте).
> Просто считайте, что я тестирую не «читающее апи», а «маппящий метод».
Мапящий метод в бд не лезет, он о ней не в курсе. Он принимает какие-то данные и возвращает смапленные данные. Пишется он одинаково, что с моками, что без моков, просто потому что зависимостей не имеет (по крайней мере от бд, остальные мы на данный момент не обсуждаем).lair
06.09.2017 18:25+1Вы объявляете все это только один раз (а не в каждом тесте).
Еще раз, по буквам. В каждом тесте я объявляю тот объект, который ему нужен. Один объект на тест. Суммарно k объектов. Я не объявляю все остальные объекты они мне не интересны.
В случае глобального сетапа я объявляю те же k объектов, но в одном месте. Кода ровно столько же.
Мапящий метод в бд не лезет, он о ней не в курсе.
Извините, это какая-то ваша декомпозиция кода, не имеющая никакого отношения к тому коду, который конкретно я тестирую. В моем случае мапящий модуль, как уже говорилось, принимает данные во внешнем представлении на вход, отдает данные во внутреннем представлении в выход, а метаинформацию о том, как именно делается маппинг, берет из хранилища. (типичный реквест-фильтр)
Druu
06.09.2017 18:47> Еще раз, по буквам. В каждом тесте я объявляю тот объект, который ему нужен. Один объект на тест. Суммарно k объектов. Я не объявляю все остальные объекты они мне не интересны.
Давайте подведем некоторый итог:
1. В вашем коде неким образом получается избегать большого количества зависимостей, не нарушая SRP, в моем — нет (например — у вас бд, бизнес-логика содержится в сервисах, их много, они разделены согласно доменной логике, какой-нибудь метод-двустрочник может использовать полдесятка разных зависимостей, как вы тут будете количество зависимостей снижать?)
2. У вас есть всякий indirect output, я его по возможности избегаю и стараюсь тестировать при помощи черного ящика.
Возможно, в _вашей_ ситуации юнит-тесты имеют смысл, в моей — очевидно, не совсем так.lair
06.09.2017 18:53какой-нибудь метод-двустрочник может использовать полдесятка разных зависимостей, как вы тут будете количество зависимостей снижать?
Что-то не так с таким методом-двухстрочником. Выделять логику дальше.
Возможно, в вашей ситуации юнит-тесты имеют смысл
В моей и аналогичных, да. Это как раз те ситуации, на которые ориентированы (и которые порождают и порождаются) юнит-тесты.
Druu
06.09.2017 18:56> Выделять логику дальше.
А некуда выделять. Все, что делает этот метод — дергает 5 других методов. Конечно, можно, например, разбить его так, чтобы было 2 метода по два вызова и один скомбинированный с тремя. Причем каждый из новых этих методов будет полностью бессмысленным, да и количество зависимостей тут не изменится.lair
06.09.2017 18:57Все, что делает этот метод — дергает 5 других методов.
И все пять из них нужны для выполнения одной ответственности? Значит, у вас типичный координатор. Координаторов в системе мало.
Druu
06.09.2017 19:03> И все пять из них нужны для выполнения одной ответственности?
Да, для вполне конкретной и определенной задачи.
> Координаторов в системе мало.
Ну вот в моей практике все строго наоборот.
Druu
06.09.2017 18:48> а метаинформацию о том, как именно делается маппинг, берет из хранилища.
Ну вот у вас и нарушение SRP — вы в одном методе и в хранилище лезете, и какую-то логику мапинга реализуете. Кто еще этот мапинг разбирает и смотрит, как по нему, с-но, мапить?lair
06.09.2017 18:56Ну вот у вас и нарушение SRP — вы в одном методе и в хранилище лезете
Нет, не нарушение. Я всего лишь вызываю
_metadataProvider.GetMappingMetadataFor(someId)
, а за общение с "реальным" хранилищем отвечает провайдер.Druu
07.09.2017 03:17> Нет, не нарушение.
С моей точки зрения — нарушение. У вас в одном методе смешана логика и доступ к хранилищу.lair
07.09.2017 10:58Если
_metadataProvider.GetMappingMetadataFor(someId)
— это доступ к хранилищу, то_mapper.Map(request, mappingMetadata)
— это логика маппинга, тогда метод
Request Map(Request request) { return _mapper.Map(request, _metadataProvider.GetMappingMetadataFor(request.Type)); }
все равно имеет две ответственности, и так далее вверх по стеку.
(Вообще, для проверки SRP иногда удобнее считать "причины для изменения", а не "ответственности".)
Druu
07.09.2017 15:23Ну вот у вас метод _mapper.Map, его и тестируйте для проверки логики маппинга. Зачем вам при этом мокать _metadataProvider?
lair
07.09.2017 15:25То есть метод
Request Map(Request)
, приведенный мной выше, тестировать не надо?Druu
08.09.2017 10:14Конечно, надо — достаточно одного единственного теста, который вытягивает некоторые данные из бд и применяет некоторый маппинг. Если получилось — метод работает правильно. В итоге вам одного тестового сетапа и достаточно (о чем я выше говорил). А если вы хотите проверить именно правильность мапинга (или правильность взаимодействия с БД) — вы проверяете маппер (или провайдер), потому что это их ответственность, а не ответственность Request Map(Request).
Можно сказать тут, что любой тест для Request Map(Request) будет автоматически интеграционным, потому что сам метод единственное что делает — это обеспечивает интеграцию между двумя модулями, это метод-клей. Если вы в этом методе изолируете зависимости — то вы просто ничего не протестируете, за отсутствием какой-либо логики в данном методе.lair
08.09.2017 11:21Конечно, надо — достаточно одного единственного теста, который вытягивает некоторые данные из бд и применяет некоторый маппинг.
Только вам для этого надо знать, что БД — это неявный вход для этого метода. А вы об этом, по вашему утверждению, знать не можете.
Если вы в этом методе изолируете зависимости — то вы просто ничего не протестируете, за отсутствием какой-либо логики в данном методе.
Вот только логика там есть даже в том рудиментарном варианте, который мной написан: из реквеста достается конкретное свойство, возвращается обработанный реквест, а не исходный.
А еще есть обработка ошибок, которую мы традиционно в примере пропустили, но которая есть — и за которую потом отрывают руки. Вот банальный пример:
IMapper.Map
ожидает, что переданный на вход маппинг — неnull
(повторюсь, я исхожу из того, что у нас язык без управления nullability; в противном случае замените все наOption[T]
). АIMetadataProvider.GetMappingMetadataFor
может вернутьnull
(он используется другими местами, которые на это опираются). Чья ответственность проверить этотnull
? Точно не маппера, он только guard на входе может поставить (и если мы эту ошибку прокинем вверх, получим нечитаемое сообщение). Значит, нам нужен либо (еще один) враппер вокругIMetadataProvider.GetMappingMetadataFor
, либо проверка внутри нашего SUT — и в любом случае этот кейс надо покрыть.Druu
08.09.2017 11:28> Только вам для этого надо знать, что БД — это неявный вход для этого метода.
Как я могу этого не знать, если согласно спецификации этот метод использует мапинг из бд?
> Вот только логика там есть даже в том рудиментарном варианте, который мной написан: из реквеста достается конкретное свойство, возвращается обработанный реквест, а не исходный.
И один единственный тест эту логику проверит. Вы же выше речь вели о том, что вам надо много сетапов, т.к. разные мапинги. Но в данном случае валидность метода от того, какой мапинг лежит в бд, вообще никак не отличается, если вы и напишете кучу тестов с разными мапингами (которые будут возвращаться из моков), то если один тест прошел — остальные практически гарантированно пройдут. Зачем тогда эти тесты нужны, и почему бы не перенести их в тестирование маппера, где это будет проще (т.к. туда все что надо передается рпотсо аргументом, это всегда удобнее чем настраивать моки, согласитесь)?
> Значит, нам нужен либо (еще один) враппер вокруг IMetadataProvider.GetMappingMetadataFor
Вроде, вы сами ответили на свой вопрос?lair
08.09.2017 11:35Как я могу этого не знать, если согласно спецификации этот метод использует мапинг из бд?
Нет, согласно спецификации этот метод использует маппинг, предоставленный провайдером (если мы говорим о спецификации на компонент, а не на приложение). И это легко понять, если представить себе, что провайдер грузит маппинги из БД в память на старте приложения.
Зачем тогда эти тесты нужны, и почему бы не перенести их в тестирование маппера, где это будет проще (т.к. туда все что надо передается рпотсо аргументом, это всегда удобнее чем настраивать моки, согласитесь)?
Это если вы можете переписать все приложение таким образом, что у вас данные везде передаются аргументами — иными словами, перейдете на функциональную парадигму. Ну так да, известно, что в функциональной парадигме к зависимостям отношение другое.
Но, еще раз обращу ваше внимание: в итоге от "не надо мокать зависимости" вы перешли к "давайте делать как можно меньше зависимостей в том коде, который нуждается в обширном тестировании".
Вроде, вы сами ответили на свой вопрос?
Там не было вопроса (кроме риторического), там был пойнт, что эти кейсы тоже надо тестировать (и они снова требуют знания indirect inputs).
Druu
08.09.2017 11:48> Нет, согласно спецификации этот метод использует маппинг, предоставленный провайдером
Я не могу понять, откуда у вас спецификация знает про провайдер? Почему в спецификацию протекают детали реализации метода? Может быть, я вообще не буду использовать в этом методе указанный провайдер? Собственно, это и есть одна из основных претензий к мокам — они сильно завязаны на реализацию и заставляют часто переписывать тесты (когда реализация поменялась, а спецификация — нет).
> Но, еще раз обращу ваше внимание: в итоге от «не надо мокать зависимости» вы перешли к «давайте делать как можно меньше зависимостей в том коде, который нуждается в обширном тестировании».
Ну так вы тоже перешли от «надо мокать все зависимости» к «функциональные можно и не мокать» :)
> Там не было вопроса (кроме риторического), там был пойнт, что эти кейсы тоже надо тестировать (и они снова требуют знания indirect inputs).
Так если обработать ошибку и выкинуть нужное исключение — это ответственность обертки (или и вовсе провайдера, почему нет? он же с хранилищем взаимодействует), то почему надо делать эти тесты на Map? Тестируйте обертку.lair
08.09.2017 11:51Я не могу понять, откуда у вас спецификация знает про провайдер?
Спецификация на компоненты знает компонентную структуру приложения (по крайней мере, в части тех компонентов, которые надо использовать).
Может быть, я вообще не буду использовать в этом методе указанный провайдер?
Архитектор против.
Так если обработать ошибку и выкинуть нужное исключение — это ответственность обертки (или и вовсе провайдера, почему нет? он же с хранилищем взаимодействует), то почему надо делать эти тесты на Map? Тестируйте обертку.
Вот только надо протестировать, что SUT использует обертку, а не провайдер, да же?
(точнее, надо протестировать, что SUT в ответ на реквест, для которого не определен маппинг, бросает конкретную строго определенную ошибку, а вот остальное — детали реализации)
Druu
08.09.2017 11:57> Архитектор против.
Я думаю, в таком ключе смысла обсуждать, что и как тестировать, нет, т.к. архитектор вам также скажет и моки, или не моки. Безусловно, что выбор оптимальной методологии тестирования будет зависеть от архитектуры, это факт самоочевидный. Если архитектура фиксируется неким «архитектором», то это и на способ тестирования автоматически наложит ограничения.
> Вот только надо протестировать, что SUT использует обертку, а не провайдер, да же?
SUT не сможет использовать провайдер, если не содержит зависимостей от провайдера :)lair
08.09.2017 12:02Безусловно, что выбор оптимальной методологии тестирования будет зависеть от архитектуры, это факт самоочевидный.
Ура. Так вот, есть архитектуры, которые удобно и выгодно тестировать с моками.
SUT не сможет использовать провайдер, если не содержит зависимостей от провайдера
Муа-ха-ха. Надо протестировать, что он не получает их скрытым образом. Черный ящик же, да?
Druu
08.09.2017 12:15> Муа-ха-ха. Надо протестировать, что он не получает их скрытым образом.
Это тестировать не надо. Это ограничение архитектуры :)lair
08.09.2017 12:17Ограничения архитектуры тоже надо тестировать (до тех пор пока они не гарантируются чем-то, что вам неподконтрольно, конечно).
Druu
08.09.2017 12:20> Ограничения архитектуры тоже надо тестировать (до тех пор пока они не гарантируются чем-то, что вам неподконтрольно, конечно).
Есть же вполне себе языковые средства, с-но все ООП как раз про ограничение видимости и изоляции. Естественно, все подобные вещи _можно_ обойти, но если считать, что кто-то это зачем-то будет делать (не имея на то разумной причины) — то тут явно что-то не так. И наличие какой-то зависимости где-то, где она не нужна — будет наименьшей из проблем.lair
08.09.2017 12:21Естественно, все подобные вещи можно обойти, но если считать, что кто-то это зачем-то будет делать (не имея на то разумной причины) — то тут явно что-то не так.
Проблема в том, что в достаточно большом проекте у разработчика всегда найдется "разумная причина" сделать не так, как от него ожидают. В том числе — нарушить изоляцию.
Druu
08.09.2017 12:24> Проблема в том, что в достаточно большом проекте у разработчика всегда найдется «разумная причина» сделать не так, как от него ожидают.
Если разумная причина есть (без кавычек), то это вполне можно сделать.lair
08.09.2017 12:25… вот только тест про это ничего не знает (черный ящик же), и все посыпалось.
Druu
08.09.2017 12:30> … вот только тест про это ничего не знает (черный ящик же), и все посыпалось.
Вроде, причина была разумной, а это предполагает, что должен получиться рабочий код, а не тот, что все сломает.
Druu
08.09.2017 12:18> Ура. Так вот, есть архитектуры, которые удобно и выгодно тестировать с моками.
Да с этим вобщем-то никто и не спорил. Обычно под рекомендацию можно почти всегда построить ситуацию, в которой она неверна (или верна), максим в программировании практически не существует.
lair
06.09.2017 14:27+1Да все абсолютно, кто когда-либо писал юнит-тесты, встречались с адом, когда настройка моков занимает 3/4 самих тестов.
Все, кто когда-либо писал код, встречался с адом. Это повод код не писать?
При этом без моков этого кода бы просто не было.
А протестировать при этом все еще можно?
Druu
06.09.2017 16:32+1> А протестировать при этом все еще можно?
А почему бы нельзя? Пишете тот же самый тест, что с моками, только без моков. В итоге затрат меньше (т.к. не надо тратить время на моки), а результат — лучше (т.к. поймано больше багов).lair
06.09.2017 16:36+1А почему бы нельзя? Пишете тот же самый тест, что с моками, только без моков.
Вот у меня есть простенькая преобразовалка: вход — конверсия по метаданным из БД — выход. И штук сорок текст-кейсов вида "вход — метаданные — ожидаемый вход". Как мне это удобно сделать без моков?
Заодно как мне проверить без моков, что мой код ведет себя верно, когда зависимость (а) отдает неверные данные (б) отдает ошибку (ц) зависает?
а результат — лучше (т.к. поймано больше багов).
А почему поймано больше багов?..
Druu
06.09.2017 16:41> вход — конверсия по метаданным из БД
Давайте сразу определимся — внешнее окружение (бд, удаленные сервисы и т.д.), естественно следует мокировать, это попадает под «использовать только в тех случаях, когда без них не обойтись».
> А почему поймано больше багов?..
Простой пример, у вас есть один и тот же тест на некоторый модуль, вы его запускаете в двух форматах — либо заменив зависимости моками, либо не заменив. В первом случае выполняется только код непосредственно тестируемого модуля, а во втором случае — и код всех используемых зависимостей, то есть один интеграционный тест заменяет 1+количество_используемых_зависимостей юнит-тестов. Кроме того, вы просто можете забыть обновить моки, в итоге с реальными зависимостями тест падает (т.к. их поведение изменилось), а с моками — проходит. Естественно, может быть обратная ситуация — когда несколько багов, взаимонакладываясь друг на друга, в итоге дают правильное поведение — но это очень большая редкость по сравнению с предыдущими двумя пунктамиlair
06.09.2017 16:50+1Давайте сразу определимся — внешнее окружение (бд, удаленные сервисы и т.д.), естественно следует мокировать
Вот только преобразовалка смотрит не в БД, а в сервис, поставляющий метаданные в удобном ей формате. Мне мокировать БД или этот сервис? И почему?
И еще — локальные зависимости ошибаться не могут?
В первом случае выполняется только код непосредственно тестируемого модуля, а во втором случае — и код всех используемых зависимостей, то есть один интеграционный тест заменяет 1+количество_используемых_зависимостей юнит-тестов.
А вот теперь смотрите: у вас есть модули А и Б, оба зависят от Ц и Д. Мы решили, по вашей методике, сэкономить тесты, и написали только интеграционные тесты на А и Б (тем самым Ц и Д тестируются имплицитно). Мы поменяли Ц. Какие тесты нам надо запустить, чтобы быть уверенными, что мы ничего не сломали? А теперь представьте, что наш сосед написал модуль Г, зависящий от Ц, на который он решил тесты не писать, потому что "модуль тривиальный".
Или вот наоборот: модуль Х зависит от У. У, в свою очередь, зависит от внешних зависимостей З1, З2 и З3. Согласно вашему же правилу, эти зависимости надо мокировать. Значит, в вашем тесте на Х есть три мока (З1, З2, З3) вместо одного (У). Теперь наш сосед добавляет в У зависимость З4. Что происходит? Правильно, падают тесты на Х, хотя казалось бы.
Druu
06.09.2017 16:58> Мне мокировать БД или этот сервис? И почему?
Бд. Потому что затрат меньше, чем в случае использование тестовой бд. Если в вашем случае тестовая бд дает меньше затрат (что бывает редко, но вдруг) — ну тогда, конечно, можно и тестовую бд. Чем ближе тест к реальности — тем он, конечно же, лучше.
> И еще — локальные зависимости ошибаться не могут?
Могут, конечно же, но вероятность того, что два модуля согласованно ошибутся так, чтобы съесть ошибку, весьма мала.
> А вот теперь смотрите: у вас есть модули А и Б, оба зависят от Ц и Д. Мы решили, по вашей методике, сэкономить тесты, и написали только интеграционные тесты на А и Б (тем самым Ц и Д тестируются имплицитно).
Конечно же, мы не пишем тесты только на А и Б, мы пишем все те же самые тесты, что писали бы и в случае использования моков. Просто мы «искаробки» получаем некоторое дополнительное, как вы выразились, «имплицитное», тестирование зависимых модулей, что позволит поймать больше багов в этих модулях. То есть не «меньше тестов при том же результате», а «выше результат при тех же тестах».
> Значит, в вашем тесте на Х есть три мока (З1, З2, З3) вместо одного (У).
Так это глобальные моки. Они настраиваются раз и для всех тестов.lair
06.09.2017 17:06+1Бд. Потому что затрат меньше, чем в случае использование тестовой бд
Стоп-стоп. Вы сравниваете затраты с использованием тестовой БД, а я спрашиваю, мне мокать БД или сервис. Речи о тестовой БД не идет.
Могут, конечно же, но вероятность того, что два модуля согласованно ошибутся так, чтобы съесть ошибку, весьма мала.
Вы снова отвечаете не на тот вопрос. Мне надо проверить, что SUT корректно себя ведет, если его зависимость (внутренняя!) ведет себя некорректно. Как это сделать без мока?
Конечно же, мы не пишем тесты только на А и Б, мы пишем все те же самые тесты, что писали бы и в случае использования моков.
То есть вы предлагаете писать тесты на А, Б, Ц и Д?
Тогда у вас получается больше кода, а не меньше, потому что тесты те же, только еще и для А и Б надо засетапить все, что нужно для Ц и Д.
Так это глобальные моки. Они настраиваются раз и для всех тестов.
Так в разных тестах разное поведение нужно, вообще-то. Какие тут глобальные моки?
Хуже того, если у вас общий сетап, то он отнесен от теста, и прочитать, что же делает тест — и почему — становится намного сложнее.
Druu
06.09.2017 17:21> Стоп-стоп. Вы сравниваете затраты с использованием тестовой БД, а я спрашиваю, мне мокать БД или сервис.
Я ответил. Мокать БД. Просто сделал на всякий случай пояснение — если по каким-то исключительным причинам вы можете легко поднять и без проблем использовать полноценную тестовую БД — то мокать и вовсе ничего не надо (но это чисто умозрительная ситуация, понятно, что в реальности такое представить сложно).
> Мне надо проверить, что SUT корректно себя ведет, если его зависимость (внутренняя!) ведет себя некорректно. Как это сделать без мока?
Никак. А зачем решать бесполезные задачи?
> То есть вы предлагаете писать тесты на А, Б, Ц и Д?
Те же самые тесты, я же указал.
> Тогда у вас получается больше кода, а не меньше, потому что тесты те же, только еще и для А и Б надо засетапить все, что нужно для Ц и Д.
Сетапить вообще ничего не надо кроме глобального сетапа.
> Так в разных тестах разное поведение нужно, вообще-то. Какие тут глобальные моки?
Если надо подменить какую-то конкретную часть сетапа — ну не проблема, подменяйте. В любом случае, это обычно требует в десятки меньше кода, чем на полноценный сетап всех моков.lair
06.09.2017 17:25+1Я ответил. Мокать БД.
Так почему же не сервис?
Никак. А зачем решать бесполезные задачи?
То есть тоже нужен мок?
Сетапить вообще ничего не надо кроме глобального сетапа.
Глобальный сетап — зло.
Если надо подменить какую-то конкретную часть сетапа — ну не проблема, подменяйте.
Ну то есть нужно сделать целую отдельную инфраструктуру, которая будет позволять делать глобальный сетап (см. выше), и подменять любую его часть.
В любом случае, это обычно требует в десятки меньше кода, чем на полноценный сетап всех моков.
… если зависимостей мало, то и моков мало. А если моков мало, то их сетап — это ровно то же самое, что подмена в глобальном. Так что никакого "в десятки меньше кода".
Druu
06.09.2017 17:29> Так почему же не сервис?
Потому что лучше мокать только БД, чем и сервис и БД (вы же сам сервис тоже тестировать будете, это ваш код? или я неверно понял вопрос?)
> То есть тоже нужен мок?
Зачем?
> Глобальный сетап — зло.
В чем? Я вижу только добро — экономию при прочих равных.
> Ну то есть нужно сделать целую отдельную инфраструктуру, которая будет позволять делать глобальный сетап (см. выше), и подменять любую его часть.
Зачем для этого какая-то особая инфраструктура? Подмена куска сетапа не сложнее, чем его определение.
> … если зависимостей мало, то и моков мало. А если моков мало, то их сетап — это ровно то же самое, что подмена в глобальном. Так что никакого «в десятки меньше кода».
Но на практике их много, либо у вас у god-object'ы вместо классов.lair
06.09.2017 17:31+1Потому что лучше мокать только БД, чем и сервис и БД
А почему лучше?
Зачем?
Чтобы протестировать описанные кейсы.
В чем?
Антипаттерн shared fixture. Сетап не виден в тесте, возникают неявные зависимости между тестами, могут быть проблемы при конкурентном выполнении и так далее.
Зачем для этого какая-то особая инфраструктура? Подмена куска сетапа не сложнее, чем его определение.
Покажите пример, пожалуйста.
Но на практике их много, либо у вас у god-object'ы вместо классов.
Или нет. У меня на практике мало зависимостей (кроме тех классов, где я знаю, что нарушен SRP, и которые стоят в очереди на рефакторинг).
Druu
06.09.2017 17:36> А почему лучше?
Потому что нет кода, который не выполняет никакой задачи (мок сервиса в данном случае).
> Чтобы протестировать описанные кейсы.
Ну так тестируйте с моком БД.
> Антипаттерн shared fixture.
Почему это антипаттерн?
> Или нет. У меня на практике мало зависимостей (кроме тех классов, где я знаю, что нарушен SRP, и которые стоят в очереди на рефакторинг).
Если у вас весь весь функционал в куче, то, конечно, зависимостей мало. Но мы же о качественном коде говорим?
> Покажите пример, пожалуйста.
Пример чего? Использования операции присваивания? Или вызова методов фреймворка для создания моков?lair
06.09.2017 17:41+1Потому что нет кода, который не выполняет никакой задачи (мок сервиса в данном случае).
Почему же не выполняет? Он выполняет задачу "подать на вход тестируемому объекту ровно те данные, которые описаны в тесткейсе".
Ну так тестируйте с моком БД.
Мок БД не позволяет протестировать, как поведет себя SUT, зависящий от сервиса, зависящего от БД, при зависании кода сервиса.
Почему это антипаттерн?
В следующем предложении было написано. Ну и у Мезароса тоже.
Если у вас весь весь функционал в куче, то, конечно, зависимостей мало. Но мы же о качественном коде говорим?
Да, о качественном. И вот у качественного кода в моей практике мало зависимостей (кроме уже описанных исключений, которые все равно юнит-тестировать не обязательно).
Пример чего?
Пример того, как вы получаете SUT с глобальным сетапом, в котором заменено поведение одной зависимости.
Druu
06.09.2017 17:47> Он выполняет задачу «подать на вход тестируемому объекту ровно те данные, которые описаны в тесткейсе».
Это аргументы метода и внешнее состояние (сервисов, бд, етц.). То, что возвращают те или иные зависимости, данными, конечно, не является, так как мы даже не знаем (и не должны) о том, какие зависимости данный метод тянет (черный ящик же).
> В следующем предложении было написано. Ну и у Мезароса тоже.
То, что там написано — по-просту неверно.
> Да, о качественном. И вот у качественного кода в моей практике мало зависимостей
Тогда либо у вас нарушается SRP, либо вам везет.
> Пример того, как вы получаете SUT с глобальным сетапом, в котором заменено поведение одной зависимости.
Я не понимаю что конкретно вы хотите. Чем этот код, по-вашему, должен отличаться от построения сетапа с нуля, кроме того, что часть сетапа оказывается опущена?lair
06.09.2017 17:59+1Это аргументы метода и внешнее состояние (сервисов, бд, етц.). То, что возвращают те или иные зависимости, данными, конечно, не является, так как мы даже не знаем (и не должны) о том, какие зависимости данный метод тянет (черный ящик же).
А вот и нет. В тесткейсе описана доменная сущность (метаданные Х), а не состояние БД. Соответственно, если у меня есть сервис, который мне эту доменную сущность возвращает, мне проще как раз его замокать, чем думать, как же эта сущность отображается в БД.
То, что там написано — по-просту неверно.
Аргументируйте.
Тогда либо у вас нарушается SRP, либо вам везет.
Видимо, я очень везуч.
Я не понимаю что конкретно вы хотите.
Пример кода.
Druu
06.09.2017 18:09-1> А вот и нет. В тесткейсе описана доменная сущность (метаданные Х), а не состояние БД. С
Если это доменная сущность, то она тогда в аргументах. В промежуточном слое ее быть не может, т.к. я просто не знаю ничего об этом промежуточном слое. Я знаю, что мой метод принимает определенные аргументы, лезет в бд, и что-то там возвращает. Я ничего не знаю о существовании (и особенностях работы) каких-либо промежуточных слоев.
То есть если я знаю о том, что метод работает с доменной сущностью — то значит этот метод в базу не лезет, в базу за него полез (и построил потом доменную сущность) кто-то другой и в аргументах передал.
> Пример кода.
Вы можете его увидеть в описании любого фреймворка для моков.lair
06.09.2017 18:21Если это доменная сущность, то она тогда в аргументах.
Нет, это indirect input.
В промежуточном слое ее быть не может, т.к. я просто не знаю ничего об этом промежуточном слое. Я знаю, что мой метод принимает определенные аргументы, лезет в бд, и что-то там возвращает.
Тоже нет. Метод принимает определенные аргументы, и, опираясь на определенные где-то метаданные, что-то возвращает. В тесткейсе метаданные сформулированы в виде доменной сущности, не ее представления в БД.
Я ничего не знаю о существовании (и особенностях работы) каких-либо промежуточных слоев.
Значит, вы тестируете не свой метод. Не мой случай.
Вы можете его увидеть в описании любого фреймворка для моков.
Фреймворки для моков
lair
06.09.2017 18:40Фреймворки для моков
Оу, плохо отвлекаться. Читать "фреймворки для моков редко показывают примеры глобальных повторно используемых сетапов". Я, по крайней мере, не видел ни одного. Можно ссылочку?
Druu
06.09.2017 18:43> Читать «фреймворки для моков редко показывают примеры глобальных повторно используемых сетапов».
Я не могу понять, чем,, по-вашему, настройка глобального сетапа отличается от любого другого. Почему вы решили, что там есть какие-то особенности? Вызываются те же методы, с тем же результатом.lair
06.09.2017 18:47Вызываются те же методы, с тем же результатом.
"Те же методы" в мок-фреймворках, которыми я пользуюсь, просто возвращают правильным образом сконфигуренные моки. А вот как мне сделать, чтобы SUT использовал зависимости, которые используют другие зависимости, которые где-то потом неизвестно где используют эти моки?
Druu
06.09.2017 18:52> А вот как мне сделать, чтобы SUT использовал зависимости, которые используют другие зависимости, которые где-то потом неизвестно где используют эти моки?
Эм… Вам надо конфигурировать только те, последние моки. Точно так же, как вы все моки конфигурируете.lair
06.09.2017 19:00Моки надо не только конфигурировать (т.е., определять их поведение), но еще и передать пользователям (т.е., сказать, что их надо использовать).
Когда у меня моки конфигурятся под тест, я прямо в тесте создаю SUT и в качестве зависимостей передаю ему моки. А если у меня глобальный сетап, то мне нужно то ли взять откуда-то готовый SUT, то ли взять откуда-то зависимости, которые ему передать. Откуда?
Druu
07.09.2017 03:21> А если у меня глобальный сетап, то мне нужно то ли взять откуда-то готовый SUT, то ли взять откуда-то зависимости, которые ему передать. Откуда?
Ну а как вы это делаете обычно? Через IoC-контейнер, я полагаю. Зарегистрировать/заменить моки для теста в IoC-контейнере вы можете откуда угодно.lair
07.09.2017 10:59Ну а как вы это делаете обычно? Через IoC-контейнер, я полагаю.
В тестах? Конечно, нет: просто создаю SUT напрямую, передавая зависимости параметрами.
Зарегистрировать/заменить моки для теста в IoC-контейнере вы можете откуда угодно.
Вот этот IoC контейнер, со всей его настройкой, и есть "инфраструктура для глобального сетапа", о которой я говорил.
Druu
06.09.2017 18:42> Нет, это indirect input.
Тогда я о ней не могу знать.
> Значит, вы тестируете не свой метод.
Нет, просто я не хочу привязывать тесты к реализации. Тесты проверяют спецификацию, спецификация не зависит от реализации.lair
06.09.2017 18:45Тогда я о ней не могу знать
Тогда вы не можете протестировать.
Нет, просто я не хочу привязывать тесты к реализации. Тесты проверяют спецификацию, спецификация не зависит от реализации.
В спецификации написано "given mapping defined as follows", и дальше домен. Куда бы вы это ни записали — все равно будет привязка к реализации, просто в одном случае — к доменной модели, а в другом — к БД.
Druu
06.09.2017 18:53> В спецификации написано «given mapping defined as follows»
Я буду передавать эти мапинги аргументом в данном случае.lair
06.09.2017 19:01Технические ограничения не позволяют (я не зря про request filter написал).
Druu
06.09.2017 19:11> Технические ограничения не позволяют (я не зря про request filter написал).
Это как может быть? Один метод достает мапинг из хранилища и возвращает, второй — принимает то, что вернул предыдущий, и применяет.lair
06.09.2017 19:17А вот так. SUT — это компонент, находящийся в request pipeline (например, WebAPI), у которого на входе запрос (и только запрос), и на выходе — тоже запрос (преобразованный как хочется). Соответственно, эти ваши два метода — это хорошо, но они окажутся внутри SUT.
(это кстати, иллюстрация к вашему "тестировать как черный ящик")
Druu
07.09.2017 03:15> Соответственно, эти ваши два метода — это хорошо, но они окажутся внутри SUT.
Если по смыслу они должны быть снаружи — то кто вам мешает их вынести?lair
07.09.2017 10:53Контракт SUT мне мешает. Используемый фреймворк требует, чтобы фильтры в конвеере имели сигнатуру
Request ProcessRequest(Request)
— соответственно, у SUT сигнатура строго такая же.
Мы, конечно, можем вынести оба этих метода в какой-то другой класс, но тестируем-то мы этот. Если мы остаемся в парадигме интеграционного тестирования/черного ящика, то для нас нет разницы, вынесли мы их или нет (именно потому, что это черный ящик, мы не знаем, что он вызывает). Если мы в парадигме юнит-теста/прозрачного ящика, то при тестировании такого SUT нам придется проверить, что он вызывает то, куда мы вынесли эти операции (то есть, использовать моки).
Собственно, когда у вас контракт SUT строго определен требованиями, и логика завязана на информацию, не поступающую во входных данных, вам придется иметь indirect inputs, вы никуда не можете от этого деться.
Druu
07.09.2017 15:26> Контракт SUT мне мешает.
Как он может мешать, если мы говорим о внутренней реализации функции? Сигнатура у нее та же самая, данные она принимает и возвращает те же. Просто логика мапинга выделена в метод, который мы и тестируем. Тестировать же надо логику, а не БД или работу моков.lair
07.09.2017 15:28Просто логика мапинга выделена в метод, который мы и тестируем.
Вот так взяли и заменили один SUT на другой. Не надо так, первый SUT остался непротестированным.
Kobalt_x
07.09.2017 08:11"Indirect input" Явное всегда лучше неявного в отрефакторном коде их быть не должно
lair
07.09.2017 11:01Если в отрефакторенном коде нет неявных входов и выходов, то в нем нет и зависимостей; а если в нем нет зависимостей, то он за пределами этого обсуждения.
(ну то есть за исключением "чистых" функциональных зависимостей, конечно, но их тоже на моей памяти не мокают, ибо незачем, а значит, тоже за пределами обсуждения)
Druu
07.09.2017 15:28> (ну то есть за исключением «чистых» функциональных зависимостей, конечно, но их тоже на моей памяти не мокают, ибо незачем, а значит, тоже за пределами обсуждения)
Когда я выше говорил про сервисы к БД с кучей зависимостей и методе-двустрочнике, который все что делает — это дергает пяток функций из них — то я как раз про функциональные зависимости и говорил. Оказывается, их «можно» и не мокать? Ну тогда в итоге кроме внешних сервисов и нечего мокать-то, как я и предложил изначально.lair
07.09.2017 15:30Когда я выше говорил про сервисы к БД с кучей зависимостей и методе-двустрочнике, который все что делает — это дергает пяток функций из них — то я как раз про функциональные зависимости и говорил.
А эти ваши зависимости — "чистые" функциональные? Никаких побочных эффектов и полная детерминистичность?
Druu
07.09.2017 19:24Там методы генерируют спецификации. Исполняются спецификации уже отдельно.
lair
07.09.2017 20:54Я, если честно, уже запутался, что у вас что и что что генерирует, поэтому просто повторю свою мысль целиком: обычно нет смысла мокать те зависимости, которые ведут себя как чистая функция — не имеют побочных эффектов и строго детерминистичны (т.е. для одного и того же входа всегда дадут один и тот же выход).
Druu
08.09.2017 04:59-1Так мы в итоге и пришли к предлагаемому мной изначально варианту — мокать только внешние зависимости, т.к. все остальные — могут (и должны) быть функциональными.
lair
08.09.2017 11:12Ну вот в моем опыте внутренних зависимостей с поведением чистой функции — подавляющее меньшинство (наверное, следствие ООП?). Поэтому ваш вариант получается неприменим.
Druu
08.09.2017 11:19> Ну вот в моем опыте внутренних зависимостей с поведением чистой функции — подавляющее меньшинство
Так это уже вопрос исключительно вашей архитектуры. Никто же вам не мешает максимально выделять логику в чистые функции.
Кроме того, не совсем понятно, почему чистые зависимости мокать не надо, а «грязные» — надо. Почему такое разделение?lair
08.09.2017 11:25Так это уже вопрос исключительно вашей архитектуры. Никто же вам не мешает максимально выделять логику в чистые функции.
"Мешает" используемая парадигма и принятые архитектурные соглашения.
Кроме того, не совсем понятно, почему чистые зависимости мокать не надо, а «грязные» — надо. Почему такое разделение?
Не "не надо", а незачем. Их поведение (если они покрыты тестами) предсказуемо, и они чаще всего не вносят собственный эффект в тест. Грубо говоря, никто же не мокает
string.Split
? (ну, я надеюсь)
Когда я понимаю, что зависимость — не важно уже, грязная она или чистая, — мешает мне корректно сформулировать тесты, я ее мокаю. Так уж вышло, что — в моем опыте — чистые зависимости мокать обычно не приходится.
Druu
08.09.2017 11:44> Не «не надо», а незачем. Их поведение (если они покрыты тестами) предсказуемо, и они чаще всего не вносят собственный эффект в тест.
Давайте немного уточним, вот есть метод, у него нет зависимостей. Теперь мы какой-то из аргументов перенесли в поле класса (сделав зависимостью). В каком случае эта зависимость будет «функциональной», а в каком — нет?lair
08.09.2017 11:46Ну то есть было
void Do(x arg)
, сталоvoid Do()
иx _dependency
? Первый и самый важный вопрос — а какого типаarg
?Druu
08.09.2017 11:53> Ну то есть было void Do(x arg), стало void Do() и x
Ну не обязательно void, может и что-то другое возвращать.
> Первый и самый важный вопрос — а какого типа arg?
Вот я это и хочу узнать, при аргументах с какими свойствами (тип, то как мы работаем со значением внутри метода, еще какие особенности) зависимость будет функциональной, а при какой — не будет. Просто уточниться, чтобы друг друга верно понимать.lair
08.09.2017 12:00… а что вообще в вашем примере зависимость?
Потому что изначально я подумал, что "зависимость" — это то, что было
arg
, а стало_dependency
. И в этом случае то, зависимость ли оно, зависит (извините) не от того, где оно (в параметре или в филде), а от того, какого оно типа. Если это сервис, то оно всегда зависимость (не важно, как она вбрасывается), если это значение, то оно никогда не зависимость.
Так что давайте на примере хотя бы двух компонентов:
IMetadataProvider {Mapping GetMappingFor(string requestType)} IRequestMapper {Request MapRequest(Request request, ?)}
Вопросительный знак оставлен как раз для того, чтобы вы выбрали, что же туда передается, и что является зависимостью чего, если является.
IMetadataProvider.GetMappingFor
, очевидно, не чистая зависимость.Druu
08.09.2017 12:13> IMetadataProvider.GetMappingFor, очевидно, не чистая зависимость.
Потому что он тянет значение из какого-то хранилища, верно (этого в типе нет, но по смыслу, вроде, ясно)? А если будет так — GetMappingFor возвращает не сам маппинг, а то, как его достать (ну пусть для простоты и конкретики, это просто строка с sql запросом, чисто для примера). Запускать полученные запросы будет какой-то третий сервис, естественно, на него добавиться зависимость (очевидно, нефункциональная) в маппер. Тогда зависимость на провайдер будет функциональной или нет?lair
08.09.2017 12:17Тогда зависимость на провайдер будет функциональной или нет?
Если то, что возвращает провайдер, всегда зависит только и исключительно от того, что в него передано, провайдер, как зависимость, можно считать чистой функцией.
Druu
08.09.2017 12:23> Если то, что возвращает провайдер, всегда зависит только и исключительно от того, что в него передано, провайдер, как зависимость, можно считать чистой функцией.
Я бы еще добавил, что это «что-то» надо суметь провалидировать внутренними средствами (а то если лямбду возвращать всегда — то у нас тоже получатся везде формально «чистые» функции, но от самой что ни на есть грязной грязи по факту это отличаться не будет, ведь вы не можете проверить свою лямбду, не запустив ее, со всей грязью).
А по примеру — в итоге получается, что мокать вам придется только «запускатор запросов», то есть это, вобщем-то, и есть БД.lair
08.09.2017 12:24А по примеру — в итоге получается, что мокать вам придется только «запускатор запросов», то есть это, вобщем-то, и есть БД.
...если вам удастся построить систему так, что у вас все грязные эффекты находятся в паре модулей,
то у вас получится хаскелькоторые легко замокать.Druu
08.09.2017 12:29> то у вас получится хаскель
Хаскель, кстати, проблемы не решит, т.к. действия в ИО-монаде как раз нарушают мое примечание из предыдущего поста (они не валидируются). То есть, вы не можете проверить свое ИО, не запустив его, в итоге для валидации генерящих ИО ф-й вы вынуждены будете мокать зависимости (по вашему подходу с моками нефункциональных зависимостей).Bonart
09.09.2017 02:23У чисто функциональных зависимостей в хаскеле никакой монады IO нет и не будет по определению.
Druu
09.09.2017 07:34В хаскеле все зависимости функционально чистые. Грязных функций там в принципе не бывает, их нельзя написать :)
Bonart
09.09.2017 14:07Грязных функций там в принципе не бывает, их нельзя написать
Да ладно?
А как же UnsafePerformIO?
Но и без таких фокусов монада IO позволяет сохранить язык чистым, но явно обозначив зависимость от состояния окружения, совместив "чистую" программу с "грязным" результатом выполнения.
Как результат в хаскеле вопрос с моками решается тривиально — там зависимость от внешнего мира без адских хаков не спрячешь.Druu
10.09.2017 13:21> А как же UnsafePerformIO?
А это и не хаскель, unsafePerformIO ломает семантику. Это особенность конкретной реализации.
> совместив «чистую» программу с «грязным» результатом выполнения.
Нету в хаскеле никакого грязного результата выполнения. Когда вы в хаскеле возвращаете IO, то никаких сайд-эффектов не происходит, они происходят при запуске ИО, которое уже к хаскелю не относится, изнутри хаскеля запустить ИО невозможно.Bonart
10.09.2017 13:37А это и не хаскель, unsafePerformIO ломает семантику. Это особенность конкретной реализации.
Это GHC, стандарт хаскеля де-факто, про него говорить "особенности конкретной реализации" скорее вредно чем бесполезно.
они происходят при запуске ИО
Ну вот мы и пришли к тому с чего начали — IO оказывается отличным несмываемым маркером зависимости результата работы программы от внешних данных. У шарпа такого маркера нет.
Druu
10.09.2017 13:55> Это GHC
А это компилятор. У языка есть определенная семантика, unsafePErformIO в нее не входит. С точки зрения хаскеля эта ф-я вообще ничего не делает.
> Ну вот мы и пришли к тому с чего начали — IO оказывается отличным несмываемым маркером зависимости результата работы программы от внешних данных.
Так результат у программы один и тот же — одно и то же ИО. А вот результат ИО — уже другое дело. Но к программе на хаскеле этот результат отношения не имеет :)
Bonart
06.09.2017 16:54Простой пример, у вас есть один и тот же тест на некоторый модуль, вы его запускаете в двух форматах — либо заменив зависимости моками, либо не заменив.
И каким образом у вас вариант с моками оказался больше?
С моками досточно имитировать непосредственные зависимости, без моков — надо построить все зависимости в графе. Похоже, вы нам что-то недоговариваете.Druu
06.09.2017 17:01> И каким образом у вас вариант с моками оказался больше?
С моками вам надо настраивать моки, без моков — соответственно, не надо.
> С моками досточно имитировать непосредственные зависимости, без моков — надо построить все зависимости в графе.
Надо мокировать только что, что надо мокировать в итоге (какие-то внешние зависимости), а не все в графе. И делается это один раз. Под какие-то тесть-сьюиты могут вноситься, конечно, какие-то необходимые изменения, но в самих тестах уже ничего дополнительно делать практически никогда не надо. В итоге лишнего кода в разы меньше.
Bonart
06.09.2017 14:42Назначение тестов — поиск ошибок. Количество ошибок, которые отловил ваш тест, деленное на затраты для написания теста — это и есть прямая оценка качества данного теста
Например, у вас есть простой однострочный тест, который ловит абсолютно все ошибки (идеал по вашим же критериям). Вот только искать, где именно ошибка, вам придется самому и тест, несмотря на высочайшую "оценку качества", не сможет здесь помочь.
Druu
06.09.2017 16:37> Например, у вас есть простой однострочный тест, который ловит абсолютно все ошибки (идеал по вашим же критериям). Вот только искать, где именно ошибка, вам придется самому и тест, несмотря на высочайшую «оценку качества», не сможет здесь помочь.
Тест укажет, в чем состоит ошибка (какое именно требование нарушено), а если известно, в чем ошибка, то ее локализация (в тех рамках, в которых она может быть выполнена за счет изоляции тестов) — тривиальная задача, которая даже в самых сложных случаях решается за время порядка единиц минут, обычно же — секунды.
И, да, почти всегда лучше обнаружить две плохо локализованные ошибки, чем одну — хорошо локализованную.Bonart
06.09.2017 16:49тривиальная задача, которая даже в самых сложных случаях решается за время порядка единиц минут
Это для теста, покрывающего несколько классов с 20 зависимостями в каждом? Позвольте вам не поверить.
Допустим, ваш тест показал, что сумма контракта считается неверно. За ее расчет отвечает около сотни классов. Тривиально?
И, да, почти всегда лучше обнаружить две плохо локализованные ошибки, чем одну — хорошо локализованную
А еще лучше — обнаружить две хорошо локализованные ошибки, что наличие юнит-тестов и позволяет сделать.
Druu
06.09.2017 17:12> Допустим, ваш тест показал, что сумма контракта считается неверно. За ее расчет отвечает около сотни классов. Тривиально?
А юнит-тесты вам как помогут? Если какой-то из юнит-тестов падает на сломанной зависимости, то он и в интеграционном виде на ней упадет и вы увидите ошибку там же. Если же не падает — ну тогда в случае интеграционных тестов вы эту ошибку увидите, а в случае юнит-тестов — вообще нет, она будет пропущена.
> А еще лучше — обнаружить две хорошо локализованные ошибки, что наличие юнит-тестов и позволяет сделать.
Конечно, лучше. И еще лучше, если бы не надо было совершать кучу лишних телодвижений для моков. Но такую методологию тестирования еще не придумали. Либо мы пишем простые и качественные тесты, которые, однако, чуть менее точно локализуют ошибку, либо сложные и некачественные, которые локализуют ошибку лучше.
khaletskiy
06.09.2017 14:28Тестирование только на моках может приводить к «тавтологическим тестам».
Держал это в голове, но после прочтения habrahabr.ru/company/badoo/blog/336194 можно в терминах).Bonart
06.09.2017 14:43Тавтологичность тестов не зависит от использования моков.
nizkopal
06.09.2017 16:22Конечно, «тавтологичность» зависит от неправильного и неразумного использования моков. Меня смутило, судя по всему как и автора предыдущего комментария, напутствие использовать моки как можно чаще. Было бы правильно упомянуть в этом пункте и про подводные камни, коих не мало. Все таки статья посвящена правильному написанию unit-тестов.
vryashentsev
08.09.2017 06:16Мокать надо зависимости имеющие поведение — для того чтобы тест не имел тенденции падать при изменении других модулей.
shurutov
06.09.2017 13:263 правило. На *них-машине оба варианта путей приведут к ошибке. Внезапно, однако!
Если уж используется java, то, по-моему, будет уместным пользоваться File.separator или path.separator для формирования путей;
6 правило. Я думаю, будет уместным упомянуть про виртуалки/контейнеры с возможностью создания снимков ФС.
vintage
06.09.2017 13:28+3Тесты — это тоже код, и относиться к нему нужно как к рабочему коду.
То есть нужно писать тесты и на тесты тоже? :-)
И всё-равно оставили виндовые разделители путей :-D
at_wrike Автор
06.09.2017 13:58+1Тесты на тесты — это перебор.
Проверял только на винде и там действительно работало оба варианта. Сейчас проверил на маке и исправлю скрин. Спасибо за помощь.
Throwable
06.09.2017 15:18- Третье правило настоящего джентльмена — следи за путями.
Что за треш? Paths.get(".", "log", "service.log") позволит вам навсегда забыть о File.separator.
nizkopal
06.09.2017 16:27Спасибо за статью. Я с удовольствием посмеялся над пунктом три, и мне понравился последний пункт про запуск тестов не только локально.
Но в целом от такой статьи хотелось бы побольше примеров «из жизни» и, возможно, «вредных» примеров и антипаттернов для наглядности. Некоторые пункты показались довольно банальными.
Tarmik
06.09.2017 20:30-2Как вам мое «тестирование» — syncProj — просто и элегантно? :)
lair
06.09.2017 23:02+1Отвратительно.
Tarmik
07.09.2017 00:09-1Чем аргументируешь?
lair
07.09.2017 00:12Тем, что (вообще) не понятно, как это работает и должно работать.
С другой стороны, вы же спросили "как вам", так вот, мне — отвратительно. Эмоции аргументировать не обязательно.
Tarmik
08.09.2017 19:31-1Но думаю на эмоции можно отвечать эмоциями. :)
Странные у вас комментарии. Т.е. скачать исходники проекта и запустить unit testing не умеем, но высказать эмоции умеем. Да… По меньшей мере странные… :)lair
08.09.2017 21:20Так я скачал и запустил. Вместо нормального зеленого списка тестов в раннере я получил низачем мне не нужное диалоговое окно. Мне страшно подумать, что будет, если я это в CI запущу.
Так что нет, отвратительно.
Tarmik
08.09.2017 23:29-1Тут дело тоже в требованиях к тестированию — моя изначальная цель была сделать не 5 ступенчатый шаттл, а простенький unit testing. Возможно что-то не закоммичено из-за чего тест не проходит, возможно у меня английский windows, а у тебя русский и regional settings не совпадают, и он плюется messageboxом в ответ — что мол будем делать с этим тестом.
Если на работе я скажем коммичу изменения, то у меня и build компьютер проверяет что не работает, то с open source code проектом у меня нет build компьютера — все делается мануально.
Возможно я когда-нибуть забогатею с syncProj утилитой и куплю домой себе ещё компьютер для автоматических буйлдов, а также инвестирую себе деньги+ время на полную автоматизацию данного unit теста, но пока это не произошло можно и мануально запустить и самому проверить что к чему.
деньги + время + возможности влияют на требования, подход и код.
и я считаю что прежде чем выражать свои эмоции надо бы понять почему это так сделано а не иначе.lair
08.09.2017 23:33Тут дело тоже в требованиях к тестированию — моя изначальная цель была сделать не 5 ступенчатый шаттл, а простенький unit testing.
Ну так он уже есть в студии готовый. Бери и пользуйся.
то с open source code проектом у меня нет build компьютер — все делается мануально.
Но вообще отсутствие билд-машины никак не влияет на подход к тестам. Я даже для собственных учебных проектов пишу тесты на том же самом движке, что и для работы, с тем же подходом. И до сих пор не видел причины изобретать велосипеды.
деньги + время + возможности влияют на требования, подход и код.
Обычные юнит-тесты на банальном xUnit пишутся не дольше, чем вы писали свой собственный раннер, а денег не стоят нисколько.
и я считаю что прежде чем выражать свои эмоции надо бы понять почему это так сделано а не иначе.
Так я выражаю эмоции от того, что и как сделано, а не почему оно так сделано.
Tarmik
08.09.2017 23:42> Ну так он уже есть в студии готовый. Бери и пользуйся.
я видел много unit testов, и более менее представляю что это и с чем едят.
Вы пишете unit testы. Я их записываю и повторяю («record & playback») — это быстрее чем написать их.
И тут дело ещё не в том что написать
assert( return == 25 )
а в том что никому кроме программиста не понятно что такое 25 — тут идет заточка на описании ошибки, а также видимости всех параметров ошибки конечному пользователю или же пользователю API.lair
09.09.2017 00:10Я их записываю и повторяю («record & playback») — это быстрее чем написать их.
Быстрее, но лучше ли?
а в том что никому кроме программиста не понятно что такое 25
Требований не существует?
Tarmik
09.09.2017 01:13> Быстрее, но лучше ли?
Сэкономленное на тестировании время можно использовать на разработку новых фич? Да и refactorинг проще делать. Compare-yes, compare-yes, compare-yes.
> Требований не существует?
Дело в том что отпечатывание всего текстом заставляет программиста задуматься над тем, что надо сделать (обработку ошибок) а не над тем что ему хочется. (например ввести код каждой ошибки). Требования не всегда и не все покрывают.
Я лично видел написанные функции, где вместо boolean превязывался код ошибки, он в конечном итоге он не использовался. (Правда без unit testa)lair
09.09.2017 01:18Да и refactorинг проще делать. Compare-yes, compare-yes, compare-yes.
Рефакторинг — это изменение структуры кода без изменения его поведения. Иными словами, интеграционные тесты (а у вас именно они) при рефакторинге меняться не должны вовсе.
Дело в том что отпечатывание всего текстом заставляет программиста задуматься над тем, что надо сделать (обработку ошибок)
Ну так тесты — это и есть "отпечатывание всего текстом".
Требования не всегда и не все покрывают.
Тогда откуда вы знаете, что надо тестировать?
Tarmik
09.09.2017 01:38> Рефакторинг — это изменение структуры кода без изменения его поведения.
С моей точки зрения ректоринг требуется именно тогда когда добавляешь новые фичи, и хорошо если изменения не затрегивают поведение — но иногда и поведение приходится изменять. Так что я бы не давал такой ограниченной формулировки.
> Ну так тесты — это и есть «отпечатывание всего текстом».
Но заставляет задуматься что печатаешь что бы было понятно что функциональность делает.
Кстати —assert( result == 25 );
заменяетсяConsole.WriteLine( result );
с последующим test result accept. Но естественно если напечаешь 25 — будет не понятно про что речь.
В итоге ты напечатаешь скажем так:
Console.WriteLine( "Port number: " + result );
И если в следующей итерации ты решишь поменять номер порта, то мой вариант проще и одобрить и понять что изменилось и почему.
> Тогда откуда вы знаете, что надо тестировать?
к чему клоните, не совсем понимаю.lair
09.09.2017 01:44С моей точки зрения ректоринг требуется именно тогда когда добавляешь новые фичи, и хорошо если изменения не затрегивают поведение — но иногда и поведение приходится изменять. Так что я бы не давал такой ограниченной формулировки.
Это классическое определение. Если вы изменили поведение — это не рефакторинг.
Кстати —
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".
к чему клоните, не совсем понимаю.
К тому, что обычно необходимое поведение определяется требованиями.
Tarmik
09.09.2017 11:57Это классическое определение. Если вы изменили поведение — это не рефакторинг.
Да, вы кстати правы на 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% меньше работы чем вы.
lair
09.09.2017 12:59мне не хватает термина refactoring with external changes.
Чем это отличается от просто "доработки программы"?
Я пишу только тестовый код — т.е. делаю 50% меньше работы чем вы.
А если не писать тестовый код вообще, то будет на 100% меньше работы. Правда же, круто?
Когда я пишу результат тестирования, я явно указываю, каким должно быть поведение программы. Я знаю, что оно должно быть таким, и я его фиксирую.
Как иначе вы предлагаете это сделать?
Tarmik
09.09.2017 14:31Чем это отличается от просто «доработки программы»?
В общем-то я хотел выразиться относительно большие изменения в программе — refactoring как раз подходит.
А если не писать тестовый код вообще, то будет на 100% меньше работы. Правда же, круто?
Да, до полного тестирования мне тоже ещё очень далеко. Дело в том что тестировать можно все связанное с файлами, базами данных, когда дело касается скажем 3d отрисовки или скорости выполнения, то это намного сложнее покрывать тестированием. Но syncProj позволяет тестирование — command line utility как никак.
Когда я пишу результат тестирования, я явно указываю, каким должно быть поведение программы. Я знаю, что оно должно быть таким, и я его фиксирую.
Когда я пишу тестирование, я сначала пишу что программа должна производить, а затем запускаю и проверяю вывела ли она то что я хотел.
Кстати — в syncProj идет сравнение не только по console output — там ещё идет сравнение по тому что сама программа генерирует. т.е. .sln (Solution file format) файлы и .vcxproj (xml, C++ project file format).
Альтернатива этому — это действительно прописывать все возвращаеммые значения, скажем как это сделано в premake.lair
09.09.2017 14:34В общем-то я хотел выразиться относительно большие изменения в программе — refactoring как раз подходит.
Рефакторинг — это очень конкретная вещь: изменение реализации без изменения поведения. Вы уж определитесь.
Когда я пишу тестирование, я сначала пишу что программа должна производить
Тогда вы тоже пишете "результат тестирования", и вашему "на 50% меньше" взяться неоткуда.
там ещё идет сравнение по тому что сама программа генерирует. т.е. .sln (Solution file format) файлы и .vcxproj (xml, C++ project file format).
И что? "Правильные" же все равно должны откуда-то взяться. А после того, как они есть, есть куча готовых модулей для юнит-тестирования, которые их сравнят.
Tarmik
09.09.2017 14:59И что? «Правильные» же все равно должны откуда-то взяться. А после того, как они есть, есть куча готовых модулей для юнит-тестирования, которые их сравнят.
Правильные результаты или результаты тестирования появляются тогда когда ты нажимаешь Yes в том Message boxе что ты получил и результаты спасаются отдельным файлом («одобрено программистом»).lair
09.09.2017 15:03… и откуда вы знаете, что они правильные? Они же уже сгенерены кодом.
Tarmik
09.09.2017 15:07Если это console output, то я, как программист, знаю что программа должна написать.
Если это .sln или .vcxproj — то обычно я открываю проект Визуал Студией и проверяю что бы она не плювалась, а также при закрытии solution, Visual studio не пыталась бы спасти изменения. (Обычно такое просходит если .sln содержит ошинки, и Visual Studio пытается их исправить).lair
09.09.2017 15:15Если это console output, то я, как программист, знаю что программа должна написать.
… и проверяете это глазами? Серьезно?
Если это .sln или .vcxproj — то обычно я открываю проект Визуал Студией и проверяю что бы она не плювалась, а также при закрытии solution, Visual studio не пыталась бы спасти изменения.
… и не сверяете с нужным набором файлов?
Не, это плохое тестирование, и уж точно не автоматизированное. О чем изначально речь и шла.
Tarmik
09.09.2017 15:21… и проверяете это глазами? Серьезно?
Да.
Кстати можно сделать проще:
Console.WriteLine( "Test " + result.ToString()
Что даст Test True или Test False, можно проверить визуально тоже.
Не, это плохое тестирование, и уж точно не автоматизированное. О чем изначально речь и шла.
У меня была идея что автоматизировать это можно было бы просто без message box promptа — т.е. результат не совпадает — ошибка / майл, и так далее.lair
09.09.2017 15:23Да.
Ну вот это и плохо. Рано или поздно вы ошибетесь. Вся идея автоматизированного тестирования в том, чтобы исключить такие ошибки.
У меня была идея что автоматизировать это можно было бы просто без message box promptа — т.е. результат не совпадает — ошибка / майл, и так далее.
Серьезно, "была идея"? Это же все много раз реализовано.
vryashentsev
08.09.2017 08:01
maz_d
08.09.2017 14:33Чаще используйте заглушки (моки) вместо реальных объектов.
По моему спорно, тот же мартин фаулер писал, про два стиля написания тестов и про то, что есть факторы по которым следует выбирать, какой стиль использовать, его точка зрения мне как то ближе чем просто заявление «пишите вот так».vryashentsev
08.09.2017 14:50Не подскажете где он это писал? Интересно узнать о критериях, да и о самих стилях.
lair
08.09.2017 15:04Mocks Aren't Stubs, раздел "Classical and Mockist Testing". Только это разделение не по "использовать заглушки или нет", а "что верифицировать".
Druu
08.09.2017 15:32Наоборот же, он в итоге пишет:
So as we see, state versus behavior verification is mostly not a big decision. The real issue is between classic and mockist TDD.
lair
08.09.2017 15:12-1Фаулер все-таки писал немного о другом. Фаулер вводит четкое разделение моков, фейков, заглушек и так далее, и уже в зависимости от этого проводит разделение по тому, что верифицируется — состояние SUT или набор вызовов на моке.
Если вы посмотрите в статью, то там написано следующее:
Не нужно подгонять состояние системы для конкретного случая, а просто настроил заглушку на возвращение нужного значения при вызове определенного метода и все.
По классификации Фаулера это стаб, а не мок, и такое использование вполне попадает в "классический", а не "мокистский" стиль тестирования.
Это, на самом деле, давняя и смешная проблема, вырастающая из банальной терминологической путаницы.
Druu
08.09.2017 15:35> По классификации Фаулера это стаб, а не мок, и такое использование вполне попадает в «классический», а не «мокистский» стиль тестирования.
Он как раз это не разделяет:
Now I'm at the point where I can explore the second dichotomy: that between classical and mockist TDD. The big issue here is when to use a mock (or other double).
The classical TDD style is to use real objects if possible and a double if it's awkward to use the real thing. So a classical TDDer would use a real warehouse and a double for the mail service. The kind of double doesn't really matter that much.
shpaker
Годно. Про Атомарность тестов ещё не мешало бы упомянуть. И листинги кода картинкой это сильный изврат в то время, когда Хабр уже давно умеет нормально вставлять и подсвечивать код.