Время прочтения: 7 мин.
В процессе написания ПО у меня возникло понимание о целесообразности применения unit-тестов.
Так в моей практике появилось несколько проектов, в которых мне довелось писать unit-тесты, каждый из которых выполнял определенную роль – поиск ошибок в основных алгоритмах кода, нагрузочное тестирование и отладка бэкенда веб-приложения. В каждой из поставленных задач unit-тесты оказались эффективны, позволив существенно сократить время работы и обеспечить своевременное обнаружение ошибок кода.
Согласно данным[1] исследований, цена ошибки в ходе разработки и поддержании ПО экспоненциально возрастает при несвоевременном их обнаружении.
На представленном рисунке видно, что при выявлении ошибки на этапе формирования требований мы получим экономию средств в соотношении 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-тестов:
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+E,T). В появившемся окне можно увидеть 3 добавленных теста:
Для запуска всех тестов нажмите Test -> Run -> All tests (Ctrl+R,A).
Если тесты выполнятся успешно, в окне Test Explorer отобразятся зеленые пиктограммы, обозначающие успешность выполнения.
В противном случае пиктограммы будут красными.
Unit-тесты имеют обширную, строго не регламентированную область применения — зачастую фантазия самого автора кода подсказывает решение нестандартных задач с помощью этого инструмента.
Случай написания тестов для бэкенда веб-приложения в моей практике является не совсем стандартным вариантом применения unit-тестов. В данной ситуации unit-тесты вызывали методы контроллера MVC-приложения, в то же время передавая тестовые данные в контроллеры. Далее в режиме отладки шаг за шагом выполнялись все действия алгоритма. В этом случае применение тестов позволило произвести быструю отладку бэкенда веб-приложения.
Существуют случаи, когда модульные тесты применять нецелесообразно. Например, если вы веб-разработчик, который делает сайты, где мало логики. В таких случаях имеются только представления, как, например, для сайтов-визиток, рекламных сайтов, или, когда вам поставлена задача реализовать пилотный проект «на посмотреть, что получится». У вас ограниченные ресурсы и время. А ПО будет работать только один день – для показа руководству.
Сжатые сроки, малый бюджет, размытые цели или довольно несложные требования – случаи, в которых вы не получите пользы от написания тестов.
Для определения целесообразности использования unit-тестов можно воспользоваться следующим методом: возьмите лист бумаги и ручку и проведите оси X и Y. X — алгоритмическая сложность, а Y – количество зависимостей. Ваш код поделим на 4 группы.
- Простой код (без каких-либо зависимостей)
- Сложный код (содержащий много зависимостей)
- Сложный код (без каких-либо зависимостей)
- Не очень сложный код (но с зависимостями)
Первое – это случай, когда все просто и тестировать здесь ничего не нужно.
Второе – случай, когда код состоит только из плотно переплетенных в один клубок реализаций, перекрестно вызывающих друг друга. Тут неплохо было бы провести рефакторинг. Именно поэтому тесты писать в этом случае не стоит, так как код все равно будет переписан.
Третье – случай алгоритмов, бизнес-логики и т.п. Важный код, поэтому его нужно покрыть тестами.
Четвертый случай – код объединяет различные компоненты системы. Не менее важный случай.
Последние два случая — это ответственная логика. Особенно важно писать тесты для ПО, которые влияют на жизни людей, экономическую безопасность, государственную безопасность и т.п.
Подводя итог всего описанного выше хочется отметить, что тестирование делает код стабильным и предсказуемым. Поэтому код, покрытый тестами, гораздо проще масштабировать и поддерживать, т.к. появляется большая доля уверенности, что в случае добавления нового функционала нигде ничего не сломается. И что не менее важно — такой код легче рефакторить.
[1] Данные взяты из книги «Технология разработки программного обеспечения» автора Ларисы Геннадьевны Гагариной