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

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

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

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

Просмотреть или скачать образец кода (описание загрузки)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Копирует файл зависимостей (.deps) из SUT в каталог тестового проекта 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 класс для тестового проекта, выполнив одно из следующих действий:

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 страниц на более низком уровне, не заботясь о том, как они отображаются в браузере, рассмотрите возможность использования Application Parts. Подход "Части приложения" внедряет контроллер или Razor страницу в приложение, которое можно использовать для выполнения JSзапросов ON для получения необходимых значений. Дополнительные сведения см. в блоге по тестированию интеграции ASP.NET основных ресурсов, защищенных с помощью антифоргерии с помощью частей приложений и связанного репозитория GitHub мартином Costello.

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

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

  1. Используйте настраиваемый 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.

  2. Обычный тест использует 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 и Application Parts для антифоргерийных проверка в этой статье.

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

См. раздел "Расширение запуска с помощью фильтров запуска", в котором показано, как настроить по промежуточному слоям, IStartupFilterкоторое полезно, если тест требует пользовательской службы или ПО промежуточного слоя.

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

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

Пример кода вызывается WithWebHostBuilder для замены настроенных служб заглушками теста. Дополнительные сведения и примеры использования см. в статье "Внедрение макетов служб ".

Метод теста 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
        });
    }

ПРИМЕЧАНИЕ. Чтобы избежать предупреждений перенаправления HTTPS в журналах при использовании ПО промежуточного слоя перенаправления HTTPS, задайте BaseAddress = new Uri("https://localhost")

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

Службы можно переопределить в тесте с помощью вызова ConfigureTestServices в построителе узлов. Чтобы область переопределенные службы для самого теста, WithWebHostBuilder метод используется для получения построителя узлов. Это можно увидеть в следующих тестах:

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

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 настроен таким образом, чтобы запретить перенаправление (путем установки значения false для параметра AllowAutoRedirect):

[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)
        : base(options, logger, encoder)
    {
    }

    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.

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

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

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

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

dotnet test

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

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

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

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

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

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

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

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

Используемая платформа тестирования — 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, см. следующие разделы:

Примечание.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Копирует файл зависимостей (.deps) из SUT в каталог тестового проекта 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

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 настроен таким образом, чтобы запретить перенаправление (путем установки значения false для параметра AllowAutoRedirect):

[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.

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

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

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

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

dotnet test

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

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

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

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

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

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

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

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

Используемая платформа тестирования — 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 тестового приложения выполняется позже.

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

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

Просмотреть или скачать образец кода (описание загрузки)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Копирует файл зависимостей (.deps) из SUT в каталог тестового проекта 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 класс для тестового проекта, выполнив одно из следующих действий:

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 страниц на более низком уровне, не заботясь о том, как они отображаются в браузере, рассмотрите возможность использования Application Parts. Подход "Части приложения" внедряет контроллер или Razor страницу в приложение, которое можно использовать для выполнения JSзапросов ON для получения необходимых значений. Дополнительные сведения см. в блоге по тестированию интеграции ASP.NET основных ресурсов, защищенных с помощью антифоргерии с помощью частей приложений и связанного репозитория GitHub мартином Costello.

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

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

  1. Используйте настраиваемый 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.

  2. Обычный тест использует 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 и Application Parts для антифоргерийных проверка в этой статье.

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

См. раздел "Расширение запуска с помощью фильтров запуска", в котором показано, как настроить по промежуточному слоям, IStartupFilterкоторое полезно, если тест требует пользовательской службы или ПО промежуточного слоя.

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

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

Пример кода вызывается WithWebHostBuilder для замены настроенных служб заглушками теста. Дополнительные сведения и примеры использования см. в статье "Внедрение макетов служб ".

Метод теста 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
        });
    }

ПРИМЕЧАНИЕ. Чтобы избежать предупреждений перенаправления HTTPS в журналах при использовании ПО промежуточного слоя перенаправления HTTPS, задайте BaseAddress = new Uri("https://localhost")

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

Службы можно переопределить в тесте с помощью вызова ConfigureTestServices в построителе узлов. Чтобы область переопределенные службы для самого теста, WithWebHostBuilder метод используется для получения построителя узлов. Это можно увидеть в следующих тестах:

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

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 настроен таким образом, чтобы запретить перенаправление (путем установки значения false для параметра AllowAutoRedirect):

[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.

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

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

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

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

dotnet test

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

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

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

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

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

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

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

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

Используемая платформа тестирования — 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.

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