Модульное тестирование с помощью xUnit в ASP.NET Core MVC

Внимание

Данный материал является частью цикла статей «Тестирование в ASP.NET Core». Не забудьте посмотреть другие статьи по этой теме :-)

  1. Модульное тестирование с помощью xUnit в ASP.NET Core MVC
  2. Тестирование контроллеров MVC в ASP.NET Core
  3. Интеграционное тестирование в ASP.NET Core MVC
  4. AntiForgeryToken для интеграционного тестирования в ASP.NET Core
  5. Тестирование UI с помощью Selenium в ASP.NET Core MVC

Мы начнем с краткого обзора инструмента xUnit и нашего стартового проекта.

Позже мы добавим новый класс с логикой проверки и, наконец, узнаем, как протестировать эту новую функциональность с помощью проекта xUnit.

Обзор инструмента xUnit

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

  • [Fact] - атрибут указывает, что метод должен выполняться исполнителем теста
  • [Theory] - атрибут подразумевает, что мы собираемся отправить некоторые параметры в наш тестовый код. Таким образом, он похож на атрибут [Fact], потому что он утверждает, что метод должен выполняться исполнителем теста, но дополнительно подразумевает, что мы собираемся отправить параметры методу тестирования
  • [InlineData] - атрибут предоставляет те параметры, которые мы отправляем методу тестирования. Если мы используем атрибут [Theory], мы также должны использовать [InlineData]

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

Как только мы напишем наш тестовый метод, нам нужно запустить его, чтобы убедиться, работает он или нет. Для этой цели мы собираемся использовать обозреватель тестов Visual Studio, который можно открыть, открыв меню «Test», а затем «Windows»> «Test Explorer». Мы также можем использовать сочетание клавиш: CTRL + E.

Краткий обзор стартового проекта

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

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

Наш контроллер содержит три действия: одно для запроса GET и два для запроса POST. Также у нас есть представления для действий Index и Create.

Теперь мы можем перейти к следующему этапу, добавив дополнительный класс с логикой проверки.

Добавление функциональности проверки в наш проект

Прежде чем мы начнем, давайте взглянем на наш класс сущности Employee:

[Table("Employee")]
public class Employee
{
    public Guid Id { get; set; }
[Required(ErrorMessage = "Name is required")]
public string Name { get; set; }

[Required(ErrorMessage = "Age is required")]
public int Age { get; set; }

[Required(ErrorMessage = "Account number is required")]
public string AccountNumber { get; set; }

}

И действие HttpPost в классе контроллера:

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create([Bind("Name,AccountNumber,Age")] Employee employee)
{
    if(!ModelState.IsValid)
    {
        return View(employee);
    }

    _repo.CreateEmployee(employee);
    return RedirectToAction(nameof(Index));
}

В действии Create мы добавляем новый объект сотрудника в базу данных, если модель действительна. Но что если мы захотим добавить дополнительную проверку для свойства AccountNumber. Для этого нам нужно создать новый класс проверки и после этого написать тесты для каждого правила проверки внутри этого класса.

Итак, начнем с добавления новой папки с именем Validation и внутри нее нового класса AccountNumberValidation:

public class AccountNumberValidation
{
    private const int startingPartLength = 3;
    private const int middlePartLength = 10;
    private const int lastPartLength = 2;
    public bool IsValid(string accountNumber)
    {
        var firstDelimiter = accountNumber.IndexOf('-');
        var secondDelimiter = accountNumber.LastIndexOf('-');

        if(firstDelimiter == -1 || secondDelimiter == -1) 
            throw new ArgumentException();

        var firstPart = accountNumber.Substring(0, firstDelimiter);
        if (firstPart.Length != startingPartLength)
            return false;

        var tempPart = accountNumber.Remove(0, startingPartLength + 1);
        var middlePart = tempPart.Substring(0, tempPart.IndexOf('-'));
        if (middlePart.Length != middlePartLength)
            return false;

        var lastPart = accountNumber.Substring(secondDelimiter + 1);
        if (lastPart.Length != lastPartLength)
            return false;

        return true;

    }
}

Итак, мы хотим убедиться, что AccountNumber состоит из трех частей разной длины (3, 10 и 2). Кроме того, мы хотим, чтобы в качестве разделителя использвался знак минус.

При этом обратите внимание, что если разделители недействительны, мы генерируем исключение. Если какая-либо из частей AccountNumber недействительна, мы возвращаем false. Наконец, если все идет хорошо, мы возвращаем true.

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

Готовим проект для тестирования

Начнем с создания нового тестового проекта xUnit и присвоения ему имени EmployeesApp.Tests:

В новом проекте будет подготовлен один тестовый класс для использования с именем UnitTest1.cs, а также будет установлена библиотека xUnit и средство запуска xUnit:

Мы можем удалить класс UnitTest1, добавить новую папку Validation и создать в ней новый класс AccountNumberValidationTests:

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

После того, как мы закончили с подготовкой, можем написать несколько тестов.

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

mkdir EmployeesApp.Tests - для создания новой папки

cd EmployeesApp.Tests - для перехода к новой папке

dotnet new xUnit - для создания проекта xUnit с тем же именем, что и у родительской папки

