Comments 60
Хорошая статья, положу в закладки!
у меня вопрос по существу, я совсем ничего не понимаю в юнит-тестах, поскажите пожалуйста, что следовало бы тестировать и как, в приложении для учёта расходов
у него, собственно, две функции
добавление в базу расхода
отображение из базы расходов
Вообще, статья как раз о том, что и как тестировать. Вы точно дочитали до конца? Исходя из вашего коммента (не очень много информации) я предложил бы вам точно протестировать добавление и отображение «расходов» ;)
протестировать добавление и отображение «расходов» ;)

до этого я сам дотумкал, спасибо =)
другой вопрос, как тестировать?
делать добавление и сразу чтение, чтобы проверить, что добавилось именно то, что я добавлял в базу?
нет, вам нужно отдельно тестировать слои приложения, отвечающие за Data Layer, а в другие классы передать фейки ваших Data Layer — объектов. Правило простое: один класс — один тестирующий класс. Если вы будете сразу тестировать с БД — это несколько слоев приложение. Это интеграционный, а не юнит-тест.
каждый метод имеющий сколь бы то ни было сложную логику. Геттеры и сеттеры например покрывать не нужно — хотя некоторые товарищи пишут тесты и для них, ради красивой цифры в coverage report =)
Вот бы это стало серией статей. С указанием и подробным описанием инструментов тестирования. Такую Хабранеделю я бы лучше запомнил, чем GTD недели и недели статей про руководителей.
Я бы тоже почитал про создание тестов, т.к свсем начинающий разработчик и хочется изначально прививать себе правильные рефлексы
Шоукейсы перечисленных изоляционных фреймворков можно посмотреть тут.

У кого-нибудь удалось запустить данные шоукейсы?
И вот как всегда в статье о Unit-тестировании примеры тестирования методов add и multiply. Уснул где-то в том месте, когда от первого перешли ко второму. Дальше пошли всякие «умные» вещи, которые хорошо ложаться в идеологию юнит-тестинга.

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

Почему во всех статьях о юнит-тестах в качестве примеров идёт такая легкотня?

И да.
// arrange
var calc = new Calculator();
    
// act
var res = calc.Sum(2,5);

// assert
Assert.AreEqual(7, res);


Такая форма записи гораздо легче читается, чем


 Assert.AreEqual( 7, new Calculator().sum(2,5) );



Нифига, имхо как раз нижний код читается намного легче
Калькулятор — де факто, — уже промышленный стандарт примеров тестирования. Я в этих местах писал о форме записи и способах декомпозиции. Пример умышленно упрощен, чтобы акцентировать не тестируемом методе, а на способах решения выше обозначенных проблем.
Калькулятор — де факто, — уже промышленный стандарт примеров тестирования

Ну я про это и говорю. Hello-world прям. Вот только толку от этого примера — ноль. Красиво только выглядит.
Про форму записи — дело не в чтение — курите рефакторинг «излишний ввод локальных переменных», хотя понятно, что не надо доводить до идиотизма (т.е. функционального программирования).
Простите, Чак, мы тут так, случайно заглянули. :)

Вспомнилось: “It works on my machine” always holds true for Chuck Norris.
Спасибо, вы очень доходчиво пишете. Хотелось бы увидеть от вас статьи и об автоматизации тестирования UI, если вы это используете.
>>Вернемся к примеру с калькулятором
А что, до этого в тексте уже был код с калькулятором? Не видно!
>>Каждый тест должен проверять только одну вещь
Более точная формулировка будет такой «Одна концепция на тест». А то не совсем понятно что такое «вещь»?
> Есть всего три причины, почему тест перестал проходить

:) Хотя бы честно, теперь при тестировании мне нужно ловить не только одну причину (собственно баги), а еще и баги теста, и необходимость поддержки… Ну, минусы видны невооруженным взглядом — когда плюсы начнутся?
> Тесты – такой-же код. Разница только в том, что у тестов другая цель – обеспечить качество вашего приложения.

Ага, и типа без тестов мы уже качественно ПО писать не умеем — спасибо, умиляет :)
> Если вы сначала пишете код, вам возможно, придется его менять, чтобы сделать тестируемым.

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

