Интеграционные тесты на платформе ASP.NET Core

Джос ван дер Тиль, Мартин Костелло и Хавьер Голварро Нельсон.

С помощью интеграционных тестов можно проверить работу компонентов приложения на уровне, который включает инфраструктуру, поддерживающую приложение, такую как база данных, файловая система и сеть. ASP.NET Core поддерживает интеграционные тесты с помощью платформы модульного тестирования с тестовым веб-узлом и сервером тестирования в памяти.

В этой статье предполагается базовое понимание модульных тестов. Если вы не знакомы с понятиями тестирования, см. статью Модульное тестирование в .NET Core и .NET Standard и связанное с ней содержимое.

Просмотреть или скачать образец кода (как скачивать)

В качестве примера используется приложение Razor Pages (предполагается базовое понимание Razor Pages). Если вы не знакомы с Pages, ознакомьтесь со Razor следующими статьями:

Для тестирования spas мы рекомендуем использовать такое средство, как Playwright для .NET, которое может автоматизировать браузер.

Общие сведения об интеграционных тестах

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

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

  • База данных
  • Файловая система
  • Сетевые устройства
  • Конвейер "запрос-ответ"

В модульных тестах вместо компонентов инфраструктуры используются структурные компоненты, известные как имитации или макеты объектов.

В отличие от модульных тестов, интеграционные тесты:

  • Используют фактические компоненты, которые приложение использует в рабочей среде.
  • Требуют больше кода и обработки данных.
  • Выполняются дольше.

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

При обсуждении интеграционных тестов тестируемый проект часто называется тестируемой системой или сокращенно SUT. "SUT" используется в этой статье для обозначения тестируемого приложения ASP.NET Core.

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

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

Для интеграционных тестов в ASP.NET Core требуется следующее:

  • Тестовый проект, который используется для хранения и выполнения тестов. Тестовый проект содержит ссылку на ТС.
  • Тестовый проект создает тестовый веб-узел для ТС и использует клиент тестового сервера для обработки запросов и ответов с помощью ТС.
  • Средство запуска тестов используется для выполнения тестов и передачи результатов тестов.

Интеграционные тесты придерживаются последовательности событий из обычных шагов теста Подготовка, Выполнение и Проверка.

  1. Настраивается веб-узел ТС.
  2. Создается клиент тестового сервера для отправки запросов к приложению.
  3. Выполняется шаг теста Подготовка: тестовое приложение готовит запрос.
  4. Выполняется шаг теста Выполнение: клиент отправляет запрос и получает ответ.
  5. Выполняется шаг теста Проверка: фактический ответ определяется как правильный или ошибочный на основе ожидаемого ответа.
  6. Процесс продолжается до тех пор, пока не будут выполнены все тесты.
  7. Выводятся результаты теста.

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

Компоненты инфраструктуры, такие как тестовый веб-узел и сервер тестирования в памяти (TestServer), предоставляются или управляются пакетом Microsoft.AspNetCore.Mvc.Testing. Использование этого пакета упрощает создание и выполнение тестов.

Пакет Microsoft.AspNetCore.Mvc.Testing выполняет следующие задачи:

  • Копирует файл зависимостей (.deps) из ТС в папку bin тестового проекта.
  • Задает корневой каталог содержимого в корне проекта ТС, чтобы при выполнении тестов были найдены статические файлы и страницы или представления.
  • Предоставляет класс WebApplicationFactory для упрощения начальной загрузки ТС с TestServer.

В документации модульные тесты описано, как настроить тестовый проект и средство запуска тестов, а также представлены подробные инструкции по выполнению тестов и рекомендаций по именованию тестов и тестовых классов.

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

  • Помогает гарантировать, что компоненты тестирования инфраструктуры не будут случайно включены в модульные тесты.
  • Позволяет контролировать, какой набор тестов выполняется.

В сущности, между конфигурацией для тестов приложений Razor Pages и приложений MVC нет никаких отличий. Единственное отличие заключается в том, как именуются тесты. В приложении Razor Pages тесты конечных точек страницы обычно именуются по классу модели страницы (например, IndexPageTests для тестирования интеграции компонентов на странице индекса). В приложении MVC тесты обычно упорядочены по классам контроллеров и именуются по контроллеру, который они проверяют (например, HomeControllerTests для тестирования интеграции компонентов на контроллере Home).

Проверка необходимых требований к приложению

Тестовый проект должен выполнять следующие требования.

  • Укажите ссылку на пакет Microsoft.AspNetCore.Mvc.Testing.
  • Указывать веб-пакет SDK в файле проекта (<Project Sdk="Microsoft.NET.Sdk.Web">).

Выполнение необходимых требований можно посмотреть в примере приложения. Изучите файл tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. В примере приложения используются платформа тестирования xUnit и библиотека средства синтаксического анализа AngleSharp, поэтому он также ссылается на:

В приложениях, которые используют xunit.runner.visualstudio версии 2.4.2 или более поздней версии, тестовый проект должен ссылаться на пакет Microsoft.NET.Test.Sdk.

В тестах также используется Entity Framework Core. См. файл проекта в GitHub.

Среда ТС

Если среда ТС не задана, то по умолчанию среда имеет значение Development.

Базовые тесты со стандартной WebApplicationFactory

