Тестирование в .NET. Часть 2 — MSTest

21 января, 2021DEV

Привет. В прошлой статье я немного рассказал о тестировании программного обеспечения в целом и вообще, сегодня мы попробуем написать несколько первых тестов при помощи фреймворка тестирования MSTest (Microsoft Test). Это простой, лёгкий и удобный вариант для быстрого тестирования личных или не очень больших проектов. Хотя, конечно, никто не запрещает покрывать все свои проекты при помощи данной технологии, но это уже больше вкусовщина и дело ньюансов, о которых я постараюсь поговорить чуть ниже.

Итак, для того, чтобы начать тестировать код — нам в первую очередь понадобится код 🙂 Да, в этой статье мы не поднимаем тему TDD и подходу написания программного обеспечения через тестирования (test first) — об этом когда-нибудь потом. Сейчас наша цель иметь возможность покрыть уже написанный код или код, который создаётся прямо сейчас, в конкретный момент.

Подготовка.

Для нашей первой, пробной реализации проверки кода я выбрал банальный и просто пример калькулятора. Буквально в несколько строк и с кучей проблемных мест, но так даже лучше:

public class Calc
{

	public decimal Value { get; private set; }

	public Calc(decimal value) => this.Value = value;

	public void Reset() => this.Value = 0;

	public void Set(decimal v) => this.Value = v;

	public decimal Sum(decimal v) => this.Value = this.Value + v;

	public decimal Min(decimal v) => this.Value = this.Value - v;

	public decimal Div(decimal v) => this.Value = this.Value / v;

	public decimal Mul(decimal v) => this.Value = this.Value * v;

}

На всякий случай стоит ещё раз отдельно оговорить, что это очень плохой калькулятор 🙂 во всех смыслах, единственная причина существования которого — наиболее простая демонстрация тестов.

Здесь мы видим класс Calc и несколько методов, которые работают с локальной переменной (доступной через свойство) Value. Здесь мы можем проводить примитивные арифметические операции, такие как сложение, вычитание, умножение и деление: Sum, Min, Mul и Div, соответственно. Каждый из данных методов принимает в аргументах одну переменную decimal v, которую мы прибавляем, отнимаем, на которую умножаем или делим. Никаких проверок. Пока что так, будем считать это первой версией.

Теперь давайте добавим к своему Solution новый проект с типом MSTest Test Project (данный тип существует в обоих вселенных — в старом добром .NET Framework и в новом прекрасном .NET Core). Назовём его Tests:

У нас сгенерируется простой проект с одним классом, который переименуем в CalcTests. Теперь необходимо добавить проект Calc в референсы CalcTests, чтобы наш тестовый проект получил доступ и видимость к нашему коду. По сути это все приготовления, всё очень просто — в Solution Explorer кликаем правой кнопкой мыши по меню Dependencies и выбираем Add Project Reference, как показано на рисунке:

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

Фреймворк MSTest.

В основе тестирования лежит идея о том, что мы, как разработчики, знаем наверняка то, что нам требуется. Конкретно и по существу. Поэтому нам нужен инструмент этой самой проверки желаемого результата и суровой действительности. Для этого у нас есть статический класс Assert с массой статических методов для проверки.

Как правило, в основном это методы с двумя аргументами — expected и actual, т.е. то, что мы ожидаем и то, что мы получили. В случае если наше поведение не соответствует нашему ожиданию — тест «валится», становится красным и мы при помощи Test Explorer можем понять что и где пошло не так. Также, в перегрузках методов можно найти строковую переменную message и задать текст ошибки — в каждом отдельном случае.

Также следует отметить, что существует также и методы, внутрь которых не нужно передавать эти самые expected и actual. Для ситуаций, когда нам нужно проверить, что мы ожидаем получить какой-либо Exception, к примеру, существует метод ThrowsException.

Методы

Теперь немного о каждом из методов более подробно:

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

AreNotEqual — как следует из названия, функция, обратная по смыслу AreEqual — тест проходит в случае, если аргументы НЕ являются эквивалентными и падает, если они равны, соответственно.

AreSame — проверка эквивалентности по указателю. В случае если оба объекта имеет одинаковую ссылку и ведут на один и тот же объект — тест проходит, в противном случае — падает.

AreNotSame — обратная проверка AreSame на НЕ одинаковую ссылку.

IsTrue, IsFalse — проверка предикатов на истинность или ложь.

IsNull, IsNotNull — проверка на то, что объект является пустым (null) или же наоборот, что объект не является пустым.

IsInstanceOfType, IsNotInstanceOfType — проверка на соответствие типов объекта.

ThrowsException — проверка на то, что ожидаемым поведением должен быть Exception.

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

Атрибуты

Для того, чтобы фреймворк знал какие из наших методов и классов связаны с тестами и как с ними работать вообще — используются специальные атрибуты, со своим жизненным циклом: сначала мы инициализируем настройки сборки, далее идём в настройки класса, после чего попадаем в тест метод, из него чистим класс и в самом конце чистим сборку: AssemblyInitialize, TestClass, TestInitialize, TestMethod, TestCleanup, AssemblyCleanup.