ага, а программировать когда?
Ага, а должно быть наоборот… баги не кончаются никогда
Я имел в виду нечто иное. При правильном подходе к написанию автоматизированных тестов количество багов в каждом новом релизе должно снижаться. Раз вы меньше тратите время на исправление багов, у вас больше времени на новые фичи. Все просто.
Немаловажный момент то, что тесты позволяют выловить баг, порой, до попадания кода в VCS. «Цена» исправления, в таком случае, гораздо ниже, чем при хот-патчах на продакшне с последующими мержами в основную ветку разработки.
Баги не кончатся, но наша задача не искоренить все баги. Задача — минимизировать количество ошибок и регрессии в основных юз-кейсай нашего приложения. Это вполне посильная задача.
Ну, как ДОЛЖНО снижаться, и как происходит на самом деле — РАЗНЫЕ ВЕЩИ.
На моем опыте так и происходит. Результаты появляются с третьего-четвертого спринта. Иначе бы я не писал этой статьи.
Это спорное утверждение, вам может так казаться, потому что вы вложили не мало затрат на модульное тестирование. В то время как после выпуска определенной прикладной части — есть просто уровень распределения ошибок в виде «горба» — т.е. ровно так же и при ручном тестировании ошибки в готовом ПО практически исчезают… а тесты остаются невостребованными далее…
Впрочем замедти — что я не возражаю против автоматизации интеграционных тестов — но вот почему то про это никто не пишет, а именно это я считаю наиболее важным и не против был бы это начать эксплуатировать…
> «этот ваш тестируемый дизайн» нарушает инкапсуляцию

Конечно нарушает, а ваши две причинки совсе не о том. Инкапсуляция нужна для того, чтобы другие программисты не вызывали бы то, что не нужно! Для того, чтобы они писали код в соответствии с публичным интерфейсом класса, а не делали ли бы все с черного хода… А тут вы сами портите качество кода ради «тестируемости», а еще каком то качестве кода во время приведения к тестируемости говорите… смешно.
Вы, видимо, не читали статьи. Сеттеры можно оставить в реализации, а работать по интерфейсу, в котором сеттера не будет, зависимости можно внедрить через конструктор. Если ваши объекты инстанцируются через фабрику или IOC-контейнер до конструктора вы тоже не дотянитесь.

Черный ход здесь не при чем. Качество кода ради тестируемости на падает, а улучшается, т.к. тестируемый код подразуевает слабую связь компонентов.
Знаете если тестирование хоть как то влияет на архитектуру — значит это плохое тестирование. Задача тестирования одна — найти максимальное число ошибок. а все остальное от лукавого… когда вы ради тестирования вводите интерфейсы или изменения в конструкторах — то вы уже изменяете код, а этого не должно быть.
> Архитектура не тестируема
> У нас есть жесткие связи, костыли и прочие радости жизни.

В огороде бузина, а в Киеве дядька! Ну, с какого перепугу вы все это смешали вместе?
> Каждый тест должен проверять только одну вещь

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

Если и есть смысл в автоматическом тестировании — то только для сложных интеграционных тестов.
> Как «измерить» прогресс

А вот попробуйте лучше измерить так — время потраченная на всю эту лабуду с созданием юнит-тестов разделите на время ручного тестирования при отсутствии этого… а еще лучше разделите зарплату программиста на зарплату тестировщика
Хорошие QA стоят примерно столько-же, сколько хорошие программисты. В гугле существует должность software developer in test
И наконец, возвращаясь к Чаку — господа, вы просто забыли принципы хорошего структурного программирования, все это юнит тестирование более качественно заменяется тем, что функция должна содержать проверку граничных условий для входов и выходов функции… и тогда действительно код объясняет клиенту, что его требования — при введенной информации нельзя выполнить, это еще называется дружественный интерфейс (кто в танке), таким образом, пишите самотестирущиеся программы и проверяйте граничные условия — и не тратьте время на лабуду.
Если функция содержит ошибку, то что вам даст проверка входных параметров и как вы собираетесь проверять выход функции, вызовом той же функции с теми же параметрами?
А юнит-тестирование как раз проверяет корректность работы функции с заданными параметрами, сравнивая ее результат с заранее известным результатом для этих параметров.
Т.е. как делать проверку в математике начальной школы не проходили?, для сложения x = y + z, надо перепроверить y = x — z и etc. В качественно написанном коде, такого рода проверки есть сами по себе.
Бессмысленно делать модульное тестирование, когда в нем нет никакой необходимости. А вы собственно похоже не понимаете, что на таком анализе ограничений построенно все функциональное программирование… только там это излишнее «парадно» представлено (и сделано через ж..), а суть как раз именно в том, что я написал выше…
Статья интересная и полезная, правда, она явно написана не для чайников, в ней много недомолвок (в частности, не описан принцип именования, хотя, например мне, очевиден: что-тестируем_что-делаем_ожидаемый-результат, но, думаю, это должно явно указываться; в более сложных примерах не описаны интерфейсы, не показана реализация методов, в целом выглядят перегруженно и выдернутыми из контекста, может просто я излишне придираюсь, хотя, пример с умножением хороший), но, самое главное, после ее прочтения чайник так и не узнает как создавать тесты. Не хватает пошаговых инструкций как начать писать тесты (установить то-то, создать то-то, написать то-то, запустить, исправить, повторить, etc..).
Сейчас как раз изучаю модульное тестирование, и возникает следующий вопрос.
Для тестирования модульного нам необходимо разбить программный код на части и предоставить каждой части поддельные внешние зависимости, иначе это будет уже тестирование интеграционное. Есть ли какие-либо общие рекомендации, что считать в данном случае модулем, а что внешней зависимостью? Модуль здесь — это обязательно класс или может быть компонент системы, состоящий из нескольких связанных классов? Внешние зависимости — это только БД, сеть, файловая система или связи между классами, компонентами?
Вопрос связан с тем, что большая часть проблем возникает именно на связи классов.
Страустрап в своей книге о C++ предлагает следующий подход к ООП в целом: если «это» действие — сделайте метод. Если несколько действий объединены общим смыслом и/или процессом — объявите класс. Если придерживаться этого правила, то автоматом класс будет модулем вашего приложения.

