Тестирование приложения ASP.NET Core

Для того, чтобы можно было протестировать ASP.NET Core приложения (и не только), нужно разбить приложения на логические части. Для этого используются архитектурные паттерны: MVC, MVVM, MVP и т.д. Для приложения, написаного на ASP.NET Core, используется паттерн MVC. Данный паттерн предполагает разделения структуры приложения на Model (Бизнес логика)-Controller (Посредник между БЛ и представлением)-View (Представление). Но чтобы приложение было легко маштабируемое, легко поддерживаемое и легко тестируемое нужно, чтобы уровни паттерна зависили от абстракции.

Но вернемся к теме моего поста. Для того, чтобы приложения написанное на Core можно было легко протестировать будем успользовать следующее практики и Framework’и:

  • Dependency Inversion Principle;
  • Dependency Injection;
  • IoC Container;
  • Moq framework .

И так, Dependency Inversion - это один из приципов SOLID. Он служит для создания слабосвязанных сущностей. Которых, можно легко модифицировать при минимальных изменениях в коде. Не следует путать Dependency Inversion с Dependency Injection. Это разные вещи.

Далее. Dependency Injection - это внедрения зависимостей. Если Dependency Inversion - это прицип, то Dependency Injection - это одна из реализаций этого приципа.

Существует два вида внедрения зависимостей:

  • Внедрения через конструктор;
  • Внедрения через свойства.

Внедрения через свойства можно реализовать через атрибуты [Dependency] в Unity, но как по мне лучше реализовывать через конструктор. Тогда объект становится более безопасным, в случае отказа от IoC Container’a. Так, что в рамках этого поста я буду использовать внедрения через конструктор.

IoC Container - это сущность на которой “завязываются” все узелки, именно этот класс и берет на себя “грех” создания объектов. Так же, как и Dependency Injection, IoC Container - это реализация приципов Dependency Inversion. IoC Container похож чем том на Абстрактную фабрику, только он берет на себя ещё обязанности нахождения зависимостей для каждого создаваемого объекта.

Для тестирования я буду использовать встроенный в ASP.NET Core IoC Container.

Moq framework - это framework, который позволяет имитировать объект и/или его функциональность. Такой объект называется moq-объект.

Что можно тестировать в ASP.NET Core MVC?

Да всё очень просто. Для начало можно тестировать View, или лучше сказать Frontend, который зачастую представлен в виде приложения. Можно также тестировать Model, бизнес логику, и Controller. Тестирования Frontend’a - это отдельная тема для поста. Сейчас же я буду тестировать Model и Controller.

Для начала надо создать слабо связанную архитектуру приложения.

Первым делом я создам интерфейс для репозитория. Репизиторием его можно назвать условно. Конечно, настоящий репозиторий имеет архитектуру гораздно сложнее:

 public interface IHomeRepository
{
        List<string> GetProducts();
}

Мой репозиторий будет возвращать список продуктов.

Теперь нужна релизация для моего репизитория. Релизация будет возвращать список продуктов, для простоты я буду создавать продукты прямо в методе:

public class HomeRepository : IHomeRepository
{
    public List<string> GetProducts()
    {
        List<string> products = new List<string>();
        products.Add("iPhone Xs");
        products.Add("Pixel 3");
        products.Add("Surface Pro 6");

        return products;
    }
}

Теперь я создам модель, которая будет использовать данный репозиторий. Но я не буду создавать просто реализацию, т.к. я хочу показать, что можно (и нужно) использовать слабосвязанные сущности, то я создам интерфейс, который будет представлять мою модель, а потом реализацию.

Для начала код интерфейса:

 public interface IHome 
 {
    List<string> GetProducts();
 }

Сама реализация:

public class HomeModel : IHome
{
    private readonly IHomeRepository _repository;

    public HomeModel(IHomeRepository repository)
    {
        _repository = repository ?? throw new OperationCanceledException("ctor: IHomeRepository can't be a null");
    }

    public List<string> GetProducts()
    {
         return _repository.GetProducts();
    }
}