Предопробуй неявно определенный Program класс для тестового проекта, выполнив одно из следующих действий:

  • Предоставить внутренние типы из веб-приложения в тестовый проект. Это можно сделать в файле проекта SUT (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Program Сделайте класс открытым с помощью частичного объявления класса:

    var builder = WebApplication.CreateBuilder(args);
    // ... Configure services, routes, etc.
    app.Run();
    + public partial class Program { }
    
    public class BasicTests 
        : IClassFixture<WebApplicationFactory<Program>>
    {
        private readonly WebApplicationFactory<Program> _factory;
    
        public BasicTests(WebApplicationFactory<Program> factory)
        {
            _factory = factory;
        }
    
        [Theory]
        [InlineData("/")]
        [InlineData("/Index")]
        [InlineData("/About")]
        [InlineData("/Privacy")]
        [InlineData("/Contact")]
        public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
        {
            // Arrange
            var client = _factory.CreateClient();
    
            // Act
            var response = await client.GetAsync(url);
    
            // Assert
            response.EnsureSuccessStatusCode(); // Status Code 200-299
            Assert.Equal("text/html; charset=utf-8", 
                response.Content.Headers.ContentType.ToString());
        }
    }
    

    В примере приложения используется разделяемый Program класс.

WebApplicationFactory<TEntryPoint> используется для создания TestServer для интеграционных тестов. TEntryPoint — это класс точки входа SUT, обычно Program.cs.

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

Следующий тестовый класс, BasicTests, использует WebApplicationFactory для начальной загрузки ТС и предоставляет HttpClient тестовому методу Get_EndpointsReturnSuccessAndCorrectContentType. Метод проверяет успешное выполнение кода состояния ответа (200–299), а Content-Type заголовок предназначен text/html; charset=utf-8 для нескольких страниц приложения.

CreateClient() создает экземпляр класса HttpClient, который автоматически следует за перенаправлениями и обрабатывает файлы cookie.

public class BasicTests 
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}

По умолчанию несущественные cookieэлементы не сохраняются в запросах, когда включена политика согласия Общего регламента по защите данных . Чтобы сохранить ненужные файлы cookie, например те, которые используются поставщиком TempData, пометьте их как основные в тестах. Инструкции по маркировке файла cookie в качестве основы см. в разделе Основные файлы cookie.

AngleSharp vs Application Parts для проверок защиты от подделки

В этой статье для обработки проверок защиты от подделки используется синтаксический анализатор AngleSharp , загружая страницы и анализируя HTML. Для тестирования конечных точек представлений контроллера и Razor Pages на более низком уровне, не заботясь о том, как они отображаются в браузере, рассмотрите возможность использования Application Parts. Подход "Части приложения " внедряет в приложение контроллер или Razor страницу, которые можно использовать для выполнения JSзапросов ON для получения необходимых значений. Дополнительные сведения см. в блоге Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts и связанном репозитории GitHubмартина Костелло.

Настройка WebApplicationFactory

Конфигурацию веб-узла можно создать независимо от тестовых классов путем наследования от WebApplicationFactory<TEntryPoint> для создания одной или нескольких пользовательских фабрик:

  1. Наследование от WebApplicationFactory и переопределение ConfigureWebHost. позволяет IWebHostBuilder настроить коллекцию служб с помощью IWebHostBuilder.ConfigureServices

    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    

    Заполнение базы данных в примере приложения выполняется методом InitializeDbForTests. Этот метод описан в разделе Пример интеграционных тестов: организация приложения для тестирования.

    Контекст базы данных SUT регистрируется в Program.cs. Обратный вызов builder.ConfigureServices тестового приложения выполняется после выполнения кода Program.cs приложения. Чтобы использовать для тестов базу данных, отличную от базы данных приложения, необходимо заменить контекст базы данных приложения в builder.ConfigureServices.

    Пример приложения находит дескриптор службы для контекста базы данных и использует дескриптор для удаления регистрации службы. Затем фабрика добавляет новый ApplicationDbContext объект , который использует для тестов базу данных в памяти.

    Чтобы подключиться к базе данных, отличной от базы данных в памяти, измените вызов UseInMemoryDatabase, чтобы подключить контекст к другой базе данных. Чтобы использовать тестовую базу данных SQL Server, выполните следующие действия.

    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    
  2. Используйте настраиваемый CustomWebApplicationFactory в тестовых классах. В следующем примере используется фабрика в классе IndexPageTests:

    public class IndexPageTests :
        IClassFixture<CustomWebApplicationFactory<Program>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<Program>
            _factory;
    
        public IndexPageTests(
            CustomWebApplicationFactory<Program> factory)
        {
            _factory = factory;
            _client = factory.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
        }
    

    Клиент примера приложения настроен так, чтобы не допустить выполнение клиентов HttpClient следующих перенаправлений. Как описано далее в разделе Имитация проверки подлинности, это позволяет тестам проверять результат первого ответа приложения. Первый ответ — это перенаправление во многих из этих тестов с заголовком Location.

  3. Обычный тест использует HttpClient и вспомогательные методы для обработки запроса и ответа:

    [Fact]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.Equal("/", response.Headers.Location.OriginalString);
    }
    

Любой запрос POST к ТС должен соответствовать проверке защиты от подделки, которая автоматически вносится в систему защиты данных от подделки приложения. Чтобы упорядочить запрос POST теста, тестовое приложение должно:

  1. Выполнить запрос к странице.
  2. Проанализировать файл cookie для защиты от подделки и запросить маркер проверки из ответа.
  3. Выполните запрос POST с файлом cookie для защиты от подделки и запросом маркера проверки на месте.

Вспомогательные методы расширения SendAsync (Helpers/HttpClientExtensions.cs) и вспомогательный метод GetDocumentAsync (Helpers/HtmlHelpers.cs) в примере приложения используют средство синтаксического анализа AngleSharp для обработки защиты от подделки с помощью следующих методов:

  • GetDocumentAsync: получает HttpResponseMessage и возвращает IHtmlDocument. GetDocumentAsync использует фабрику, которая подготавливает виртуальный ответ на основе исходного HttpResponseMessage. Дополнительные сведения см. в документации по AngleSharp.
  • Методы расширения SendAsync для HttpClient составляют HttpRequestMessage и вызывают SendAsync(HttpRequestMessage) для отправки запросов к ТС. Перегрузки для SendAsync принимают HTML-форму (IHtmlFormElement) и следующие элементы:
    • Кнопка "Отправить" в форме (IHtmlElement)
    • Коллекция значений формы (IEnumerable<KeyValuePair<string, string>>)
    • Кнопка "Отправить" (IHtmlElement) и значения формы (IEnumerable<KeyValuePair<string, string>>)

