Comments 28
Среди разработчиков бытует справедливое мнение, что если программист не покрывает код тестами, то попросту не понимает зачем они нужны и как их готовить.
Кажется, разработчики как раз понимают. Не понимает начальство. Жду статью, как заставить начальство понять и выделить ресурсы на написание и поддержку тестов...
Именно тут на страже стоят unit тесты, которые фиксируют утвержденные правила, по которым должна работать система!
Систему целиком контролируют IT-тестами, никак не unit. Поэтому если времени в обрез, лучше тратить его на IT-тесты, от них выхлопа больше
“у нас есть метод, который подсчитывает сумму чисел”
“на данный метод можно написать тест”
В статье скатились в тоже самое, только тестирование спринга добавилось.
В статье скатились в тоже самое, только тестирование спринга добавилось.
не совсем, в статье заложен принцип написания тестов для взаимоинтегрируемых систем
поясните пожалуйста, что вы называете IT-тестами?
Integration testing?
Покрывать нельзя, вы правы. Насколько я понял выражение "Систему целиком контролируют IT-тестами", то тут главное "систему целиком", а не "целиком контролируют". Мысль, что если ресурсов на тестирование мало, то их лучше пустить на интеграционные тесты — проверять систему в сборе, а не по отдельным компонентам.
Посыл статьи в целом поддерживаю, но на мой взгляд проблема даже чуть проще: основная масса текстов по юнит-тестированию не даёт хорошего представления о том, что такое этой самый юнит.
Более того, типичные примеры (типа как в статье про тест сложения чисел) наводят на мысль, что юнит — это вот что-то совсем обособленное от всего. А разжевывание концепции моков — на то, что каждая тестируемая часть кода должна быть полностью отгорожена от всех других частей стенами моков.
И потом пишутся тесты, которые, скажем, тестируют прямое перекладывание входных аргументов в выходные, потому что в коде есть некий уровень, где это перекладывание происходит, и его конечно же нужно огородить моками и протестировать.
Меж тем, юнит таки безразмерная единица. Это часть кода, но никто (из вменяемых людей) никогда не утверждает, что это должна быть часть определенного размера, определенной связанности, и тому подобное. Единственное разумное ограничение, которое тут выставляется самим процессом юнит-тестирования — это то, что у юнита должен быть определенный формально фиксируемый контракт поведения (а иначе что тестировать-то?).
Все дальнейшие лимиты на размер и комплексность юнита уже не абсолютны, а зависят от конкретной ситуации — как правило, для юнит-тестов нужна приемлемая скорость их выполнения (а потому "медленные" зависимости придётся обрубить), ну и при неуспехе того или иного теста нам неплохо бы точно знать, где случилась проблема, а поэтому один тест должен тестировать не слишком большую часть кода (но этого можно достигать не только через обрубание зависимостей).
И вот когда люди не пытаются применить жесткие и часто абсолютно необоснованные требования к тому, что может входить в "юнит" — вот тогда с написанием тестов становится всё ощутимо лучше.
Статья начала и закончила за здравие. А в середине те же самые 2+2 с кровавым Enterprise.
Посыл статьи полностью разделяю и поддерживаю. Юнит тесты используются для фиксации ожидаемого поведения наиболее эффективно. Нет бесполезной нагрузки на тестирование геттеров\сеттеров, или мокания целых хранимых процедур в базе данных.
Однако, юнит тесты для тестирования поведения от внешних зависимостей не очень полезны из-за нагрузки на дублирование поведения через моки и фейки. Кода становится сильно больше, и он очень хрупкий.
Куда как интереснее и ценнее разделять бизнес-логику приложения на настолько элементарные части, что они становятся самодостаточными и тестируемы независимо. Тогда сложная логика может быть реализована комбинацией готовых частей, поведение которых зафиксировано тестами.
Утрированно, пример с 2+2. Тестировать 2+2==4 не имеет смысла в силу отсутствия здесь бизнес требований. Что первично — два аргумента для суммирования, или возврат 4 — не оговорено.
Формализуя бизнес-требование — нам нужно складывать два числа — уже можно написать первый полезный тест, который фиксирует кол-во аргументов.
И при создании такого теста, мы как разработчики задумываемся — сегодня два аргумента, есть ли шанс что завтра будет 1 или 3. Шанс есть. Но требования нет. Значит имеет смысл за дешево вложить в архитектуру точку роста на случай изменений и не затачиваться внутри на два поля в структуре. Но и без фанатизма.
Затем приходит бизнес-требование — хотим вычитание. В этот момент — не надо копировать тесты сложения и изменять. Куда полезнее, задаться вопросом — что общего между двуми требованиями, а в чем разница — и обобщить реализацию, например выделив функтор. Немедленно закрепив бизнес-требование — юнит тестом, которое применяет функтор к паре чисел.
Итого — у нас реализовано два бизнес -требования, у нас два юнит-теста, которые надо поддерживать, и наша система уже готова к расширению кол-ва аргументов, или появлению новых операций. И вот эта готовность архитектуры бизнес-логики к непредсказуемым изменениям и есть основная полезность юниттестирования.
А в середине те же самые 2+2 с кровавым Enterprise
не совсем, не 2+2, а тогда уж x + 2, легко расширяемый до ax2+bx+c… и далее…
пример элементарный, но принцип очень важный — как покрывать тестами функционал содержащий зависимости и интеграции — применим к любой сложности.
Проблема в том, что бизнес-требование редко приходит в виде "нам нужно складывать два числа", чаще в виде "пользователь при регистрации в системе должен указать уникальное входное имя". Функционал оказывается размазанным по слоям (от UI до БД) и юнит-тестов на одно бизнес-требование нужно писать много больше одного.
Куда как интереснее и ценнее разделять бизнес-логику приложения на настолько элементарные части, что они становятся самодостаточными и тестируемы независимо. Тогда сложная логика может быть реализована комбинацией готовых частей, поведение которых зафиксировано тестами.
Полностью согласен. Но это уже искусство — разделить бизнес-требование, написанное человеком для человеков, на элементарные части, которые могут быть проверены юнит-тестами. IMHO, такого проверяемого юнит-тестами кода в моих приложениях может быть процентов 10-15 (у других может и 80-90-100%, но у меня вот такой опыт). В основном, сложная логика. Настолько сложная, что нужно применять TDD, чтобы понять, что должно получиться в результате.
поднимать в юнит тесте Spring контекст для проверки конкатенации в контроллере это чересчур.
вы про WebMvcTest? но это уже стандарт гонять тесты контроллера через MockMvc, вы поступаете иначе?
условие Mockito.atLeastOnce() допускает многократный вызов сервиса
согласен, нужно указать точнее
вы про WebMvcTest? но это уже стандарт гонять тесты контроллера через MockMvc, вы поступаете иначе?
Мы убираем всю бизнес-логику из контроллеров и в юнит тестах контроллеров проверяем только вызовы сервисов. Проверку поведения самого контроллера, как элемента инфраструктуры, мы выносим в интеграционные тесты. Тем самым экономится время разработчиков — при работе им достаточно регулярно прогонять быстрые юнит тесты, медленные интеграционные тесты выполняет CI-система.
при детальном покрытии быстрыми юнит тестами — долгие и тяжелые интеграционные могут вообще не понадобиться.
юниты дают несравнимо больше профита, и накапливают ценность в перспективе.
Увидев такое, и перечитав для верности дважды, соискатель восклицает: “Что за лютый бред?..” Ведь, у него в коде практически нет методов, которые все необходимое получают через аргументы, а затем отдают однозначный результат по ним. Это типичные утилитарные методы, и они практически не меняются. А как быть со сложными процедурами, с внедренными зависимостями, с методами без возврата значений? Там это подход не применим от слова “совсем”.
Печально, если такая ситуация. По хорошему, все функции с нетривиальной логикой стоит делать такими, чистыми: кроме аргументов ничего не используют, кроме возвращаемого значения ничего не делают. Ну ладно, не все — но большинство точно.
Тогда "грязные" функции почти не будут содержать логику, и покроются общими интеграционными тестами.
Как тестирование модуля может зафиксировать ожидаемое поведение системы?
Проблемы проявятся, если сервис будет возвращать json, а не string. Любое добавление поля приведет к тому, что нужно будет переписать каждый подобный тест.
это не проблема, а суть фиксации поведения. Некогда бизнес потребовал string, это реализовали и зафиксировали тестом. Теперь если теперь некий разработчик изменит это поведение — тест упадет, сигнализируя о несовместимости с прежними требованиями.
Если это ошибочно — уф, спасибо тебе, тест!
Если сознательно — меняется тест, фиксируются актуальные требования = ожидаемое поведение системы.
Тест кода — военнослужащий отдела технического контроля – готов по тревоге выполнить приказ.
Отказ от тестов выглядит как отказ от мобилизации.
Чтобы получить выгоды (времени, денег, нервов) от тестов:
— код нужен простой и «тупой», чтобы тесты были такими же, чтобы их можно было автоматически создавать.
— повторять себя — создавать сложные сценарии общением уже протестированных простых объектов, методов.
— на новые сложные сценарии придумывать простые, дешёвые тесты.
На каких-то участках кода придётся использовать спецназ — креативных, дорогих.
Но большая часть армии тестов: дешёвые, малообученные наёмники.
Принцип слоеного теста