Модульное тестирование с помощью xUnit

Итак, давайте изменим класс AccountNumberValidationTests:

public class AccountNumberValidationTests
{
    private readonly AccountNumberValidation _validation;
    public AccountNumberValidationTests()
    {
        _validation = new AccountNumberValidation();
    }

    [Fact]
    public void IsValid_ValidAccountNumber_ReturnsTrue()
    {
        Assert.True(_validation.IsValid("123-4543234576-23"));
    }
}

Мы собираемся использовать объект _validation со всеми тестовыми методами в этом классе. Поэтому лучший способ - создать его в конструкторе, а затем просто использовать, когда он нам нужен. Таким образом мы предотвращаем повторение создания экземпляра объекта _validation.

Под конструктором мы видим наш первый тестовый метод с атрибутом [Fact]. Обратите внимание на соглашение об именах, которое мы используем для методов тестирования:

[MethodWeTest_StateUnderTest_ExpectedBehavior]

Название метода подразумевает, что мы тестируем действительный номер учетной записи и что метод тестирования должен возвращать true. Этого можно добиться, используя класс Assert и метод True, который проверяет, что выражение внутри него возвращает true. Для выражения мы вызываем метод IsValid из класса AccountNumberValidation и передаем действительный номер счета.

Теперь мы можем запустить обозреватель тестов и проверить, проходит ли наш тест:

Мы видим, что этот тест проходит в самом классе:

Работает

Атрибуты [Theory] и [InlineData]

В классе AccountNumberValidation метод IsValid содержит проверки для первой, средней и последней части номера счета. Поэтому мы собираемся написать тесты для всех этих ситуаций. Начнем с теста, в котором неверна первая часть:

[Fact]
public void IsValid_AccountNumberFirstPartWrong_ReturnsFalse()
{
    Assert.False(_validation.IsValid("1234-3454565676-23"));
}

Мы ожидаем, что наш тест вернет false, если у нас неправильный номер счета. Поэтому мы используем метод False()с предоставленным выражением. Конечно, чтобы проверить это, мы должны использовать Test Explorer:

Мы видим, что тест прошел. Но теперь, если мы хотим протестировать номер учетной записи с 2 цифрами для первой части (мы тестировали только с 4 цифрами), нам придется снова написать тот же метод, только с другим номером учетной записи. Очевидно, это не лучший сценарий. Чтобы улучшить это, мы собираемся изменить этот метод тестирования, удалив атрибут [Fact]и добавив [Theory] и [InlineData] атрибуты:

[Theory]
[InlineData("1234-3454565676-23")]
[InlineData("12-3454565676-23")]
public void IsValid_AccountNumberFirstPartWrong_ReturnsFalse(string accountNumber)
{
    Assert.False(_validation.IsValid(accountNumber));
}

Теперь проверим результат:

Несмотря на то, что у нас всего два метода тестирования, средство запуска тестов запускает три теста. Один тест для первого метода тестирования и два теста для каждого атрибута [InlineData].

Дополнительные тесты

Теперь, когда мы знаем, как использовать атрибуты [Theory] и [InlineData], давайте напишем дополнительные тесты для номера нашей учетной записи:

[Theory]
[InlineData("123-345456567-23")]
[InlineData("123-345456567633-23")]
public void IsValid_AccountNumberMiddlePartWrong_ReturnsFalse(string accNumber)
{
    Assert.False(_validation.IsValid(accNumber));
}
[Theory]
[InlineData("123-3434545656-2")]
[InlineData("123-3454565676-233")]
public void IsValid_AccountNumberLastPartWrong_ReturnsFalse(string accNumber)
{
    Assert.False(_validation.IsValid(accNumber));
}

В приведенном выше коде нет ничего нового (кроме других параметров), поэтому мы можем сразу запустить средство запуска тестов:

Выбрасывание исключений при тестировании

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

[Theory]
[InlineData("123-345456567633=23")]
[InlineData("123+345456567633-23")]
[InlineData("123+345456567633=23")]
public void IsValid_InvalidDelimiters_ThrowsArgumentException(string accNumber)
{
    Assert.Throws<ArgumentException>(() => _validation.IsValid(accNumber));
}

Мы тестируем здесь три разные ситуации, когда второй разделитель неверен, когда неверен первый разделитель и когда оба они неверны. Чтобы проверить исключение, мы должны использовать метод Throwsс типом исключения в качестве значения T. Обратите внимание, что мы используем лямбда-выражение внутри метода Throws, которое немного отличается от того, что мы использовали раньше.

Сделав это, проверим результат:

Как видите, тест не прошел

Если быть точнее, два из сценариев не прошли, а один прошел. Это означает, что наша проверка в методе IsValid неверна. И теперь понятно, почему тесты так важны. Несмотря на то, что на первый взгляд код выглядел хорошо, на самом деле оказывается, что он не так хорош. Итак, давайте исправим:

var firstDelimiter = accountNumber.IndexOf('-');
var secondDelimiter = accountNumber.LastIndexOf('-');
if (firstDelimiter == -1 || (firstDelimiter == secondDelimiter))
throw new ArgumentException();

А теперь снова запустим тест:

Отлично! Все работает.