AngleSharp — это сторонняя библиотека синтаксического анализа, используемая в целях демонстрации в этой статье и в примере приложения. AngleSharp не поддерживается интеграционным тестированием приложением ASP.NET Core и не требуется для этого. Можно использовать и другие средства синтаксического анализа, такие как Html Agility Pack (HAP). Другой подход заключается в написании кода для непосредственной работы с маркером проверки запроса системы защиты от подделки и файлом cookie защиты от подделки. Дополнительные сведения см. в статье AngleSharp vs Application Parts for antiforgery checks в этой статье.

Поставщик базы данных EF-Core в памяти можно использовать для ограниченного и базового тестирования, однако для тестирования в памяти рекомендуется использовать поставщик SQLite.

Настройка клиента с помощью WithWebHostBuilder

Если в методе теста требуется дополнительная настройка, WithWebHostBuilder создает новый объект WebApplicationFactory с IWebHostBuilder, который дополнительно настраивается в конфигурации.

Метод теста Post_DeleteMessageHandler_ReturnsRedirectToRoot в примере приложения демонстрирует использование WithWebHostBuilder. Этот тест выполняет удаление записи из базы данных, активируя отправку формы в ТС.

Поскольку другой тест в классе IndexPageTests выполняет операцию, которая удаляет все записи из базы данных и может выполняться до метода Post_DeleteMessageHandler_ReturnsRedirectToRoot, база данных повторно заполняется в этом методе теста, чтобы обеспечить наличие записи для ее удаления ТС. Выбор первой кнопки удаления в форме messages в ТС имитируется в запросе к ТС:

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

Параметры клиента

WebApplicationFactoryClientOptions На странице приведены значения по умолчанию и доступные параметры при создании HttpClient экземпляров.

WebApplicationFactoryClientOptions Создайте класс и передайте его в метод :CreateClient()

public class IndexPageTests :
    IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program>
        _factory;

    public IndexPageTests(
        CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

Вставка служб имитации

Службы можно переопределить в тесте с помощью вызова ConfigureTestServices в построителе узлов.

Пример ТС включает службу с заданной областью, которая возвращает цитату. Цитата внедряется в скрытое поле на странице индекса при запросе страницы индекса.

Services/IQuoteService.cs:

public interface IQuoteService
{
    Task<string> GenerateQuote();
}

Services/QuoteService.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}

Program.cs:

services.AddScoped<IQuoteService, QuoteService>();

Pages/Index.cshtml.cs:

public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

    public async Task OnGetAsync()
    {
        Messages = await _db.GetMessagesAsync();

        Quote = await _quoteService.GenerateQuote();
    }

Pages/Index.cs:

<input id="quote" type="hidden" value="@Model.Quote">

При запуске приложения ТС создается следующая разметка:

<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">

Чтобы протестировать службу и внедрение цитат в интеграционный тест, служба имитации будет внедрена в ТС тестом. Служба имитации заменяет QuoteService приложения службой, предоставляемой тестовым приложением, с именем TestQuoteService:

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

Вызывается ConfigureTestServices и регистрируется служба с заданной областью:

[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}

Разметка, созданная во время выполнения теста, отражает текст цитаты, предоставленный TestQuoteService, поэтому утверждение передается следующим образом:

<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">

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

Тесты в классе AuthTests проверяют, что безопасная конечная точка:

  • Перенаправляет пользователя без проверки подлинности на страницу входа в приложение.
  • возвращает содержимое для пользователя, прошедшего проверку подлинности.

В ТС на странице /SecurePage используется соглашение AuthorizePage, чтобы применить AuthorizeFilter к странице. Дополнительные сведения см. в разделе Соглашения проверки подлинности RazorPages.

services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});

В тесте Get_SecurePageRedirectsAnUnauthenticatedUserWebApplicationFactoryClientOptions настроен таким образом, чтобы запретить перенаправление (путем установки значения для параметра falseAllowAutoRedirect):

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}

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

  • Код состояния, возвращенный SUT, можно проверить с ожидаемым HttpStatusCode.Redirect результатом, а не с кодом конечного состояния после перенаправления на страницу входа, который будет иметь значение HttpStatusCode.OK.
  • Значение Location заголовка в заголовках ответов проверяется, чтобы убедиться, что оно начинается с http://localhost/Identity/Account/Login, а не с окончательного ответа страницы входа, где Location заголовок не будет присутствовать.

Тестовое приложение может имитировать макет AuthenticationHandler<TOptions> в ConfigureTestServices для тестирования аспектов проверки подлинности и авторизации. Для минимального сценария возвращается AuthenticateResult.Success:

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

Для проверки подлинности пользователя вызывается TestAuthHandler, если для схемы проверки подлинности задано TestScheme, где AddAuthentication зарегистрировано для ConfigureTestServices. Важно, чтобы схема TestScheme соответствовала схеме, которую ожидает приложение. В противном случае проверка подлинности не будет работать.

[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(defaultScheme: "TestScheme")
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "TestScheme", options => { });
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Дополнительные сведения о WebApplicationFactoryClientOptions см. в разделе Параметры клиента.

Базовые тесты для ПО промежуточного слоя для проверки подлинности

Основные тесты ПО промежуточного слоя для проверки подлинности см. в этом репозитории GitHub . Он содержит тестовый сервер , относящееся к сценарию тестирования.

Указание среды

Задайте среду в фабрике пользовательских приложений:

public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbContextOptions<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}

Определение тестовой инфраструктурой пути к корневому каталогу содержимого приложения

Конструктор WebApplicationFactory выводит путь к корневому каталогу содержимого приложения, выполняя поиск WebApplicationFactoryContentRootAttribute в сборке, содержащей интеграционные тесты с ключом, равным сборке System.Reflection.Assembly.FullNameTEntryPoint. Если атрибут с правильным ключом не найден, WebApplicationFactory возвращается к поиску файла решения (SLN) и добавляет имя сборки TEntryPoint в каталог решения. Корневой каталог приложения (корневой путь к содержимому) используется для обнаружения представлений и файлов содержимого.

Отключение теневого копирования

Теневое копирование приводит к тому, что тесты выполняются в каталоге, отличном от выходного каталога. Если тесты зависят от загрузки файлов относительно Assembly.Location и возникают проблемы, может потребоваться отключение теневого копирования.

