Модульное тестирование с xUnit¶
В этом разделе будем использовать фреймворк xUnit. Этот фреймворк используется и в самом ASP.NET Core для тестирования, поэтому он стал чем-то используемым по соглашению. Однако, ничто не мешает использовать любой другой знакомый фреймворк.
Создание первого тестового проекта¶
В Visual Studio есть шаблон для создания тестового проекта xUnit: File > New Project > xUnit Test Project (.NET Core). Кроме того, создать проект можно с помощью командной строки:
Во вновь созданном проекте будет содержаться, помимо прочего, пример модульного теста. Вот он:
Здесь мы видим некоторые характеристики тестов xUnit:
- тесты обозначаются атрибутом
[Fact]; - метод теста должен быть открытым (public) и без аргументов;
- метод ничего не возвращает (void) или, если он асинхронный, возвращает
Task; - метод находится внутри открытого нестатического класса.
Запуск тестов командой dotnet test¶
Чтобы запустить тесты в Visual Studio, нужно либо через команду Tests > Run All Tests в главном меню, либо кнопкой Run All в обозревателе тестов (Test Explorer).
Также можно запустить тесты командой dotnet test — либо из папки с тестовым проектом — тогда будут запущены тесты этого проекта, либо из папки с файлом .sln — тогда будут выполнены все тесты из всех тестовых проектов решения (solution).
Ссылка на приложение из тестового проекта¶
Предположим, у нас есть некое приложение для конвертации валют под названием ExchangeRates.Web. Добавим тестовый проект ExchangeRatesWeb.Tests и добавим ExchangeRates.Web в зависимости этого проекта.
От меня
В книге подробно описано, как добавить один проект из решения в зависимости другого. Но мы все и так хорошо знаем, как это сделать, не правда ли? 😉
Общепринятые соглашения для расположения проекта
Расположение и именование проектов полностью зависят от разработчика, однако в проектах ASP.NET Core обычно используются несколько соглашений, отличных от используемых при выполнении команды File > New. Эти соглашения применяются командой ASP.NET в GitHub, а также во многих других проектах C# с исходным кодом. Вкратце они выглядят так:
- файл решения с расширением .sln находится в корневом файле проекта;
- основные проекты помещаются во вложенный каталог src;
- тестовые проекты помещаются во вложенный каталог test или tests;
- у каждого основного проекта есть эквивалент тестового проекта, названный также, как и связанный с ним основной проект, с суффиксом “.Test” или “.Tests”;
другие папки, такие как samples, tools или docs, содержат примеры проектов, инструменты для создания проекта или документацию.
Пусть в основном приложении есть простой класс, используемый для конвертации валют:
public class CurrencyConverter
{
//Метод преобразует значение, используя указанный обменный курс, и округляет его.
public decimal ConvertToGbp(decimal value, decimal exchangeRate, int decimalPlaces)
{
if (exchangeRate <= 0)
{
throw new ArgumentException(
"Exchange rate must be greater than zero",
nameof(exchangeRate));
}
var valueInGbp = value / exchangeRate;
return decimal.Round(valueInGbp, decimalPlaces);
}
}
Далее напишем тесты для этого класса.
Добавление модульных тестов с атрибутами Fact и Theory¶
Начнем с простого модульного теста, проверяющего работоспособность с типичными входными значениями:
[Fact]
public void ConvertToGbp_ConvertsCorrectly()
{
var converter = new CurrencyConverter();
decimal value = 3;
decimal rate = 1.5;
int dp = 4;
decimal expected = 2;
var actual = converter.ConvertToGbp(value, rate, dp);
Assert.Equal(expected, actual);
}
Этот тест написан при помощи паттерна Arrange-Act-Assert (AAA):
- Arrange — определить все параметры и создать экземпляр тестируемой системы;
- Act — выполнить тестируемый метод и зафиксировать результат;
- Assert — убедиться, что результат этапа Act имеет ожидаемое значение.
В примере Assert — вспомогательный класс, используемый для установки утверждений (assertions) в отношении тестируемого кода. Например Assert.Equal выбросит исключение, если предоставленные параметры не будут равны. При выбросе исключения тест завершается неудачно.
Совет
Существуют альтернативные библиотеки утверждений, например Fluent Assertions и Shouldly, которые позволяют писать утверждения в более естественном стиле, например actual.Should().Be(expected), а также выдают более понятные сообщения об ошибках.
В предыдущем примере мы выбрали определенные значения для value, exchangeRate и decimalPlaces, однако логично было бы протестировать хотя бы несколько различных комбинаций.
Чтобы не писать свой код для каждого набора параметров, можно воспользоваться ледующим способом. Вместо метода тестирования с атрибутом [Fact] создадим метод тестирования с атрибутом [Theory] — параметризованный метод тестирования.
Перепишем наш тест с использованием атрибута [Theory]. Значения переменных будем передавать в виде параметров метода, декорировав метод атрибутами [InlineData]:
[Theory]
[InlineData(0, 3, 0)]
[InlineData(3, 1.5, 2)]
[InlineData(3.75, 2.5, 1.5)]
public void ConvertToGbp_ConvertsCorrectly(decimal value, decimal rate, decimal expected)
{
var converter = new CurrencyConverter();
int dps = 4;
var actual = converter.ConvertToGbp(value, rate, dps);
Assert.Equal(expected, actual);
}
При выполнении тестов этот тест будет отображаться в виде трёх тестов — по числу заданных наборов параметров.
Помимо
[InlineData] можно передавать параметры через статическое свойство тестового класса с атрибутом [MemberData] или пометить сам класс атрибутом [ClassData]1
Тестирование условий отказа¶
Ключевой частью модульного тестирования является проверка того, что тестируемая система правильно обрабатывает граничные случаи и ошибки. Так, для нашего примера нужно проверить что наш класс верно реагирует на недопустимые значения обменного курса.
[Fact]
public void ThrowsExceptionIfRateIsZero()
{
var converter = new CurrencyConverter();
decimal value = 1;
decimal rate = 0;
int dp = 2;
var ex = Assert.Throws<ArgumentException>(() => converter.ConvertToGbp(value, rate, dp));
//Дальнейшие проверки сгенерированного исключения ex
}
Здесь метод
Assert.Throws выполняет переданную лямбду и перехватывает исключение, проверяя, что перехваченное исключение нужного типа. Иначе будет выброшено новое исключение, и тест завершится с ошибкой.
-
Использование атрибутов
[InlineData],[MemberData]и[ClassData]описано автором в этом посте его блога. ↩
Дата создания : 25 ноября 2022 г.