Тестирование контроллеров MVC в ASP.NET Core

Внимание

Данный материал является частью цикла статей «Тестирование в 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 и различные атрибуты, которые он предоставляет. Мы разобрались, как тестировать правила проверки внутри одного класса проверки.

А как насчет контроллеров и всех действий внутри? Можем ли мы написать для них тесты?

Конечно.

В этой статье мы объясним, как это сделать.

Использование библиотеки Moq для создания фиктивных объектов при тестировании контроллеров MVC

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

Как видите, мы используем Injection Dependency, чтобы внедрить интерфейс в наш контроллер. Таким образом, наш контроллер зависит от логики репозитория через внедренный интерфейс.

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

Изолирование зависимостей в тестовом коде дает несколько преимуществ:

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

Конечно, есть дополнительные причины изолировать зависимости в тестовом коде, но суть вы поняли.

Давайте установим библиотеку Moq в проекте EmployeesApp.Tests:

После завершения установки необходимо создать новую папку Controller в том же проекте и добавить класс EmployeesControllerTests:

Создание фиктивного объекта

Давайте изменим класс EmployeesControllerTests:

public class EmployeesControllerTests
{
    private readonly Mock<IEmployeeRepository> _mockRepo;
    private readonly EmployeesController _controller;
    public EmployeesControllerTests()
    {
        _mockRepo = new Mock<IEmployeeRepository>();
        _controller = new EmployeesController(_mockRepo.Object);
    }
}

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

Вот и все. Все подготовлено, зависимость смоделирована, поэтому все, что нам нужно сделать, это написать несколько тестов.

Тестирование действия Index

Если мы посмотрим на действие Index в классе EmployeesController, мы увидим, что получаем всех сотрудников из базы данных и возвращаем представление с этими сотрудниками:

public IActionResult Index()
{
    var employees = _repo.GetAll();
    return View(employees);
}

В результате мы можем написать пару тестов, чтобы убедиться, что это действие делает именно то, что должно делать.

[Fact]
public void Index_ActionExecutes_ReturnsViewForIndex()
{
    var result = _controller.Index();
    Assert.IsType<ViewResult>(result);
}

Итак, мы выполняем действие Index из нашего контроллера и принимаем результат в соотв. переменную. После этого мы проверяем тип возвращаемого результата с помощью метода IsType. Если результат относится к типу ViewResult, тест будет пройден, в противном случае он завершится ошибкой.

Давайте запустим окно обозревателя тестов и посмотрим результат:

Мы видим, что наш тест прошел успешно и что результат имеет тип ViewResult.

Напишем еще один тестовый метод, чтобы убедиться, что наше действие Index возвращает точное количество сотрудников:

[Fact]
public void Index_ActionExecutes_ReturnsExactNumberOfEmployees()
{
    _mockRepo.Setup(repo => repo.GetAll())
        .Returns(new List<Employee>() { new Employee(), new Employee() });
    var result = _controller.Index();

    var viewResult = Assert.IsType<ViewResult></ViewResult>(result);
    var employees = Assert.IsType<List<Employee>>(viewResult.Model);
    Assert.Equal(2, employees.Count);
}

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

В этом методе тестирования мы извлекаем данные из базы данных с помощью метода репозитория GetAll. Конечно, мы не хотим использовать конкретный репозиторий, а использовать имитацию, и поэтому мы используем метод Setup, чтобы указать настройку для метода GetAll. Кроме того, мы должны использовать метод Returns, чтобы указать значение, возвращаемое из имитационного метода GetAll.

После того, как мы сохраняем результат действия Index, мы проверяем тип этого результата, тип объекта модели внутри этого результата и, наконец, количество сотрудников, используя метод Equal. Чтобы пройти тест, необходимо пройти все три проверки, в противном случае тест не будет считаться пройденным.

Давайте посмотрим на этот тест в действии:

Тест действительно прошел, и мы убедились, что он возвращает ровно двух сотрудников.

Тестирование действия Create

У нас есть два действия Create в нашем классе EmployeesController: действие GET и действие POST. Первое действие просто загружает представление Create View>, и это то, что мы должны проверить.

[Fact]
public void Create_ActionExecutes_ReturnsViewForCreate()
{
    var result = _controller.Create();

    Assert.IsType<ViewResult>(result);
}

У нас уже был подобный тест, только с действием Index, поэтому в нем нет ничего нового. Если мы запустим Test Explorer, мы можем убедиться, что тест проходит:

Перейдем ко второму действию "Create" - POST. В этом действии у нас есть проверка модели, и если она недействительна, мы возвращаем представление с объектом сотрудника.