Чтобы сделать это во время использования xUnit, создайте файл xunit.runner.json в каталоге тестового проекта с правильными настройками конфигурации:

{
  "shadowCopy": false
}

Удаление объектов

После выполнения тестов реализации IClassFixtureTestServer и HttpClient удаляются, когда xUnit удаляет WebApplicationFactory. Если объекты, создаваемые разработчиком, требуется удалить, удалите их в реализации IClassFixture. Дополнительные сведения см. в разделе Реализация метода Dispose.

Пример интеграционных тестов

Пример приложения состоит из двух приложений:

Приложение каталог проекта; Описание
Приложение для сообщений (ТС) src/RazorPagesProject Позволяет пользователю добавлять, удалять сообщения (по одному или все) и анализировать их.
Тестирование приложения. tests/RazorPagesProject.Tests Используется для тестирования интеграции ТС.

Тесты можно выполнять с помощью встроенных функций тестирования интегрированной среды разработки, таких как Visual Studio. В Visual Studio Code или в командной строке выполните следующую команду в каталоге tests/RazorPagesProject.Tests:

dotnet test

Организация приложения для сообщений (ТС)

Тестируемая система (ТС) — это система сообщений Razor Pages со следующими характеристиками:

  • Страница индекса приложения (Pages/Index.cshtml и Pages/Index.cshtml.cs) предоставляет методы пользовательского интерфейса и модели страницы для управления добавлением, удалением и анализом сообщений (поиск среднего числа слов на сообщение).
  • Сообщение описывается классом Message (Data/Message.cs) с двумя свойствами: Id (ключ) и Text (сообщение). Свойство Text является обязательным и ограничено 200 символами.
  • Сообщения хранятся с помощью базы данных Entity Framework в памяти†.
  • Приложение содержит слой доступа к данным DAL в своем классе контекста базы данных — AppDbContext (Data/AppDbContext.cs).
  • Если база данных пуста при запуске приложения, то хранилище сообщений инициализируется тремя сообщениями.
  • Приложение включает /SecurePage, доступ к которому может получить только пользователь, прошедший проверку подлинности.

†. В статье Ef Test with InMemory (Тестирование с помощью InMemory) объясняется, как использовать базу данных в памяти для тестов с ПОМОЩЬЮ MSTest. В этом разделе используется платформа тестирования xUnit. Концепции тестирования и реализации тестов в разных платформах тестирования похожи, но не идентичны.

Несмотря на то, что приложение не использует шаблон репозитория и не является эффективным примером шаблона "единица работы" (UoW), Razor Pages поддерживает такие шаблоны разработки. Дополнительные сведения см. в разделах Проектирование уровня сохраняемости инфраструктуры и Логика контроллера тестирования (пример реализует шаблон репозитория).

Организация приложения для тестирования

Тестовое приложение — это консольное приложение в папке tests/RazorPagesProject.Tests.

Каталог тестового приложения Описание
AuthTests Содержит методы теста для:
  • доступа к защищенной странице пользователя, не прошедшего проверку подлинности;
  • доступа к защищенной странице пользователя, прошедшего проверку подлинности с помощью макета AuthenticationHandler<TOptions>;
  • получения профиля пользователя GitHub и проверки имени входа пользователя профиля.
BasicTests Содержит метод теста для маршрутизации и типа содержимого.
IntegrationTests Содержит интеграционные тесты для страницы индекса с помощью настраиваемого класса WebApplicationFactory.
Helpers/Utilities
  • Utilities.cs содержит метод InitializeDbForTests, используемый для заполнения базы данных тестовыми данными.
  • HtmlHelpers.cs предоставляет метод, возвращающий IHtmlDocument AngleSharp для использования методами теста.
  • HttpClientExtensions.cs предоставляет перегрузки для SendAsync для отправки запросов к ТС.

Используемая платформа тестирования — xUnit. Интеграционные тесты проводятся с помощью Microsoft.AspNetCore.TestHost, который включает TestServer. Поскольку пакет Microsoft.AspNetCore.Mvc.Testing используется для настройки узла тестирования и тестового сервера, для пакетов TestHost и TestServer не требуются прямые ссылки на пакеты в файле проекта тестового приложения или в конфигурации разработчика в тестовом приложении.

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

Пример приложения заполняет базу данных тремя сообщениями в Utilities.cs, которые могут использоваться тестами при выполнении:

public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}

Контекст базы данных SUT регистрируется в Program.cs. Обратный вызов builder.ConfigureServices тестового приложения выполняется после выполнения кода Program.cs приложения. Чтобы использовать для тестов другую базу данных, необходимо заменить контекст базы данных приложения в builder.ConfigureServices. Дополнительные сведения см. в разделе Настройка WebApplicationFactory.

Дополнительные ресурсы

В этом разделе предполагается базовое понимание модульных тестов. Если вы не знакомы с концепциями тестирования, ознакомьтесь с разделом Модульное тестирование в .NET Core и .NET Standard и связанным с ним содержимым.

Просмотреть или скачать образец кода (как скачивать)

В качестве примера используется приложение Razor Pages (предполагается базовое понимание Razor Pages). Если вы не знакомы с Razor Pages, см. следующие разделы:

Примечание

Для тестирования spaas рекомендуется использовать такое средство, как Playwright для .NET, которое может автоматизировать браузер.

Общие сведения об интеграционных тестах

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

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

  • База данных
  • Файловая система
  • Сетевые устройства
  • Конвейер "запрос-ответ"

В модульных тестах вместо компонентов инфраструктуры используются структурные компоненты, известные как имитации или макеты объектов.

В отличие от модульных тестов, интеграционные тесты:

  • Используют фактические компоненты, которые приложение использует в рабочей среде.
  • Требуют больше кода и обработки данных.
  • Выполняются дольше.

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

В обсуждениях интеграционных тестов тесты тестируемый проект часто называется тестируемой системой или сокращенно "SUT". "SUT" используется в этой статье для обозначения ASP.NET Core тестируемого приложения.

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

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