Внешняя зависимость — это все, что делает ваши тесты не правдивыми и сложно-поддерживаем. Файловая система — зависимость: структура каталогов может быть другой на другой машине. БД — зависимость, ее может не быть на другой машине. Веб-сервис — зависимость: может не быть интернета или может быть злобный фаервол, а сервис, вообще может взять и упасть, скажем, от Хабра-эффекта.

Спросите себя: «будет ли этот компонент вести себя так же на другой машине?». Если ответ «нет»: нужно его подменить. Если ответ «да» — оставьте его.

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

Существует перегиб в обратную сторону. В readme NHibernate'а, не знаю, как сейчас, в прошлых релизах был пункт «пожалуйста, не тестируйте NHibernate, у нас есть свои тесты. Тестируйте вашу бизнес-логику».
>>Простой код без зависимостей. Скорее всего здесь и так все ясно. Его можно не тестировать.
Хм, практика показывает, что со временем простой код становится сложным :). А написание тестов постфактом и выборочно приводит к тому, что тесты существуют только для покрытия (тоесть применяется только вторая метрика — 100% покрытие есть — мы счастливы. То что колличество багов не стало меньше — никого не волнует).
Это зависит от того вашего стиля работы. Под очень простым кодом я понимаю гетеры, сетеры, экшны контроллеров вида return View(). Если во время делать рефакторинг, простой код остается простым. Если по каким-то причинам он становится сложнее, вы же можете сразу написать тест на этот «усложнившийся» участок.

Я думаю, что вопрос покрытия следует решать индивидуально каждой команде. Нужно пробовать и выбирать то, что работает для вас, а не то «что доктор прописал». А про «красивую» цифру 100% — это вы абсолютно верно подметили.
>>Под очень простым кодом я понимаю гетеры, сетеры, экшны контроллеров вида return View()

Я как раз и говорил о том, что такой код имеет тенденцию становится больше. Например — мы добавляем контроль формата в сеттер, кеш в геттер. И в этот момент мы и получаем ситуацию, когда тесты пишутся ПОСЛЕ написания кода.

И тут получается ситуация, когда функционал сделан, а покрытие — нет. Если присутствует некое довление со стороны менеджмента, или просто много работы — велик соблазн написать тест для галочки, или не писать вовсе.
Статья интересная и полезная, правда, она явно написана не для чайников, в ней много недомолвок (в частности, не описан принцип именования, хотя, например мне, очевиден: что-тестируем_что-делаем_ожидаемый-результат, но, думаю, это должно явно указываться; в более сложных примерах не описаны интерфейсы, не показана реализация методов, в целом выглядят перегруженно и выдернутыми из контекста, может просто я излишне придираюсь, хотя, пример с умножением хороший), но, самое главное, после ее прочтения чайник так и не узнает как создавать тесты. Не хватает пошаговых инструкций как начать писать тесты (установить то-то, создать то-то, написать то-то, запустить, исправить, повторить, etc..).
Огромное вам спасибо. При публикации я «потерял» кусок статьи. Благодаря вам я это заметил и добавил недостающий текст.
По многочисленным просьбам будет продолжение этой статьи (практическая часть) с большим количеством примеров и пошаговыми инструкциями «как начать писать тесты».
Only those users with full accounts are able to leave comments. Log in, please.