Перейти к содержанию

Модульное тестирование с xUnit

В этом разделе будем использовать фреймворк xUnit. Этот фреймворк используется и в самом ASP.NET Core для тестирования, поэтому он стал чем-то используемым по соглашению. Однако, ничто не мешает использовать любой другой знакомый фреймворк.

Создание первого тестового проекта

В Visual Studio есть шаблон для создания тестового проекта xUnit: File > New Project > xUnit Test Project (.NET Core). Кроме того, создать проект можно с помощью командной строки:

dotnet new xunit

Во вновь созданном проекте будет содержаться, помимо прочего, пример модульного теста. Вот он:
public class UnitTest1
{
    [Fact]
    public void Test1()
    {
    }
}

Здесь мы видим некоторые характеристики тестов 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 выполняет переданную лямбду и перехватывает исключение, проверяя, что перехваченное исключение нужного типа. Иначе будет выброшено новое исключение, и тест завершится с ошибкой.


  1. Использование атрибутов [InlineData], [MemberData] и [ClassData] описано автором в этом посте его блога


Последнее обновление : 14 мая 2023 г.
Дата создания : 25 ноября 2022 г.

Комментарии

Комментарии