Для интеграционных тестов в ASP.NET Core требуется следующее:

  • Тестовый проект, который используется для хранения и выполнения тестов. Тестовый проект содержит ссылку на ТС.
  • Тестовый проект создает тестовый веб-узел для ТС и использует клиент тестового сервера для обработки запросов и ответов с помощью ТС.
  • Средство запуска тестов используется для выполнения тестов и передачи результатов тестов.

Интеграционные тесты придерживаются последовательности событий из обычных шагов теста Подготовка, Выполнение и Проверка.

  1. Настраивается веб-узел ТС.
  2. Создается клиент тестового сервера для отправки запросов к приложению.
  3. Выполняется шаг теста Подготовка: тестовое приложение готовит запрос.
  4. Выполняется шаг теста Выполнение: клиент отправляет запрос и получает ответ.
  5. Выполняется шаг теста Проверка: фактический ответ определяется как правильный или ошибочный на основе ожидаемого ответа.
  6. Процесс продолжается до тех пор, пока не будут выполнены все тесты.
  7. Выводятся результаты теста.

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

Компоненты инфраструктуры, такие как тестовый веб-узел и сервер тестирования в памяти (TestServer), предоставляются или управляются пакетом Microsoft.AspNetCore.Mvc.Testing. Использование этого пакета упрощает создание и выполнение тестов.

Пакет Microsoft.AspNetCore.Mvc.Testing выполняет следующие задачи:

  • Копирует файл зависимостей (.deps) из ТС в папку bin тестового проекта.
  • Задает корневой каталог содержимого в корне проекта ТС, чтобы при выполнении тестов были найдены статические файлы и страницы или представления.
  • Предоставляет класс WebApplicationFactory для упрощения начальной загрузки ТС с TestServer.

В документации модульные тесты описано, как настроить тестовый проект и средство запуска тестов, а также представлены подробные инструкции по выполнению тестов и рекомендаций по именованию тестов и тестовых классов.

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

  • Помогает гарантировать, что компоненты тестирования инфраструктуры не будут случайно включены в модульные тесты.
  • Позволяет контролировать, какой набор тестов выполняется.

В сущности, между конфигурацией для тестов приложений Razor Pages и приложений MVC нет никаких отличий. Единственное отличие заключается в том, как именуются тесты. В приложении Razor Pages тесты конечных точек страницы обычно именуются по классу модели страницы (например, IndexPageTests для тестирования интеграции компонентов на странице индекса). В приложении MVC тесты обычно упорядочены по классам контроллеров и именуются по контроллеру, который они проверяют (например, HomeControllerTests для тестирования интеграции компонентов на контроллере Home).

Проверка необходимых требований к приложению

Тестовый проект должен выполнять следующие требования.

  • Укажите ссылку на пакет Microsoft.AspNetCore.Mvc.Testing.
  • Указывать веб-пакет SDK в файле проекта (<Project Sdk="Microsoft.NET.Sdk.Web">).

Выполнение необходимых требований можно посмотреть в примере приложения. Изучите файл tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. В примере приложения используются платформа тестирования xUnit и библиотека средства синтаксического анализа AngleSharp, поэтому он также ссылается на:

В приложениях, которые используют xunit.runner.visualstudio версии 2.4.2 или более поздней версии, тестовый проект должен ссылаться на пакет Microsoft.NET.Test.Sdk.

В тестах также используется Entity Framework Core. Ссылки на приложение:

Среда ТС

Если среда ТС не задана, то по умолчанию среда имеет значение Development.

Базовые тесты со стандартной WebApplicationFactory

В ASP.NET Core 6 представлен класс WebApplication, который избавляет от необходимости использовать класс Startup. Чтобы выполнить тестирование с WebApplicationFactory без класса Startup, приложение ASP.NET Core 6 должно предоставить явно определенный класс Program тестовому проекту одним из приведенных ниже способов.

  • Предоставить внутренние типы из веб-приложения в тестовый проект. Это можно сделать в файле проекта (.csproj):
    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Сделать класс Program открытым с помощью объявления разделяемого класса:
    var builder = WebApplication.CreateBuilder(args);
    // ... Configure services, routes, etc.
    app.Run();
    + public partial class Program { }
    

Когда изменения в веб-приложение будут внесены, тестовый проект сможет использовать класс Program для WebApplicationFactory.

[Fact]
public async Task HelloWorldTest()
{
    var application = new WebApplicationFactory<Program>()
        .WithWebHostBuilder(builder =>
        {
            // ... Configure test services
        });

    var client = application.CreateClient();
    //...
}

WebApplicationFactory<TEntryPoint> используется для создания TestServer для интеграционных тестов. TEntryPoint — это класс точки входа ТС, обычно класс Startup.

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

Следующий тестовый класс, BasicTests, использует WebApplicationFactory для начальной загрузки ТС и предоставляет HttpClient тестовому методу Get_EndpointsReturnSuccessAndCorrectContentType. Метод проверяет, что код состояния ответа свидетельствует о выполнении (коды состояния в диапазоне от 200 до 299) и что заголовок Content-Type имеет значение text/html; charset=utf-8 для нескольких страниц приложения.

CreateClient() создает экземпляр класса HttpClient, который автоматически следует за перенаправлениями и обрабатывает файлы cookie.

public class BasicTests 
    : IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
    private readonly WebApplicationFactory<RazorPagesProject.Startup> _factory;

    public BasicTests(WebApplicationFactory<RazorPagesProject.Startup> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}

По умолчанию некритические файлы cookie не сохраняются в запросах, если включена политика согласия GDPR. Чтобы сохранить ненужные файлы cookie, например те, которые используются поставщиком TempData, пометьте их как основные в тестах. Инструкции по маркировке файла cookie в качестве основы см. в разделе Основные файлы cookie.

Настройка WebApplicationFactory

