В предыдущих статьях мы узнали, как использовать xUnit для написания модульных тестов для нашего класса Validation и как тестировать наш класс Controller с его действиями с помощью библиотеки Moq для изоляции зависимостей.
В этой статье мы узнаем об интеграционном тестировании в ASP.NET Core MVC. Кроме того, мы собираемся подготовить базу данных в памяти, чтобы не использовать настоящий SQL-сервер во время интеграционных тестов.
Подготовка нового проекта для интеграционного тестирования
Сначала необходимо новый проект xUnit
с именем EmployeesApp.IntegrationTests
для целей интеграционного тестирования.
После создания проекта необходимо переименовать класс UnitTest1.cs
в EmployeesControllerIntegrationTests
:
Кроме того, необходимо добавить ссылку на основной проект и установить один пакет NuGet
, необходимый для целей тестирования:
- AspNetCore.Mvc.Testing - этот пакет предоставляет TestServer и важный класс
WebApplicationFactory
, чтобы помочь нам загрузить наше приложение в памяти. - Microsoft.EntityFrameworkCore.InMemory - поставщик базы данных в памяти.
Теперь мы можем продолжить.
Создание конфигурации фабрики In-Memory
Давайте создадим новый класс TestingWebAppFactory
и изменим его соответствующим образом:
public class TestingWebAppFactory : WebApplicationFactory
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<EmployeeContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
services.AddDbContext<EmployeeContext>(options =>
{
options.UseInMemoryDatabase("InMemoryEmployeeTest");
options.UseInternalServiceProvider(serviceProvider);
});
var sp = services.BuildServiceProvider();
using (var scope = sp.CreateScope())
{
using (var appContext = scope.ServiceProvider.GetRequiredService<EmployeeContext>())
{
try
{
appContext.Database.EnsureCreated();
}
catch (Exception ex)
{
//Log errors or do anything you think it's needed
throw;
}
}
}
});
}
}
Здесь стоит упомянуть пару вещей.
Наш класс реализует класс WebApplicationFactory<Startup>
и переопределяет метод ConfigureWebHost
. В этом методе мы удаляем регистрацию EmployeeContext
из класса Startup.cs
. Затем мы добавляем поддержку базы данных в памяти Entity Framework в контейнер DI через класс ServiceCollection
.
После этого мы добавляем контекст базы данных в контейнер службы и настраиваем его на использование базы данных в памяти вместо реальной базы данных.
Далее проверяем, что мы заполняем данные из класса EmployeeContext
(те же данные, которые вы вставили в реальную базу данных SQL Server в начале этой серии).
Сделав все необходимые приготовления, мы можем вернуться к классу тестирования и приступить к написанию тестов.
Интеграционное тестирование действия Index
В нашем тестовом классе мы можем найти единственный тестовый метод с именем по умолчанию. Но давайте удалим его и начнем с нуля.
Первое, что нам нужно сделать, это реализовать ранее созданный класс TestingWebAppFactory
:
public class EmployeesControllerIntegrationTests : IClassFixture>
{
private readonly HttpClient _client;
public EmployeesControllerIntegrationTests(TestingWebAppFactory<Startup> factory)
{
_client = factory.CreateClient();
}
}
Итак, мы реализуем класс TestingWebAppFactory
с интерфейсом IClassFixture
и внедряем его в конструктор, где мы создаем экземпляр HttpClient
. Интерфейс IClassFixture
- это декоратор, который указывает, что тесты в этом классе полагаются на выполнение фикстуры.
Теперь давайте напишем наш первый интеграционный тест:
[Fact]
public async Task Index_WhenCalled_ReturnsApplicationForm()
{
var response = await _client.GetAsync("/Employees");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("Mark", responseString);
Assert.Contains("Evelin", responseString);
}
Мы используем метод GetAsync
для вызова действия на маршруте /Employees
, которое является действием Index
, и возвращаем результат в виде переменной response
. С помощью метода EnsureSuccessStatusCode
мы проверяем, что свойство IsSuccessStatusCode
имеет значение true:
Если значение равно false, это будет означать, что запрос не был успешным, поэтому тест не пройден.
Наконец, мы сериализуем наш HTTP-контент в строку с помощью метода ReadAsStringAsync
и проверяем, что он содержит двух наших сотрудников:
Мы видим, что тест прошел, и мы успешно вернули наших сотрудников из базы данных в памяти. Если вы хотите убедиться, что мы действительно используем базу данных в памяти, а не настоящую, вы всегда можете остановить службу SQLServer в окне «Службы» и снова запустить тест.
Отлично!
Теперь мы можем продолжить интеграционное тестирование обоих действий Create
.
Тестирование действия Create (GET)
Прежде чем продолжить тестирование, давайте откроем файл Create.cshtml
из папки Views\Employees
и изменим его, изменив тэг h4
:
<h4>Please provide a new employee data</h4>
Теперь мы готовы написать наш тестовый код.
Мы хотим проверить, что когда выполняется действие Create (GET), оно возвращает форму Create:
[Fact]
public async Task Create_WhenCalled_ReturnsCreateForm()
{
var response = await _client.GetAsync("/Employees/Create");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("Please provide a new employee data", responseString);
}
И это действительно так.
Тестирование действия Create (POST)
Итак, давайте напишем код интеграционного тестирования для действия POST. Для первого метода тестирования мы собираемся проверить, что наше действие возвращает представление с соответствующим сообщением об ошибке, когда модель, отправленная со страницы Create
, недействительна.
[Fact]
public async Task Create_SentWrongModel_ReturnsViewWithErrorMessages()
{
var postRequest = new HttpRequestMessage(HttpMethod.Post, "/Employees/Create");
var formModel = new Dictionary
{
{ "Name", "New Employee" },
{ "Age", "25" }
};
postRequest.Content = new FormUrlEncodedContent(formModel);
var response = await _client.SendAsync(postRequest);
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("Account number is required", responseString);
}
Мы создаем почтовый запрос и объект formModel
в качестве словаря, который состоит из элементов, имеющихся на странице Create. Конечно, мы не предоставили все элементы AccountNumber
, потому что мы хотим отправить недопустимые данные.
После этого мы сохраняем formModel
в качестве содержимого в нашем запросе, отправляем этот запрос с помощью метода SendAsync
и проверяем успешность ответа.
Даллее, мы сериализуем наш ответ и выполняем проверку утверждения.
Если мы посмотрим на класс модели Employee
, то увидим, что если AccountNumber
не указан, сообщение об ошибке должно появиться в форме:
[Required(ErrorMessage = "Account number is required")]
public string AccountNumber { get; set; }
Теперь мы можем запустить обозреватель тестов:
Что ж, этот тест не прошел. Но с кодом все в порядке, просто по какой-то причине мы получаем сообщение 400 Bad Request.
Почему?
Итак, если мы откроем наш контроллер и посмотрим на действие Create (POST), мы увидим атрибут ValidateAntiForgeryToken. Итак, наше действие ожидает, что токен защиты от подделльного запроа будет предоставлен, но мы этого не делаем, поэтому тест не проходит. А пока (как временное решение) мы закомментируем этот атрибут и снова запустим тест:
Результат:
Теперь тест пройден. Как мы уже говорили, это временное решение. Чтобы настроить токен Anti-Forgery в нашем тестовом коде, нужно выполнить несколько шагов, и в следующей статье мы покажем вам, как это сделать шаг за шагом. Пока оставим ValidateAntiForgeryToken
закомментированым.
Тестирование успешного запроса POST
Давайте напишем последний тест в этой статье, в котором мы проверим, что действие Create
возвращает представление Index
, если запрос POST выполнен успешно:
[Fact]
public async Task Create_WhenPOSTExecuted_ReturnsToIndexViewWithCreatedEmployee()
{
var postRequest = new HttpRequestMessage(HttpMethod.Post, "/Employees/Create");
var formModel = new Dictionary<string, string>
{
{ "Name", "New Employee" },
{ "Age", "25" },
{ "AccountNumber", "214-5874986532-21" }
};
postRequest.Content = new FormUrlEncodedContent(formModel);
var response = await _client.SendAsync(postRequest);
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("New Employee", responseString);
Assert.Contains("214-5874986532-21", responseString);
}
Этот код не слишком сильно отличается от предыдущего, за исключением того, что мы отправляем действительный объект formModel
с запросом и частью утверждения. После успешного выполнения запроса POST метод Create
должен перенаправить нас к методу Index
. Там мы можем найти всех сотрудников, включая созданного. Вы всегда можете отладить свой тестовый код и проверить responseString
, чтобы визуально подтвердить, что ответ - это страница Index
с новым сотрудником.
Далее запустим обозреватель тестов:
Отлично! Проходит.
Заключение
В этой статье мы узнали, как писать интеграционные тесты в приложении ASP.NET Core MVC. Мы создали базу данных в памяти, чтобы использовать ее во время тестов вместо реального сервера базы данных. Кроме того, мы узнали, как тестировать наше действие Index, а также как писать интеграционные тесты для действий Create. Эту методологию тестирования можно применить и к другим действиям (PUT, DELETE) и т.д..
А также, мы обнаружили проблему с токеном защиты от подделки и в следующей статье, мы рассмотрим, как решить эту проблему, введя несколько новых функций в наш код.