Unit-тесты на C#

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

В процессе написания ПО у меня возникло понимание о целесообразности применения unit-тестов.

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

В каждой из поставленных задач unit-тесты оказались эффективны, позволив существенно сократить время работы и обеспечить своевременное обнаружение ошибок кода.

Согласно данным[1] исследований, цена ошибки в ходе разработки и поддержании ПО экспоненциально возрастает при несвоевременном их обнаружении.

Unit-тесты на C#

На представленном рисунке видно, что при выявлении ошибки на этапе формирования требований мы получим экономию средств в соотношении 200:1 по сравнению с их обнаружением на этапе поддержки.

Среди всех тестов львиную долю занимают именно unit-тесты. В классическом понимании unit-тесты позволяют быстро и автоматически протестировать отдельные части ПО независимо от остальных.

Рассмотрим простой пример создания unit-тестов. Для этого создадим консольное приложение Calc, которое умеет делить и суммировать числа.

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Calc { class Program { static void Main(string[] args) { } } }

Добавляем класс, в котором будут производиться математические операции.

using System; namespace Calc { /// <summary> /// Выполнение простых математических действий над числами /// </summary> public class Calculator { /// <summary> /// Получаем результат операции деления (n1 / n2) /// </summary> /// <param name="n1">Первое число</param> /// <param name="n2">Второе число</param> /// <returns>Результат</returns> public double Div(double n1, double n2) { // Проверка деления на "0" if (n2 == 0.0D) throw new DivideByZeroException(); return n1 / n2; } /// <summary> /// Получаем результат сложения чисел и их увеличения на единицу /// </summary> /// <param name="n1"></param> /// <param name="n2"></param> /// <returns></returns> public double AddWithInc(double n1, double n2) { return n1 + n2 + 1; } } }

Так, в методе Div производится операция деления числа n1 на число n2. Если передаваемое число n2 будет равняться нулю, то такая ситуация приведет к исключению. Для этого знаменатель этой операции проверяется на равенство нулю.

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

На следующем шаге добавим в решение проект тестов.

Unit-тесты на C#

Пустой проект unit-тестов:

using System; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CalcTests { [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { } } }

Переименуем наш проект: «SimpleCalculatorTests». Добавляем ссылку на проект Calc.

В проекте Calc содержатся 2 метода, которые надо протестировать на корректность работы. Для этого создадим 3 теста, которые будут проверять операцию деления двух чисел, операцию деления на нуль и операцию сложения двух чисел и инкрементацию полученной суммы.

Добавляем в проект тест для проверки метода AddWithInc.

/// <summary> /// Тест проверки метода AddWithInc /// </summary> [TestMethod] public void AddWithInc_2Plus3Inc1_Returned6() { // arrange var calc = new Calculator(); double arg1 = 2; double arg2 = 3; double expected = 6; // act double result = calc.AddWithInc(arg1, arg2); // assert Assert.AreEqual(expected, result); }

В тесте создаются 3 переменные — это аргументы, передаваемые в метод AddWithInc, и ожидаемый результат, возвращаемый этим методом. Результат выполнения метода будет записан в переменную result.

На следующем шаге происходит сравнение ожидаемого результата с реальным числом метода AddWithInc. При совпадении результата с ожидаемым числом, то есть числом 6, тест будет считаться положительным и пройденным. Если полученный результат будет отличаться от числа 6, то тест считается проваленным.

Следующим тестом мы будем проверять метод Div

[TestMethod] public void Div_4Div2_Returned2() { // arrange var calc = new Calculator(); double arg1 = 4; double arg2 = 2; double expected = 2; // act double result = calc.Div(arg1, arg2); // assert Assert.AreEqual(expected, result); }

Аналогичным образом создаются два аргумента и ожидаемый результат выполнения метода Div. Если результат деления 4/2 в методе равен 2, то тест считается пройдённым. В противном случае — не пройденным.

Следующий тест будет проверять операцию деления на нуль в методе Div.

[TestMethod] [ExpectedException(typeof(DivideByZeroException), "Oh my god, we can't divison on zero")] public void Div_4Div0_ZeroDivException() { // arrange var calc = new Calculator(); double arg1 = 4; double arg2 = 0; // act double result = calc.Div(arg1, arg2); // assert }

Тест будет считаться пройденным в случае возникновения исключения DivideByZeroException — деление на нуль. В отличии от двух предыдущих тестов, в этом тесте нет оператора Assert. Здесь обработка ожидаемого результата производится с помощью атрибута «ExpectedException».

Если аргумент 2 равен нулю, то в методе Divвозникнет исключение — деление на нуль. В таком случае тест считается пройденным. В случае, когда аргумент 2 будет отличен от нуля, тест считается проваленным.

Для запуска теста необходимо открыть окно Test Explorer. Для этого нажмите Test -> Windows -> Test Explorer (Ctrl+, T). В появившемся окне можно увидеть 3 добавленных теста:

Unit-тесты на C#

Для запуска всех тестов нажмите Test -> Run -> All tests (Ctrl+, A).

Если тесты выполнятся успешно, в окне Test Explorer отобразятся зеленые пиктограммы, обозначающие успешность выполнения.

Unit-тесты на C#

В противном случае пиктограммы будут красными.

Unit-тесты на C#

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

Случай написания тестов для бэкенда веб-приложения в моей практике является не совсем стандартным вариантом применения unit-тестов. В данной ситуации unit-тесты вызывали методы контроллера MVC-приложения, в то же время передавая тестовые данные в контроллеры.

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

Существуют случаи, когда модульные тесты применять нецелесообразно. Например, если вы веб-разработчик, который делает сайты, где мало логики. В таких случаях имеются только представления, как, например, для сайтов-визиток, рекламных сайтов, или, когда вам поставлена задача реализовать пилотный проект «на посмотреть, что получится». У вас ограниченные ресурсы и время. А ПО будет работать только один день — для показа руководству.

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

Для определения целесообразности использования unit-тестов можно воспользоваться следующим методом: возьмите лист бумаги и ручку и проведите оси X и Y. X — алгоритмическая сложность, а Y — количество зависимостей. Ваш код поделим на 4 группы.

Unit-тесты на C#
  1. Простой код (без каких-либо зависимостей)
  2. Сложный код (содержащий много зависимостей)
  3. Сложный код (без каких-либо зависимостей)
  4. Не очень сложный код (но с зависимостями)

Первое — это случай, когда все просто и тестировать здесь ничего не нужно.

Второе — случай, когда код состоит только из плотно переплетенных в один клубок реализаций, перекрестно вызывающих друг друга. Тут неплохо было бы провести рефакторинг. Именно поэтому тесты писать в этом случае не стоит, так как код все равно будет переписан.

Третье — случай алгоритмов, бизнес-логики и т.п. Важный код, поэтому его нужно покрыть тестами.

Четвертый случай — код объединяет различные компоненты системы. Не менее важный случай.

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

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

[1] Данные взяты из книги «Технология разработки программного обеспечения» автора Ларисы Геннадьевны Гагариной

44
6 комментариев

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

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

3
Ответить

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

1
Ответить

В чем фишка копипастить материал со своего же ресурса?

Ответить
Автор

Александр, спасибо за вопрос! Нам очень нравится площадка vc.ru, ее функционал, обратная связь от читателей, профессиональная аудитория. Будем очень рады, если кейсы и статьи, которые мы тут публикуем, будут полезны для аудитории vc.ru

3
Ответить

Нет, пусть здесь будет тоже.  На Хабре токсичнее :)  (почти шутка) 

1
Ответить

Кажется вы перепутали вкладки и запостили это на vc вместо хабра.

Ответить