Конфигурацию веб-узла можно создать независимо от тестовых классов путем наследования от WebApplicationFactory для создания одной или нескольких пользовательских фабрик:

  1. Наследование от WebApplicationFactory и переопределение ConfigureWebHost. IWebHostBuilder позволяет настраивать коллекцию служб с помощью ConfigureServices:

    public class CustomWebApplicationFactory<TStartup>
        : WebApplicationFactory<TStartup> where TStartup: class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));
    
                services.Remove(descriptor);
    
                services.AddDbContext<ApplicationDbContext>(options =>
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                });
    
                var sp = services.BuildServiceProvider();
    
                using (var scope = sp.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices.GetRequiredService<ApplicationDbContext>();
                    var logger = scopedServices
                        .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
    
                    db.Database.EnsureCreated();
    
                    try
                    {
                        Utilities.InitializeDbForTests(db);
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, "An error occurred seeding the " +
                            "database with test messages. Error: {Message}", ex.Message);
                    }
                }
            });
        }
    }
    

    Заполнение базы данных в примере приложения выполняется методом InitializeDbForTests. Этот метод описан в разделе Пример интеграционных тестов: организация приложения для тестирования.

    Контекст базы данных ТС регистрируется в методе Startup.ConfigureServices. Обратный вызов builder.ConfigureServices тестового приложения выполняется после выполнения кода Startup.ConfigureServices приложения. Порядок выполнения является критическим изменением для универсального узла с выпуском ASP.NET Core 3.0. Чтобы использовать для тестов базу данных, отличную от базы данных приложения, необходимо заменить контекст базы данных приложения в builder.ConfigureServices.

    В тестируемых системах, которые по-прежнему используют веб-узел, обратный вызов builder.ConfigureServices тестового приложения выполняется до выполнения кода Startup.ConfigureServices тестируемой системы. Обратный вызов builder.ConfigureTestServices тестового приложения выполняется позже.

    Пример приложения находит дескриптор службы для контекста базы данных и использует дескриптор для удаления регистрации службы. Затем фабрика добавляет новый ApplicationDbContext, который использует базу данных в памяти для тестов.

    Чтобы подключиться к базе данных, отличной от базы данных в памяти, измените вызов UseInMemoryDatabase, чтобы подключить контекст к другой базе данных. Чтобы использовать тестовую базу данных SQL Server, выполните следующие действия.

    • Сошлитесь на пакет NuGet Microsoft.EntityFrameworkCore.SqlServer в файле проекта.
    • Вызовите UseSqlServer со строкой подключения к базе данных.
    services.AddDbContext<ApplicationDbContext>((options, context) => 
    {
        context.UseSqlServer(
            Configuration.GetConnectionString("TestingDbConnectionString"));
    });
    
  2. Используйте настраиваемый CustomWebApplicationFactory в тестовых классах. В следующем примере используется фабрика в классе IndexPageTests:

    public class IndexPageTests : 
        IClassFixture<CustomWebApplicationFactory<RazorPagesProject.Startup>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<RazorPagesProject.Startup> 
            _factory;
    
        public IndexPageTests(
            CustomWebApplicationFactory<RazorPagesProject.Startup> factory)
        {
            _factory = factory;
            _client = factory.CreateClient(new WebApplicationFactoryClientOptions
                {
                    AllowAutoRedirect = false
                });
        }
    

    Клиент примера приложения настроен так, чтобы не допустить выполнение клиентов HttpClient следующих перенаправлений. Как описано далее в разделе Имитация проверки подлинности, это позволяет тестам проверять результат первого ответа приложения. Первый ответ — это перенаправление во многих из этих тестов с заголовком Location.

  3. Обычный тест использует HttpClient и вспомогательные методы для обработки запроса и ответа:

    [Fact]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.Equal("/", response.Headers.Location.OriginalString);
    }
    

Любой запрос POST к ТС должен соответствовать проверке защиты от подделки, которая автоматически вносится в систему защиты данных от подделки приложения. Чтобы упорядочить запрос POST теста, тестовое приложение должно:

  1. Выполнить запрос к странице.
  2. Проанализировать файл cookie для защиты от подделки и запросить маркер проверки из ответа.
  3. Выполните запрос POST с файлом cookie для защиты от подделки и запросом маркера проверки на месте.

Вспомогательные методы расширения SendAsync (Helpers/HttpClientExtensions.cs) и вспомогательный метод GetDocumentAsync (Helpers/HtmlHelpers.cs) в примере приложения используют средство синтаксического анализа AngleSharp для обработки защиты от подделки с помощью следующих методов:

  • GetDocumentAsync: получает HttpResponseMessage и возвращает IHtmlDocument. GetDocumentAsync использует фабрику, которая подготавливает виртуальный ответ на основе исходного HttpResponseMessage. Дополнительные сведения см. в документации по AngleSharp.
  • Методы расширения SendAsync для HttpClient составляют HttpRequestMessage и вызывают SendAsync(HttpRequestMessage) для отправки запросов к ТС. Перегрузки для SendAsync принимают HTML-форму (IHtmlFormElement) и следующие элементы:
    • Кнопка "Отправить" в форме (IHtmlElement)
    • Коллекция значений формы (IEnumerable<KeyValuePair<string, string>>)
    • Кнопка "Отправить" (IHtmlElement) и значения формы (IEnumerable<KeyValuePair<string, string>>)

Примечание

AngleSharp — это сторонняя библиотека анализа, используемая для демонстрационных целей в этом разделе и в примере приложения. AngleSharp не поддерживается интеграционным тестированием приложением ASP.NET Core и не требуется для этого. Можно использовать и другие средства синтаксического анализа, такие как Html Agility Pack (HAP). Другой подход заключается в написании кода для непосредственной работы с маркером проверки запроса системы защиты от подделки и файлом cookie защиты от подделки.

Примечание

Поставщик базы данных EF-Core в памяти можно использовать для ограниченного и базового тестирования, однако для тестирования в памяти рекомендуется использовать поставщик SQLite.

Настройка клиента с помощью WithWebHostBuilder

Если в методе теста требуется дополнительная настройка, WithWebHostBuilder создает новый объект WebApplicationFactory с IWebHostBuilder, который дополнительно настраивается в конфигурации.

Метод теста Post_DeleteMessageHandler_ReturnsRedirectToRoot в примере приложения демонстрирует использование WithWebHostBuilder. Этот тест выполняет удаление записи из базы данных, активируя отправку формы в ТС.