Как видно из названия, часть из атрибутов ориентированы на работу с глобальным состоянием. В частности, AssemblyInitialize начинает работу первым и единожды на весь запуск тестов. Здесь мы можем получить какие-то глобальные настройки, проинициализировать необходимые нам данные (к примеру, базу данных). Соответственно, AssemblyCleanup отработает в самом конце и также только один раз — специально для того, чтобы вы «почистили» то, что необходимо почистить — закрыли соединения, провели какие-то постработы. Для обоих данных атрибутов требуется статические методы и данные атрибуты можно указать в проекте лишь один раз, иначе тесты не заведутся.

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

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

TestClass — атрибут, который мы отмечаем класс с тестовыми методами.

TestMethod, соответственно, как видно из названия — для методов.

Также существуют специальные мета атрибуты для организации и структурирования наших тестов. Речь идёт о TestCategory, при помощи которого мы можем указать к какой группе относится данный метод. К слову, группировка в Test Explorer доступна по файлам и классам, таким образом при грамотном распределении тестов на файловой системе — надобность задавать категории в ручном режиме может отпасть.

Тестируем калькулятор

Если я хорошо помню арифметику, то дважды два ровняется четырем. Давайте с этого и начнём. Мы ожидаем, что результат умножения 2 на 2 вернёт нам 4:

[TestMethod]
[TestCategory("Arithmetic")]
public void Calc_Mul_Test()
{
	//Arrange
	Calc calc = new Calc(2);

	//Act
	decimal r1 = calc.Mul(2);
	decimal r2 = calc.Mul(2);
	decimal r3 = calc.Mul(2);
	decimal r4 = calc.Mul(2);
	decimal r5 = calc.Mul(2);

	//Assert
	Assert.AreEqual(4, r1);
	Assert.AreEqual(8, r2);
	Assert.AreEqual(16, r3);
	Assert.AreEqual(32, r4);
	Assert.AreEqual(64, r5);
}

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

[TestMethod]
[TestCategory("Arithmetic")]
public void Calc_Div_Test()
{
    //Arrange
    Calc calc = new Calc(60);

    //Act
    decimal r1 = calc.Div(1);
    decimal r2 = calc.Div(3);
    decimal r3 = calc.Div(5);
    decimal r4 = calc.Div(2);
    decimal r5 = calc.Div(8);

    //Assert
    Assert.AreEqual(60, r1);
    Assert.AreEqual(20, r2);
    Assert.AreEqual(4, r3);
    Assert.AreEqual(2, r4);
    Assert.AreEqual(0.25m, r5);
}

И, конечно, же не забываем про сложение и вычитание:

[TestMethod]
[TestCategory("Arithmetic")]
public void Calc_Sum_Test()
{
    //Arrange
    Calc calc = new Calc(0);

    //Act
    decimal r1 = calc.Sum(1);
    decimal r2 = calc.Sum(1);
    decimal r3 = calc.Sum(1);
    decimal r4 = calc.Sum(2);
    decimal r5 = calc.Sum(3);

    //Assert
    Assert.AreEqual(1, r1);
    Assert.AreEqual(2, r2);
    Assert.AreEqual(3, r3);
    Assert.AreEqual(5, r4);
    Assert.AreEqual(8, r5);
}
[TestMethod]
[TestCategory("Arithmetic")]
public void Calc_Min_Test()
{
    //Arrange
    Calc calc = new Calc(100);

    //Act
    decimal r1 = calc.Min(1);
    decimal r2 = calc.Min(3);
    decimal r3 = calc.Min(5);
    decimal r4 = calc.Min(7);
    decimal r5 = calc.Min(9);

    //Assert
    Assert.AreEqual(99, r1);
    Assert.AreEqual(96, r2);
    Assert.AreEqual(91, r3);
    Assert.AreEqual(84, r4);
    Assert.AreEqual(75, r5);
}

С нашими примитивными операциями мы закончили. Осталось два метода, которые мы ещё не покрыли — это сброс калькулятор в ноль Reset и установка значения Set:

[TestMethod]
[TestCategory("Common")]
public void Calc_Set_Test()
{
    //Arrange
    Calc calc = new Calc(100);

    //Act
    calc.Set(32768);

    //Assert
    Assert.AreEqual(32768, calc.Value);
}

[TestMethod]
[TestCategory("Common")]
public void Calc_Reset_Test()
{
    //Arrange
    Calc calc = new Calc(100);

    //Act
    calc.Reset();

    //Assert
    Assert.AreEqual(0, calc.Value);
}

Давайте вернёмся к делению. Все мы знаем, что делить на ноль нельзя и что результатом данной операции будет DivideByZeroException. Мы можем посчитать это поведение допустимым для нас и добавить его в тестирование следующим образом:

[TestMethod]
[TestCategory("Exceptions")]
public void Calc_Div_Exceptions_Test()
{
	//Arrange
	Calc calc = new Calc(100);

	//Act && Assert
	Assert.ThrowsException<DivideByZeroException>(() => { calc.Div(0); });
}

Выводы.

Как видите, всё очень просто. Пишем код, проверяем его поведение на предмет ожидания и адекватности работы. MSTest — это лёгкий (и с точки зрения точки входа, и с точки зрения лёгковесности решения), простой способ контролировать работоспособность всей системы во время написания нового кода и изменения старого. Теперь вы сможете с лёгкостью обнаружить проблемные места и на порядок увеличить скорость разрешения ошибок.

Leave a comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Prev Post Next Post