Тестирование приложения 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, то я не буду заморочиваться с представлением.
Вжух-вжух и я реализовал предсталения
Запущу приложения, для того, чтобы проверить его работоспособность.
Всё работает отлично.
Теперь можно создавать тесты. И так, что я буду тестировать? Я буду тестировать модель и контроллер.
Для начало претестирую модель. Для этого нужно создать проект с тестами. Я выбрал xUnit
Именуют тесты обычно так: НаименованияКласс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’и, которые были описаны в данном посте.
А закончить я хочу словами С. Макконнелла “…лучшем способом повышения производительности труда программистов и качества ПО является минимазация времени, затрачиваемого на исправления кода…”.