Поскольку другой тест в классе IndexPageTests выполняет операцию, которая удаляет все записи из базы данных и может выполняться до метода Post_DeleteMessageHandler_ReturnsRedirectToRoot, база данных повторно заполняется в этом методе теста, чтобы обеспечить наличие записи для ее удаления ТС. Выбор первой кнопки удаления в форме messages в ТС имитируется в запросе к ТС:

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                var serviceProvider = services.BuildServiceProvider();

                using (var scope = serviceProvider.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices
                        .GetRequiredService<ApplicationDbContext>();
                    var logger = scopedServices
                        .GetRequiredService<ILogger<IndexPageTests>>();

                    try
                    {
                        Utilities.ReinitializeDbForTests(db);
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, "An error occurred seeding " +
                            "the database with test messages. Error: {Message}", 
                            ex.Message);
                    }
                }
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

Параметры клиента

В следующей таблице показаны WebApplicationFactoryClientOptions по умолчанию, доступные при создании экземпляров HttpClient.

Параметр Описание По умолчанию
AllowAutoRedirect Возвращает или задает, должны ли экземпляры HttpClient автоматически следовать ответам перенаправления. true
BaseAddress Возвращает или задает базовый адрес экземпляров HttpClient. http://localhost
HandleCookies Возвращает или задает, должны ли экземпляры HttpClient обрабатывать файлы cookie. true
MaxAutomaticRedirections Возвращает или задает максимальное число ответов на перенаправление, которым должны следовать экземпляры HttpClient. 7

Создайте класс WebApplicationFactoryClientOptions и передайте его в метод CreateClient() (значения по умолчанию показаны в примере кода):

// Default client option values are shown
var clientOptions = new WebApplicationFactoryClientOptions();
clientOptions.AllowAutoRedirect = true;
clientOptions.BaseAddress = new Uri("http://localhost");
clientOptions.HandleCookies = true;
clientOptions.MaxAutomaticRedirections = 7;

_client = _factory.CreateClient(clientOptions);

Вставка служб имитации

Службы можно переопределить в тесте с помощью вызова ConfigureTestServices в построителе узлов. Чтобы внедрить службы имитации, в ТС должен иметься класс Startup с методом Startup.ConfigureServices.

Пример ТС включает службу с заданной областью, которая возвращает цитату. Цитата внедряется в скрытое поле на странице индекса при запросе страницы индекса.

Services/IQuoteService.cs:

public interface IQuoteService
{
    Task<string> GenerateQuote();
}

Services/QuoteService.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}

Startup.cs:

services.AddScoped<IQuoteService, QuoteService>();

Pages/Index.cshtml.cs:

public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

    public async Task OnGetAsync()
    {
        Messages = await _db.GetMessagesAsync();

        Quote = await _quoteService.GenerateQuote();
    }

Pages/Index.cs:

<input id="quote" type="hidden" value="@Model.Quote">

При запуске приложения ТС создается следующая разметка:

<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">

Чтобы протестировать службу и внедрение цитат в интеграционный тест, служба имитации будет внедрена в ТС тестом. Служба имитации заменяет QuoteService приложения службой, предоставляемой тестовым приложением, с именем TestQuoteService:

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

Вызывается ConfigureTestServices и регистрируется служба с заданной областью:

[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}

Разметка, созданная во время выполнения теста, отражает текст цитаты, предоставленный TestQuoteService, поэтому утверждение передается следующим образом:

<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">

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

Тесты в классе AuthTests проверяют, что безопасная конечная точка:

  • перенаправляет пользователя, не прошедшего проверку подлинности, на страницу входа;
  • возвращает содержимое для пользователя, прошедшего проверку подлинности.

В ТС на странице /SecurePage используется соглашение AuthorizePage, чтобы применить AuthorizeFilter к странице. Дополнительные сведения см. в разделе Соглашения проверки подлинности RazorPages.

services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});

В тесте Get_SecurePageRedirectsAnUnauthenticatedUserWebApplicationFactoryClientOptions настроен таким образом, чтобы запретить перенаправление (путем установки значения для параметра falseAllowAutoRedirect):

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login", 
        response.Headers.Location.OriginalString);
}

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

  • Код состояния, возвращаемый ТС, можно проверить на соответствие ожидаемому результату HttpStatusCode.Redirect, а не окончательному коду состояния после перенаправления на страницу входа, который будет равен HttpStatusCode.OK.
  • Значение заголовка Location в заголовках ответа проверяется, чтобы подтвердить, что он начинается с http://localhost/Identity/Account/Login, а не с последнего ответа страницы входа, где отсутствует заголовок Location.

Тестовое приложение может имитировать макет AuthenticationHandler<TOptions> в ConfigureTestServices для тестирования аспектов проверки подлинности и авторизации. Для минимального сценария возвращается AuthenticateResult.Success:

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, 
        ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "Test");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

Для проверки подлинности пользователя вызывается TestAuthHandler, если для схемы проверки подлинности задано Test, где AddAuthentication зарегистрировано для ConfigureTestServices. Важно, чтобы схема Test соответствовала схеме, которую ожидает приложение. В противном случае проверка подлинности не будет работать.

[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication("Test")
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "Test", options => {});
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization = 
        new AuthenticationHeaderValue("Test");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Дополнительные сведения о WebApplicationFactoryClientOptions см. в разделе Параметры клиента.

Указание среды

По умолчанию узел и среда приложений ТС настроены для использования среды Development. Чтобы переопределить среду ТС при использовании IHostBuilder, выполните следующие действия:

  • Задайте переменную среды ASPNETCORE_ENVIRONMENT (например, Staging, Production или другое настраиваемое значение, например Testing).
  • Переопределите CreateHostBuilder в тестовом приложении, чтобы считать переменные среды с префиксом ASPNETCORE.
protected override IHostBuilder CreateHostBuilder() =>
    base.CreateHostBuilder()
        .ConfigureHostConfiguration(
            config => config.AddEnvironmentVariables("ASPNETCORE"));

Если ТС использует веб-узел (IWebHostBuilder), переопределите CreateWebHostBuilder.