[Fact]
public void Create_InvalidModelState_ReturnsView()
{
    _controller.ModelState.AddModelError("Name", "Name is required");
    var employee = new Employee { Age = 25, AccountNumber = "255-8547963214-41" };

    var result = _controller.Create(employee);

    var viewResult = Assert.IsType<ViewResult>(result);
    var testEmployee = Assert.IsType<Employee>(viewResult.Model);
    Assert.Equal(employee.AccountNumber, testEmployee.AccountNumber);
    Assert.Equal(employee.Age, testEmployee.Age);
}

Мы должны добавить ошибку модели в свойство ModelState, чтобы проверить недопустимое состояние модели.

После этого мы создаем нового сотрудника без свойства Name, что также делает его недействительным.

Наконец, мы вызываем действие Create и выполняем несколько утверждений.

С помощью операторов Assert мы проверяем, имеет ли результат тип ViewResult и что модель имеет тип Employee. Кроме того, мы гарантируем, что вернем того же сотрудника, сравнивая значения свойств из объектов testEmployee и сотрудников:

Дополнительный тест недопустимой модели

Давайте напишем еще один тест, чтобы убедиться, что метод CreateEmployee из нашего репозитория никогда не выполняется, если состояние модели недопустимо:

[Fact]
public void Create_InvalidModelState_CreateEmployeeNeverExecutes()
{
    _controller.ModelState.AddModelError("Name", "Name is required");
    var employee = new Employee { Age = 34 };

    _controller.Create(employee);

    _mockRepo.Verify(x => x.CreateEmployee(It.IsAny<Employee>()), Times.Never);

}

Первые три строки кода такие же, как и в предыдущем методе тестирования, мы добавляем ошибку модели, создаем недопустимый объект сотрудника и вызываем действие Create из нашего контроллера.

В этом действии есть метод CreateEmployee, который не должен выполняться, если модель недействительна. Это именно то, что мы проверяем с помощью метода Verify из фиктивного объекта. Используя выражение It.IsAny, мы заявляем, что не имеет значения, какой сотрудник передается в качестве параметра методу CreateEmployee. Важно только то, что параметр имеет тип Employee.

Последний параметр метода Verify - это количество выполнений нашего метода. В этой ситуации мы используем Times.Never, потому что мы не хотим, чтобы метод CreateEmployee вообще выполнялся, если модель недействительна.

Вернемся в обозреватель тестов:

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

Если мы изменим тестовый код, поместив Times.Once вместо Times.Never, тест обязательно завершится ошибкой. Возможно, вы захотите попробовать сами:

Сообщение поясняет, в чем проблема и почему тест не проходит.

Проверка допустимости модели в действии создания

Если состояние модели допустимо, метод CreateEmployee следует выполнить только один раз:

public void Create_ModelStateValid_CreateEmployeeCalledOnce()
{
    Employee emp = null;
    _mockRepo.Setup(r => r.CreateEmployee(It.IsAny()))
        .Callback(x => emp = x);
    var employee = new Employee
    {
        Name = "Test Employee",
        Age = 32,
        AccountNumber = "123-5435789603-21"
    };

    _controller.Create(employee);

    _mockRepo.Verify(x => x.CreateEmployee(It.IsAny<Employee>()), Times.Once);

    Assert.Equal(emp.Name, employee.Name);
    Assert.Equal(emp.Age, employee.Age);
    Assert.Equal(emp.AccountNumber, employee.AccountNumber);
}

Итак, мы настраиваем метод CreateEmployee с любым сотрудником и используемдля заполнения объекта emp значениями из параметра employee. После этого мы создаем локальный объект employee, выполняем действие Create с этим employee и проверяем, если CreateEmployee был выполнен только один раз.

Кроме того, мы проверяем, что объект emp совпадает с объектом employee, предоставленным для действия Create.

Добавим еще один тест:

[Fact]
public void Create_ActionExecuted_RedirectsToIndexAction()
{
    var employee = new Employee
    {
        Name = "Test Employee",
        Age = 45,
        AccountNumber = "123-4356874310-43"
    };
    var result = _controller.Create(employee);

    var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
    Assert.Equal("Index", redirectToActionResult.ActionName);
}

Если мы посмотрим на действие Create в нашем контроллере, то мы увидим, что после создания нового сотрудника мы перенаправляем пользователя на действие Index.

Заключение

Вот и все. Мы использовали действия Index и Create, чтобы показать вам, как протестировать наш контроллер и действия внутри него, но все правила могут применяться к другим типам действий (PUT, DELETE) и др..

Итак, подводя итог, мы узнали:

  • Как использовать библиотеку Moq для изоляции зависимостей в тестовом коде
  • Как написать различные тесты для наших действий внутри контроллера