Как видно из кода выше, в модели я исполльзовал принцип DI через конструктор. В итоге я получил слабосвязанную сущность, которая зависит от абстракции IHomeRepository, а не от реализации.

Ну что, осталось внедрить зависимость в контроллер:

public HomeController(IHome home)
{
       _home = home ?? throw new OperationCanceledException("ctor: IHome cant's be a null");
}

Осталось зарегистрировать объекты в контейнере. Я как писал выше, я буду использовать встроенный контейнер в ASP.NET Core. Вот сообственно и сама регистрация:

services.AddTransient<IHome, HomeModel>();
services.AddTransient<IHomeRepository, HomeRepository>();

Теперь надо настроить отображения и проверить работоспособность приложения.

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

Вжух-вжух и я реализовал предсталения

Запущу приложения, для того, чтобы проверить его работоспособность.

test-of-asp-net-core

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

Теперь можно создавать тесты. И так, что я буду тестировать? Я буду тестировать модель и контроллер.

Для начало претестирую модель. Для этого нужно создать проект с тестами. Я выбрал xUnit

test-of-asp-net-core

Именуют тесты обычно так: НаименованияКлассTest,а методы которые нужно протестировать именуют так: TestНаименованияМетода.

Например: есть HomeModel с методом GetProducts() соответсвенно тесты для неё TestHomeModel с методом TestGetProducts();

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

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

Как раз здесь мне и потребуется Moq framework. Как было написано выше с помощью moq’a можно имитировать объект.

Ок, погнали.

Для начало нужно подключить Moq framework к проекту с тестами. Это делается очень просто с помощью nuget.

Далее я создал в конструторе List, который будет я буду позиционировать как правильные данные:

private List<string> _products;

public HomeTest()
{
   _products = new List<string>();
   _products.Add("iPhone Xs");
   _products.Add("Pixel 3");
   _products.Add("Surface Pro 6");
}

При написаниях настоящих тестов такая реализация не допускается, но как говорится: “для примера сойдет”.

И так, а теперь самое интересно реализация метода для тестирования:

[Fact]
public void TestGetProducts()
{
    Mock<IHomeRepository> mockRepository = new Mock<IHomeRepository>();
    mockRepository.Setup(m => m.GetProducts()).Returns(new List<string>
    {
       "iPhone Xs",
       "Pixel 3",
       "Surface Pro 6"
    });

    HomeModel home = new HomeModel(mockRepository.Object);
    Assert.True(home.GetProducts().GetEnumerator().MoveNext());

    string surface = home.GetProducts()[2];
    Assert.Equal(_products[2], surface);
}

Самое интересное здесь - это то, как я создал репозиторий. Как видно из кода выше, я не создаю реализацию репозитирия. Я создал moq-объект, который в свою очередь имитируюет реализацию. Даже если, не знать как создавать moq-объект из кода выше можно понять это: во-первых, я указал интерфейс, для которого мне нужна реализация, во-вторых, я с имитировал возвращаемый результат.

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

Теперь я покажу реализацию метода для тестирования контролера:

[Fact]
public void TestIndex()
{
    Mock<IHome> mockModel = new Mock<IHome>();
    mockModel.Setup(m => m.GetProducts()).Returns(new List<string>
    {
       "iPhone Xs",
       "Pixel 3",
       "Surface Pro 6"
    });

    HomeController controller = new HomeController(mockModel.Object);
    var indexResult = controller.Index() as ViewResult;
    string pixel = ((List<string>)indexResult.Model)[1];
    Assert.Equal(_products[1], pixel);
}

Здесь происходит примерно тоже самое, что в предыдущем тесте. Был создан moq-объект для IHome интерфейса. Далее я вытащил модель из контроллера и проверил возвращаемые данные.

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

А закончить я хочу словами С. Макконнелла “…лучшем способом повышения производительности труда программистов и качества ПО является минимазация времени, затрачиваемого на исправления кода…”.

Написано 27 января 2019