protected override IWebHostBuilder CreateWebHostBuilder() =>
    base.CreateWebHostBuilder().UseEnvironment("Testing");

Определение тестовой инфраструктурой пути к корневому каталогу содержимого приложения

Конструктор WebApplicationFactory выводит путь к корневому каталогу содержимого приложения, выполняя поиск WebApplicationFactoryContentRootAttribute в сборке, содержащей интеграционные тесты с ключом, равным сборке System.Reflection.Assembly.FullNameTEntryPoint. Если атрибут с правильным ключом не найден, WebApplicationFactory возвращается к поиску файла решения (SLN) и добавляет имя сборки TEntryPoint в каталог решения. Корневой каталог приложения (корневой путь к содержимому) используется для обнаружения представлений и файлов содержимого.

Отключение теневого копирования

Теневое копирование приводит к тому, что тесты выполняются в каталоге, отличном от выходного каталога. Если тесты зависят от загрузки файлов относительно Assembly.Location и возникают проблемы, может потребоваться отключение теневого копирования.

Чтобы сделать это во время использования xUnit, создайте файл xunit.runner.json в каталоге тестового проекта с правильными настройками конфигурации:

{
  "shadowCopy": false
}

Удаление объектов

После выполнения тестов реализации IClassFixtureTestServer и HttpClient удаляются, когда xUnit удаляет WebApplicationFactory. Если объекты, создаваемые разработчиком, требуется удалить, удалите их в реализации IClassFixture. Дополнительные сведения см. в разделе Реализация метода Dispose.

Пример интеграционных тестов

Пример приложения состоит из двух приложений:

Приложение каталог проекта; Описание
Приложение для сообщений (ТС) src/RazorPagesProject Позволяет пользователю добавлять, удалять сообщения (по одному или все) и анализировать их.
Тестирование приложения. tests/RazorPagesProject.Tests Используется для тестирования интеграции ТС.

Тесты можно выполнять с помощью встроенных функций тестирования интегрированной среды разработки, таких как Visual Studio. В Visual Studio Code или в командной строке выполните следующую команду в каталоге tests/RazorPagesProject.Tests:

dotnet test

Организация приложения для сообщений (ТС)

Тестируемая система (ТС) — это система сообщений Razor Pages со следующими характеристиками:

  • Страница индекса приложения (Pages/Index.cshtml и Pages/Index.cshtml.cs) предоставляет методы пользовательского интерфейса и модели страницы для управления добавлением, удалением и анализом сообщений (поиск среднего числа слов на сообщение).
  • Сообщение описывается классом Message (Data/Message.cs) с двумя свойствами: Id (ключ) и Text (сообщение). Свойство Text является обязательным и ограничено 200 символами.
  • Сообщения хранятся с помощью базы данных Entity Framework в памяти†.
  • Приложение содержит слой доступа к данным DAL в своем классе контекста базы данных — AppDbContext (Data/AppDbContext.cs).
  • Если база данных пуста при запуске приложения, то хранилище сообщений инициализируется тремя сообщениями.
  • Приложение включает /SecurePage, доступ к которому может получить только пользователь, прошедший проверку подлинности.

†В разделе документации о EF Тестирование с помощью InMemory объясняется, как использовать базу данных в памяти для тестов с помощью MSTest. В этом разделе используется платформа тестирования xUnit. Концепции тестирования и реализации тестов в разных платформах тестирования похожи, но не идентичны.

Несмотря на то, что приложение не использует шаблон репозитория и не является эффективным примером шаблона "единица работы" (UoW), Razor Pages поддерживает такие шаблоны разработки. Дополнительные сведения см. в разделах Проектирование уровня сохраняемости инфраструктуры и Логика контроллера тестирования (пример реализует шаблон репозитория).

Организация приложения для тестирования

Тестовое приложение — это консольное приложение в папке tests/RazorPagesProject.Tests.

Каталог тестового приложения Описание
AuthTests Содержит методы теста для:
  • доступа к защищенной странице пользователя, не прошедшего проверку подлинности;
  • доступа к защищенной странице пользователя, прошедшего проверку подлинности с помощью макета AuthenticationHandler<TOptions>;
  • получения профиля пользователя GitHub и проверки имени входа пользователя профиля.
BasicTests Содержит метод теста для маршрутизации и типа содержимого.
IntegrationTests Содержит интеграционные тесты для страницы индекса с помощью настраиваемого класса WebApplicationFactory.
Helpers/Utilities
  • Utilities.cs содержит метод InitializeDbForTests, используемый для заполнения базы данных тестовыми данными.
  • HtmlHelpers.cs предоставляет метод, возвращающий IHtmlDocument AngleSharp для использования методами теста.
  • HttpClientExtensions.cs предоставляет перегрузки для SendAsync для отправки запросов к ТС.

Используемая платформа тестирования — xUnit. Интеграционные тесты проводятся с помощью Microsoft.AspNetCore.TestHost, который включает TestServer. Поскольку пакет Microsoft.AspNetCore.Mvc.Testing используется для настройки узла тестирования и тестового сервера, для пакетов TestHost и TestServer не требуются прямые ссылки на пакеты в файле проекта тестового приложения или в конфигурации разработчика в тестовом приложении.

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

Пример приложения заполняет базу данных тремя сообщениями в Utilities.cs, которые могут использоваться тестами при выполнении:

public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}

Контекст базы данных ТС регистрируется в методе Startup.ConfigureServices. Обратный вызов builder.ConfigureServices тестового приложения выполняется после выполнения кода Startup.ConfigureServices приложения. Чтобы использовать для тестов другую базу данных, необходимо заменить контекст базы данных приложения в builder.ConfigureServices. Дополнительные сведения см. в разделе Настройка WebApplicationFactory.

В тестируемых системах, которые по-прежнему используют веб-узел, обратный вызов builder.ConfigureServices тестового приложения выполняется до выполнения кода Startup.ConfigureServices тестируемой системы. Обратный вызов builder.ConfigureTestServices тестового приложения выполняется позже.

Дополнительные ресурсы