Поделиться через


Интеграционные тесты на платформе 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 .
  • Устанавливает корневой каталог содержимого в корневой каталог проекта SUT, чтобы статические файлы и страницы/представления были найдены при выполнении тестов.
  • Предоставляет класс 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

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

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

Следующий тестовый класс, BasicTests, использует WebApplicationFactory для начальной загрузки SUT и предоставляет 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 страницу в приложение, которое можно использовать для отправки запросов JSON для получения необходимых значений. Дополнительные сведения см. в блоге Тестирование интеграции защищенных от фальсификации ресурсов ASP.NET Core с использованием частей приложения и в соответствующем репозитории GitHub Мартином Костелло.

Настройка WebApplicationFactory

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

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

    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType == 
                        typeof(IDbContextOptionsConfiguration<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 рекомендуется для тестирования в памяти.

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

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

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

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

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

Поскольку другой тест в классе IndexPageTests выполняет операцию, которая удаляет все записи из базы данных и может выполняться до метода Post_DeleteMessageHandler_ReturnsRedirectToRoot, база данных в этом методе теста повторно заполняется. Это делается для того, чтобы гарантировать наличие записи, которую можно будет удалить с помощью SUT (система под испытанием). Имитация выбора первой кнопки удаления формы 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">

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

<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_SecurePageRedirectsAnUnauthenticatedUser элемент WebApplicationFactoryClientOptions устанавливается для запрета перенаправлений, устанавливая AllowAutoRedirect в false.

[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(IDbContextOptionsConfiguration<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 в сборке с интеграционными тестами, где ключ равен сборке TEntryPointSystem.Reflection.Assembly.FullName. Если атрибут с правильным ключом не найден, WebApplicationFactory возвращается к поиску файла решения (SLN) и добавляет имя сборки TEntryPoint в каталог решения. Корневой каталог приложения (корневой путь к содержимому) используется для обнаружения представлений и файлов содержимого.

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

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

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

{
  "shadowCopy": false
}

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

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

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

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

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

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

dotnet test

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

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

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

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

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

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

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

Каталог тестового приложения Описание
AuthTests Содержит методы теста для:
  • доступа к защищенной странице пользователя, не прошедшего проверку подлинности;
  • Доступ к защищенной странице пользователем, прошедшим проверку подлинности с помощью макета AuthenticationHandler<TOptions>.
  • Получение профиля пользователя GitHub и проверка имени пользователя для входа в профиль.
BasicTests Содержит метод теста для маршрутизации и типа содержимого.
IntegrationTests Содержит интеграционные тесты для страницы индекса с помощью настраиваемого класса WebApplicationFactory.
Helpers/Utilities
  • Utilities.cs содержит метод InitializeDbForTests, используемый для заполнения базы данных тестовыми данными.
  • HtmlHelpers.cs предоставляет метод для возврата 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 .
  • Устанавливает корневой каталог содержимого в корневой каталог проекта SUT, чтобы статические файлы и страницы/представления были найдены при выполнении тестов.
  • Предоставляет класс 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. Метод описан в примере тестов интеграции: раздел "Тестирование организации приложения".

    Контекст базы данных SUT регистрируется в методе 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, база данных в этом методе теста повторно заполняется. Это делается для того, чтобы гарантировать наличие записи, которую можно будет удалить с помощью SUT (система под испытанием). Выбор первой кнопки удаления в форме 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">

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

<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 настроен для запрета перенаправлений путем установки AllowAutoRedirect в false.

[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);
}

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

  • Код состояния, возвращаемый Системой Под Тестом (CPT), можно сверить с ожидаемым результатом 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 см. в разделе Параметры клиента.

Настройка среды

По умолчанию хост и окружение приложений SUT настроены на использование среды разработки. Чтобы переопределить среду ТС при использовании 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 в сборке с интеграционными тестами, где ключ равен сборке TEntryPointSystem.Reflection.Assembly.FullName. Если атрибут с правильным ключом не найден, WebApplicationFactory возвращается к поиску файла решения (SLN) и добавляет имя сборки TEntryPoint в каталог решения. Корневой каталог приложения (корневой путь к содержимому) используется для обнаружения представлений и файлов содержимого.

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

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

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

{
  "shadowCopy": false
}

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

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

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

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

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

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

dotnet test

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

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

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

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

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

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

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

Каталог тестового приложения Описание
AuthTests Содержит методы теста для:
  • доступа к защищенной странице пользователя, не прошедшего проверку подлинности;
  • Доступ к защищенной странице пользователем, прошедшим проверку подлинности с помощью макета AuthenticationHandler<TOptions>.
  • Получение профиля пользователя GitHub и проверка имени пользователя для входа в профиль.
BasicTests Содержит метод теста для маршрутизации и типа содержимого.
IntegrationTests Содержит интеграционные тесты для страницы индекса с помощью настраиваемого класса WebApplicationFactory.
Helpers/Utilities
  • Utilities.cs содержит метод InitializeDbForTests, используемый для заполнения базы данных тестовыми данными.
  • HtmlHelpers.cs предоставляет метод для возврата 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 регистрируется в методе 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 .
  • Устанавливает корневой каталог содержимого в корневой каталог проекта SUT, чтобы статические файлы и страницы/представления были найдены при выполнении тестов.
  • Предоставляет класс 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 страницу в приложение, которое можно использовать для отправки запросов JSON для получения необходимых значений. Дополнительные сведения см. в блоге Тестирование интеграции защищенных от фальсификации ресурсов ASP.NET Core с использованием частей приложения и в соответствующем репозитории GitHub Мартином Костелло.

Настройка WebApplicationFactory

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

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

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

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

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

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

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

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

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

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

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

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

Поскольку другой тест в классе IndexPageTests выполняет операцию, которая удаляет все записи из базы данных и может выполняться до метода Post_DeleteMessageHandler_ReturnsRedirectToRoot, база данных в этом методе теста повторно заполняется. Это делается для того, чтобы гарантировать наличие записи, которую можно будет удалить с помощью SUT (система под испытанием). Нажатие первой кнопки удаления в форме 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">

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

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

[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 в сборке с интеграционными тестами, где ключ равен сборке TEntryPointSystem.Reflection.Assembly.FullName. Если атрибут с правильным ключом не найден, WebApplicationFactory возвращается к поиску файла решения (SLN) и добавляет имя сборки TEntryPoint в каталог решения. Корневой каталог приложения (корневой путь к содержимому) используется для обнаружения представлений и файлов содержимого.

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

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

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

{
  "shadowCopy": false
}

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

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

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

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

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

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

dotnet test

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

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

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

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

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

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

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

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

Для тестирования 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 .
  • Устанавливает корневой каталог содержимого в корневой каталог проекта SUT, чтобы статические файлы и страницы/представления были найдены при выполнении тестов.
  • Предоставляет класс 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 страницу в приложение, которое можно использовать для отправки запросов JSON для получения необходимых значений. Дополнительные сведения см. в блоге Тестирование интеграции защищенных от фальсификации ресурсов ASP.NET Core с использованием частей приложения и в соответствующем репозитории GitHub Мартином Костелло.

Настройка WebApplicationFactory

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

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

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

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

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

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

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

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

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

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

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

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

Поскольку другой тест в классе IndexPageTests выполняет операцию, которая удаляет все записи из базы данных и может выполняться до метода Post_DeleteMessageHandler_ReturnsRedirectToRoot, база данных в этом методе теста повторно заполняется. Это делается для того, чтобы гарантировать наличие записи, которую можно будет удалить с помощью SUT (система под испытанием). Выбор первой кнопки удаления в форме 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">

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

<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_SecurePageRedirectsAnUnauthenticatedUser параметр WebApplicationFactoryClientOptions настроен на запрет перенаправлений путем установки AllowAutoRedirect в false.

[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 в сборке с интеграционными тестами, где ключ равен сборке TEntryPointSystem.Reflection.Assembly.FullName. Если атрибут с правильным ключом не найден, WebApplicationFactory возвращается к поиску файла решения (SLN) и добавляет имя сборки TEntryPoint в каталог решения. Корневой каталог приложения (корневой путь к содержимому) используется для обнаружения представлений и файлов содержимого.

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

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

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

{
  "shadowCopy": false
}

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

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

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

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

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

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

dotnet test

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

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

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

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

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

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

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

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

Для тестирования 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 .
  • Устанавливает корневой каталог содержимого в корневой каталог проекта SUT, чтобы статические файлы и страницы/представления были найдены при выполнении тестов.
  • Предоставляет класс 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 страницу в приложение, которое можно использовать для отправки запросов JSON для получения необходимых значений. Дополнительные сведения см. в блоге Тестирование интеграции защищенных от фальсификации ресурсов ASP.NET Core с использованием частей приложения и в соответствующем репозитории GitHub Мартином Костелло.

Настройка WebApplicationFactory

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

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

    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType == 
                        typeof(IDbContextOptionsConfiguration<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 рекомендуется для тестирования в памяти.

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

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

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

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

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

Поскольку другой тест в классе IndexPageTests выполняет операцию, которая удаляет все записи из базы данных и может выполняться до метода Post_DeleteMessageHandler_ReturnsRedirectToRoot, база данных в этом методе теста повторно заполняется. Это делается для того, чтобы гарантировать наличие записи, которую можно будет удалить с помощью SUT (система под испытанием). Выбор первой кнопки удаления в форме 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">

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

<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_SecurePageRedirectsAnUnauthenticatedUser параметр WebApplicationFactoryClientOptions настроен на запрет перенаправлений путем установки AllowAutoRedirect в false:

[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(IDbContextOptionsConfiguration<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 в сборке с интеграционными тестами, где ключ равен сборке TEntryPointSystem.Reflection.Assembly.FullName. Если атрибут с правильным ключом не найден, WebApplicationFactory возвращается к поиску файла решения (SLN) и добавляет имя сборки TEntryPoint в каталог решения. Корневой каталог приложения (корневой путь к содержимому) используется для обнаружения представлений и файлов содержимого.

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

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

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

{
  "shadowCopy": false
}

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

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

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

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

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

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

dotnet test

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

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

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

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

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

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

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

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

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