Интеграционное тестирование в ASP.NET Core MVC

  • Михаил
  • 12 мин. на прочтение
  • 170
  • 24 Nov 2022
  • 24 Nov 2022

В предыдущих статьях мы узнали, как использовать 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) и т.д..

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