Pull to refresh

Comments 65

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

до этого я сам дотумкал, спасибо =)
другой вопрос, как тестировать?
делать добавление и сразу чтение, чтобы проверить, что добавилось именно то, что я добавлял в базу?
нет, вам нужно отдельно тестировать слои приложения, отвечающие за Data Layer, а в другие классы передать фейки ваших Data Layer — объектов. Правило простое: один класс — один тестирующий класс. Если вы будете сразу тестировать с БД — это несколько слоев приложение. Это интеграционный, а не юнит-тест.
каждый метод имеющий сколь бы то ни было сложную логику. Геттеры и сеттеры например покрывать не нужно — хотя некоторые товарищи пишут тесты и для них, ради красивой цифры в coverage report =)
Сеттеры и геттеры покрывать вообще вредно.
и я свою домашнюю бухгалтерию на Asp.Net'е пишу. Ой.
Вот бы это стало серией статей. С указанием и подробным описанием инструментов тестирования. Такую Хабранеделю я бы лучше запомнил, чем 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..).
Огромное вам спасибо. При публикации я «потерял» кусок статьи. Благодаря вам я это заметил и добавил недостающий текст.
По многочисленным просьбам будет продолжение этой статьи (практическая часть) с большим количеством примеров и пошаговыми инструкциями «как начать писать тесты».
Отличная статья, спасибо (даже в 2020 году)! Можно ссылку на следующую статью?)
К сожалению, нет. Я так и не написал продолжение. Может быть, когда-нибудь…
Думаю 2 и 5 являются блоком arrange

class CalculatorTests
{
	public void Sum_2Plus5_7Returned()
	{
		// arrange
		var calc = new Calculator();
                var arg1 = 2;
                var arg2 = 5;
	
		// act
		var res = calc.Sum(arg1, arg2);

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

Возможно для данного примера это излишне, но когда в метод передается объект в определенном состоянии, он будет предварительно дан, как и 2 и 5 по идее результаты предварительных вычислений.
С серьезным покрытием. Все тесты проходят.
...
Система надежно протестирована и сама рассказывает о себе путем тестов.

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

       public AccountManagementController()
          :this(
             OrderManagerFactory.GetOrderManager(),
             UserManagerFactory.Get()
        )
        {
        }

        /// <summary>
        /// For testability
        /// </summary>
        /// <param name="accountData"></param>
        /// <param name="userManager"></param>
        public AccountManagementController(
            IAccountData accountData,
            IUserManager userManager)
        {
            _accountData = accountData;
            _userManager = userManager;
            _disabledAccountsFilter = new FilterParam("Enabled", Expression.Eq, true);
        }
Sign up to leave a comment.

Articles