Интеграционные тесты на платформе 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 требуется следующее:
- Тестовый проект служит для хранения и выполнения тестов. Тестовый проект содержит ссылку на тестируемую систему.
- Тестовый проект создает тестовый веб-хост для ТС и использует клиент тестового сервера для обработки запросов и ответов во взаимодействии с ТС.
- Средство запуска тестов используется для выполнения тестов и отчета о результатах тестов.
Интеграционные тесты придерживаются последовательности событий из обычных шагов теста Подготовка, Выполнение и Проверка.
- Веб-хостинг СНИ настроен.
- Создается клиент тестового сервера для отправки запросов к приложению.
- Выполняется шаг "Упорядочить" — тестовое приложение подготавливает запрос.
- Выполняется шаг теста Act: клиент отправляет запрос и получает ответ.
- Выполняется шаг теста Assert: фактический ответ проверяется как проход или сбой на основе ожидаемого ответа.
- Процесс продолжается до тех пор, пока не будут выполнены все тесты.
- Результаты теста сообщены.
Как правило, тестовый веб-узел настраивается отлично от обычного веб-узла приложения для тестовых запусков. Например, для тестов может использоваться другая база данных или другие параметры приложения.
Компоненты инфраструктуры, такие как тестовый веб-узел и сервер тестирования в памяти (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
для тестового проекта, используя один из следующих методов.
Открыть доступ к внутренним типам веб-приложения тестовому проекту. Это можно сделать в файле проекта SUT (
.csproj
):<ItemGroup> <InternalsVisibleTo Include="MyTestProject" /> </ItemGroup>
Program
Сделайте класс общедоступным с помощью объявления частичного класса:var builder = WebApplication.CreateBuilder(args); // ... Configure services, routes, etc. app.Run(); + public partial class Program { }
В примере приложения используется подход к частичному классу
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> для создания одной или нескольких пользовательских фабрик:
Наследуйте от
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, выполните следующие действия.- Сошлитесь на пакет NuGet
Microsoft.EntityFrameworkCore.SqlServer
в файле проекта. - Вызовите
UseInMemoryDatabase
.
- Сошлитесь на пакет NuGet
Используйте настраиваемый
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
.Обычный тест использует
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 теста, тестовое приложение должно:
- Сделать запрос к странице.
- Проанализировать файл cookie для защиты от подделки и запросить маркер проверки из ответа.
- Выполните запрос 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 для получения конструктора хоста. Это можно увидеть в следующих тестах:
- Get_QuoteService_ProvidesQuoteInPage
- Get_GithubProfilePageCanGetAGithubUser
- Возвращение_Защищенной_Страницы_Для_Аутентифицированного_Пользователя
Пример ТС включает службу с ограниченной областью действия, которая возвращает предложение. Цитата внедряется в скрытое поле на странице индекса при запросе страницы индекса.
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've an appointment in
London, and we'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'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 в сборке с интеграционными тестами, где ключ равен сборке TEntryPoint
System.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 |
Содержит методы теста для:
|
BasicTests |
Содержит метод теста для маршрутизации и типа содержимого. |
IntegrationTests |
Содержит интеграционные тесты для страницы индекса с помощью настраиваемого класса WebApplicationFactory . |
Helpers/Utilities |
|
Используемая платформа тестирования — 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 требуется следующее:
- Тестовый проект используется для хранения и выполнения тестов. Тестовый проект содержит ссылку на ТС.
- Тестовый проект создает тестовый веб-хост для тестируемой системы и использует клиент тестового сервера для обработки запросов и ответов с тестируемой системой.
- Средство запуска тестов используется для выполнения тестов и передачи результатов тестов.
Интеграционные тесты придерживаются последовательности событий из обычных шагов теста Подготовка, Выполнение и Проверка.
- Веб-хостинг ТС настроен.
- Создается клиент тестового сервера для отправки запросов к приложению.
- Выполняется шаг "Упорядочить" — тестовое приложение подготавливает запрос.
- Выполняется шаг теста Act: клиент отправляет запрос и получает ответ.
- Выполняется шаг теста Assert: фактический ответ проверяется как проход или сбой на основе ожидаемого ответа.
- Процесс продолжается до тех пор, пока не будут выполнены все тесты.
- Результаты теста были опубликованы.
Как правило, тестовый веб-узел настраивается отлично от обычного веб-узла приложения для тестовых запусков. Например, для тестов может использоваться другая база данных или другие параметры приложения.
Компоненты инфраструктуры, такие как тестовый веб-узел и сервер тестирования в памяти (TestServer), предоставляются или управляются пакетом Microsoft.AspNetCore.Mvc.Testing. Использование этого пакета упрощает создание и выполнение тестов.
Пакет Microsoft.AspNetCore.Mvc.Testing
выполняет следующие задачи:
- Копирует файл зависимостей (
.deps
) из SUT в каталог тестового проектаbin
. - Задает корневой каталог содержимого в корне проекта ТС, чтобы при выполнении тестов были найдены статические файлы и страницы или представления.
- Предоставляет класс WebApplicationFactory для упрощения начальной загрузки ТС с
TestServer
.
В документации модульные тесты описано, как настроить тестовый проект и средство запуска тестов, а также представлены подробные инструкции по выполнению тестов и рекомендаций по именованию тестов и тестовых классов.
Отделяйте модульные тесты от тестов интеграции в разные проекты. Разделение тестов:
- Помогает убедиться, что компоненты тестирования инфраструктуры не случайно включены в модульные тесты.
- Позволяет контролировать выполнение набора тестов.
В сущности, между конфигурацией для тестов приложений Razor Pages и приложений MVC нет никаких отличий. Единственное отличие заключается в том, как именуются тесты. В приложении Razor Pages тесты конечных точек страницы обычно именуются по классу модели страницы (например, IndexPageTests
для тестирования интеграции компонентов на странице индекса). В приложении MVC тесты обычно упорядочены по классам контроллеров и именуются по контроллеру, который они проверяют (например, HomeControllerTests
для тестирования интеграции компонентов на контроллере Home).
Проверка необходимых требований к приложению
Тестовый проект должен выполнять следующие требования.
- Обратитесь к пакету
Microsoft.AspNetCore.Mvc.Testing
. - Указывать веб-пакет SDK в файле проекта (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
Выполнение необходимых требований можно посмотреть в примере приложения. Изучите файл tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
. В примере приложения используются платформа тестирования xUnit и библиотека средства синтаксического анализа AngleSharp, поэтому он также ссылается на:
В приложениях, которые используют xunit.runner.visualstudio
версии 2.4.2 или более поздней версии, тестовый проект должен ссылаться на пакет Microsoft.NET.Test.Sdk
.
В тестах также используется Entity Framework Core. Ссылки из приложения:
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Microsoft.AspNetCore.Identity.EntityFrameworkCore
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.InMemory
Microsoft.EntityFrameworkCore.Tools
Среда ТС
Если среда ТС не задана, то по умолчанию среда имеет значение 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 как важный, см. в разделе «Основные куки-файлы».
Настройка WebApplicationFactory
Конфигурацию веб-узла можно создать независимо от тестовых классов путем наследования от WebApplicationFactory
для создания одной или нескольких пользовательских фабрик:
Наследуйте от
WebApplicationFactory
и переопределите ConfigureWebHost. IWebHostBuilder позволяет настраивать коллекцию служб с помощью ConfigureServices:public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup: class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var descriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>)); services.Remove(descriptor); services.AddDbContext<ApplicationDbContext>(options => { options.UseInMemoryDatabase("InMemoryDbForTesting"); }); var sp = services.BuildServiceProvider(); using (var scope = sp.CreateScope()) { var scopedServices = scope.ServiceProvider; var db = scopedServices.GetRequiredService<ApplicationDbContext>(); var logger = scopedServices .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>(); db.Database.EnsureCreated(); try { Utilities.InitializeDbForTests(db); } catch (Exception ex) { logger.LogError(ex, "An error occurred seeding the " + "database with test messages. Error: {Message}", ex.Message); } } }); } }
Заполнение базы данных в примере приложения выполняется методом
InitializeDbForTests
. Метод описан в примере тестов интеграции: раздел "Тестирование организации приложения".Контекст базы данных ТС регистрируется в методе
Startup.ConfigureServices
. Обратный вызовbuilder.ConfigureServices
тестового приложения выполняется после выполнения кодаStartup.ConfigureServices
приложения. Порядок выполнения является критическим изменением для универсального узла с выпуском ASP.NET Core 3.0. Чтобы использовать для тестов базу данных, отличную от базы данных приложения, необходимо заменить контекст базы данных приложения вbuilder.ConfigureServices
.В тестируемых системах, которые по-прежнему используют веб-хостинг, обратный вызов
builder.ConfigureServices
тестового приложения выполняется до выполнения кодаStartup.ConfigureServices
тестируемой системы. Обратный вызовbuilder.ConfigureTestServices
тестового приложения выполняется позже.Пример приложения находит дескриптор службы для контекста базы данных и использует дескриптор для удаления регистрации службы. Затем фабрика добавляет новый
ApplicationDbContext
, который использует базу данных, хранящуюся в оперативной памяти, для тестов.Чтобы подключиться к базе данных, отличной от базы данных в памяти, измените вызов
UseInMemoryDatabase
, чтобы подключить контекст к другой базе данных. Чтобы использовать тестовую базу данных SQL Server, выполните следующие действия.- Добавьте ссылку на пакет NuGet
Microsoft.EntityFrameworkCore.SqlServer
в файле проекта. - Вызовите
UseSqlServer
со строкой подключения к базе данных.
services.AddDbContext<ApplicationDbContext>((options, context) => { context.UseSqlServer( Configuration.GetConnectionString("TestingDbConnectionString")); });
- Добавьте ссылку на пакет NuGet
Используйте настраиваемый
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
.Обычный тест использует
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 теста, тестовое приложение должно:
- Выполнить запрос к странице.
- Проанализировать файл cookie для защиты от подделки и запросить маркер проверки из ответа.
- Выполните запрос POST с файлом cookie для защиты от подделки и запросом маркера проверки на месте.
Вспомогательные методы расширения SendAsync
(Helpers/HttpClientExtensions.cs
) и вспомогательный метод GetDocumentAsync
(Helpers/HtmlHelpers.cs
) в примере приложения используют средство синтаксического анализа AngleSharp для обработки защиты от подделки с помощью следующих методов:
-
GetDocumentAsync
: получает HttpResponseMessage и возвращаетIHtmlDocument
.GetDocumentAsync
использует фабрику, которая подготавливает виртуальный ответ на основе исходногоHttpResponseMessage
. Дополнительные сведения см. в документации по AngleSharp. - Методы расширения
SendAsync
дляHttpClient
составляют HttpRequestMessage и вызывают SendAsync(HttpRequestMessage) для отправки запросов к ТС. Перегрузки дляSendAsync
принимают HTML-форму (IHtmlFormElement
) и следующие элементы:- Кнопка "Отправить" в форме (
IHtmlElement
) - Коллекция значений формы (
IEnumerable<KeyValuePair<string, string>>
) - Кнопка "Отправить" (
IHtmlElement
) и значения формы (IEnumerable<KeyValuePair<string, string>>
)
- Кнопка "Отправить" в форме (
Примечание.
AngleSharp — это сторонняя библиотека анализа, используемая для демонстрационных целей в этом разделе и в примере приложения. AngleSharp не поддерживается и не требуется для интеграционного тестирования приложений ASP.NET Core. Можно использовать и другие средства синтаксического анализа, такие как Html Agility Pack (HAP). Другой подход заключается в написании кода для непосредственной работы с маркером проверки запроса системы защиты от подделки и файлом cookie защиты от подделки.
Примечание.
Поставщик базы данных EF-Core для работы в памяти может использоваться для несложного и базового тестирования, однако для тестирования в памяти рекомендуется поставщик SQLite.
Настройка клиента с помощью WithWebHostBuilder
Если в методе теста требуется дополнительная настройка, WithWebHostBuilder создает новый объект WebApplicationFactory
с IWebHostBuilder, который дополнительно настраивается в конфигурации.
Метод теста Post_DeleteMessageHandler_ReturnsRedirectToRoot
в примере приложения демонстрирует использование WithWebHostBuilder
. Этот тест выполняет удаление записи из базы данных путем инициирования отправки формы в системе, находящейся под тестированием.
Поскольку другой тест в классе IndexPageTests
выполняет операцию, которая удаляет все записи из базы данных и может выполняться до метода Post_DeleteMessageHandler_ReturnsRedirectToRoot
, база данных повторно заполняется в этом методе теста, чтобы обеспечить наличие записи для ее удаления ТС. Выбор первой кнопки удаления в форме messages
в тестируемой системе имитируется в запросе к тестируемой системе:
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var serviceProvider = services.BuildServiceProvider();
using (var scope = serviceProvider.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices
.GetRequiredService<ApplicationDbContext>();
var logger = scopedServices
.GetRequiredService<ILogger<IndexPageTests>>();
try
{
Utilities.ReinitializeDbForTests(db);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred seeding " +
"the database with test messages. Error: {Message}",
ex.Message);
}
}
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Параметры клиента
В следующей таблице показаны доступные по умолчанию WebApplicationFactoryClientOptions при создании экземпляров HttpClient
.
Вариант | Описание | По умолчанию |
---|---|---|
AllowAutoRedirect | Возвращает или задает, должны ли экземпляры HttpClient автоматически следовать ответам перенаправления. |
true |
BaseAddress | Возвращает или задает базовый адрес экземпляров HttpClient . |
http://localhost |
HandleCookies | Возвращает или задает, должны ли экземпляры HttpClient обрабатывать файлы cookie. |
true |
MaxAutomaticRedirections | Возвращает или задает максимальное число ответов на перенаправление, которым должны следовать экземпляры HttpClient . |
7 |
Создайте класс WebApplicationFactoryClientOptions
и передайте его в метод CreateClient() (значения по умолчанию показаны в примере кода):
// Default client option values are shown
var clientOptions = new WebApplicationFactoryClientOptions();
clientOptions.AllowAutoRedirect = true;
clientOptions.BaseAddress = new Uri("http://localhost");
clientOptions.HandleCookies = true;
clientOptions.MaxAutomaticRedirections = 7;
_client = _factory.CreateClient(clientOptions);
Внедрить тестовые службы
Службы можно переопределить в тесте с помощью вызова ConfigureTestServices в конструкторе хоста.
Чтобы внедрить службы имитации, в ТС должен иметься класс Startup
с методом Startup.ConfigureServices
.
Пример тестируемой системы включает в себя контекстуальную службу, которая возвращает котировку. Цитата внедряется в скрытое поле на странице индекса при запросе страницы индекса.
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Startup.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
При запуске приложения SUT создается следующая разметка:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we'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'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);
}
Если запретить клиенту следовать перенаправлению, можно выполнить следующие проверки:
- Код состояния, возвращаемый Системой Под Тестом (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 в сборке, содержащей интеграционные тесты, с ключом, совпадающим со сборкой TEntryPoint
System.Reflection.Assembly.FullName
. Если атрибут с правильным ключом не найден, WebApplicationFactory
возвращается к поиску файла решения (SLN) и добавляет имя сборки TEntryPoint
в каталог решения. Корневой каталог приложения (корневой путь к содержимому) используется для обнаружения представлений и файлов содержимого.
Отключение теневого копирования
Теневое копирование приводит к тому, что тесты выполняются в каталоге, отличном от выходного каталога. Если тесты зависят от загрузки файлов относительно Assembly.Location
и возникают проблемы, может потребоваться отключение теневого копирования.
Чтобы отключить теневое копирование при использовании xUnit, создайте файл xunit.runner.json
в каталоге тестового проекта с правильными настройками конфигурации:
{
"shadowCopy": false
}
Удаление объектов
После выполнения тестов реализации IClassFixture
, TestServer и HttpClient удаляются в то время, как xUnit удаляет WebApplicationFactory
. Если объекты, создаваемые разработчиком, требуется удалить, удалите их в реализации IClassFixture
. Дополнительные сведения см. в разделе Реализация метода Dispose.
Пример интеграционных тестов
Пример приложения состоит из двух приложений:
Приложение | Каталог проекта | Описание |
---|---|---|
Приложение для сообщений (SUT) | src/RazorPagesProject |
Позволяет пользователю добавлять, удалять сообщения (по одному или все) и анализировать их. |
Тестирование приложения. | tests/RazorPagesProject.Tests |
Используется для тестирования интеграции ТС. |
Тесты можно выполнять с помощью встроенных функций тестирования интегрированной среды разработки, таких как Visual Studio. В Visual Studio Code или в командной строке выполните следующую команду в каталоге tests/RazorPagesProject.Tests
:
dotnet test
Организация приложения для сообщений (ТС)
Тестируемая система (ТС) — это система сообщений Razor Pages со следующими характеристиками:
- Страница индекса приложения (
Pages/Index.cshtml
иPages/Index.cshtml.cs
) предоставляет методы модели пользовательского интерфейса и страницы для управления добавлением, удалением и анализом сообщений (средние слова для каждого сообщения). - Сообщение описывается классом (
Message
) с двумя свойствамиData/Message.cs
:Id
(ключ) иText
(сообщение). СвойствоText
является обязательным и ограничено 200 символами. - Сообщения хранятся с помощью базы данных Entity Framework в памяти†.
- Приложение содержит уровень доступа к данным (DAL) в классе
AppDbContext
контекста базы данных (Data/AppDbContext.cs
). - Если база данных пуста при запуске приложения, то хранилище сообщений инициализируется тремя сообщениями.
- Приложение включает
/SecurePage
, доступ к которому может получить только пользователь, прошедший проверку подлинности.
†В разделе документации о EF Тестирование с помощью InMemory объясняется, как использовать базу данных в памяти для тестов с помощью MSTest. В этом разделе используется платформа тестирования xUnit. Концепции тестирования и реализации тестов в разных платформах тестирования похожи, но не идентичны.
Несмотря на то, что приложение не использует шаблон репозитория и не является эффективным примером шаблона "единица работы" (UoW), Razor Pages поддерживает такие шаблоны разработки. Дополнительные сведения см. в разделах Проектирование уровня сохраняемости инфраструктуры и Логика контроллера тестирования (пример реализует шаблон репозитория).
Организация приложения для тестирования
Тестовое приложение — это консольное приложение в папке tests/RazorPagesProject.Tests
.
Каталог тестового приложения | Описание |
---|---|
AuthTests |
Содержит методы теста для:
|
BasicTests |
Содержит метод теста для маршрутизации и типа содержимого. |
IntegrationTests |
Содержит интеграционные тесты для страницы индекса с помощью настраиваемого класса WebApplicationFactory . |
Helpers/Utilities |
|
Используемая платформа тестирования — 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 требуется следующее:
- Тестовый проект используется для хранения и выполнения тестов. Тестовый проект содержит ссылку на тестируемую систему (ТС).
- Тестовый проект создает тестовый веб-хост для ТС и использует клиент тестового сервера для обработки запросов и ответов от ТС.
- Средство запуска тестов используется для выполнения тестов и отчета результатов.
Интеграционные тесты придерживаются последовательности событий из обычных шагов теста Подготовка, Выполнение и Проверка.
- Веб-хост ТС настроен.
- Создается клиент тестового сервера для отправки запросов к приложению.
- Выполняется шаг "Упорядочить" — тестовое приложение подготавливает запрос.
- Выполняется шаг теста Act: клиент отправляет запрос и получает ответ.
- Выполняется шаг теста Assert: фактический ответ проверяется как проход или сбой на основе ожидаемого ответа.
- Процесс продолжается до тех пор, пока не будут выполнены все тесты.
- Результаты теста представлены.
Как правило, тестовый веб-узел настраивается отлично от обычного веб-узла приложения для тестовых запусков. Например, для тестов может использоваться другая база данных или другие параметры приложения.
Компоненты инфраструктуры, такие как тестовый веб-узел и сервер тестирования в памяти (TestServer), предоставляются или управляются пакетом Microsoft.AspNetCore.Mvc.Testing. Использование этого пакета упрощает создание и выполнение тестов.
Пакет Microsoft.AspNetCore.Mvc.Testing
выполняет следующие задачи:
- Копирует файл зависимостей (
.deps
) из SUT в каталог тестового проектаbin
. - Задает корневой каталог содержимого в корне проекта ТС, чтобы при выполнении тестов были найдены статические файлы и страницы или представления.
- Предоставляет класс WebApplicationFactory для упрощения начальной загрузки ТС с
TestServer
.
В документации модульные тесты описано, как настроить тестовый проект и средство запуска тестов, а также представлены подробные инструкции по выполнению тестов и рекомендаций по именованию тестов и тестовых классов.
Отделяйте модульные тесты от тестов интеграции в разные проекты. Разделение тестов:
- Помогает убедиться, что компоненты тестирования инфраструктуры не случайно включены в модульные тесты.
- Позволяет контролировать выполнение набора тестов.
В сущности, между конфигурацией для тестов приложений Razor Pages и приложений MVC нет никаких отличий. Единственное отличие заключается в том, как именуются тесты. В приложении Razor Pages тесты конечных точек страницы обычно именуются по классу модели страницы (например, IndexPageTests
для тестирования интеграции компонентов на странице индекса). В приложении MVC тесты обычно упорядочены по классам контроллеров и именуются по контроллеру, который они проверяют (например, HomeControllerTests
для тестирования интеграции компонентов на контроллере Home).
Проверка необходимых требований к приложению
Тестовый проект должен выполнять следующие требования.
- Обратитесь к пакету
Microsoft.AspNetCore.Mvc.Testing
. - Указывать веб-пакет SDK в файле проекта (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
Выполнение необходимых требований можно посмотреть в примере приложения. Изучите файл tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
. В примере приложения используются платформа тестирования xUnit и библиотека средства синтаксического анализа AngleSharp, поэтому он также ссылается на:
В приложениях, которые используют xunit.runner.visualstudio
версии 2.4.2 или более поздней версии, тестовый проект должен ссылаться на пакет Microsoft.NET.Test.Sdk
.
В тестах также используется Entity Framework Core. См. файл проекта в GitHub.
Среда системы тестирования
Если среда тестовой системы не настроена, то по умолчанию используется среда "Development".
Базовые тесты со стандартной WebApplicationFactory
Запрограммируйте доступ к неявно определённому классу Program
для тестового проекта, выполнив одно из следующих действий:
Сделать внутренние типы веб-приложения доступными для тестового проекта. Это можно сделать в файле проекта SUT (
.csproj
):<ItemGroup> <InternalsVisibleTo Include="MyTestProject" /> </ItemGroup>
Program
Сделайте класс общедоступным с помощью объявления частичного класса:var builder = WebApplication.CreateBuilder(args); // ... Configure services, routes, etc. app.Run(); + public partial class Program { }
В примере приложения используется подход к частичному классу
Program
.
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> для создания одной или нескольких настраиваемых фабрик.
Наследуйте от
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, выполните следующие действия.
- Добавьте ссылку на пакет NuGet
Microsoft.EntityFrameworkCore.SqlServer
в файле проекта. - Вызовите
UseInMemoryDatabase
.
Используйте настраиваемый
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
.Обычный тест использует
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 теста, тестовое приложение должно:
- Сделайте запрос к странице.
- Проанализировать файл cookie для защиты от подделки и запросить маркер проверки из ответа.
- Выполните запрос POST с файлом cookie для защиты от подделки и запросом маркера проверки на месте.
Вспомогательные методы расширения SendAsync
(Helpers/HttpClientExtensions.cs
) и вспомогательный метод GetDocumentAsync
(Helpers/HtmlHelpers.cs
) в примере приложения используют средство синтаксического анализа AngleSharp для обработки защиты от подделки с помощью следующих методов:
-
GetDocumentAsync
: получает HttpResponseMessage и возвращаетIHtmlDocument
.GetDocumentAsync
использует фабрику, которая подготавливает виртуальный ответ на основе исходногоHttpResponseMessage
. Дополнительные сведения см. в документации по AngleSharp. - Методы расширения
SendAsync
дляHttpClient
формируют HttpRequestMessage и вызывают SendAsync(HttpRequestMessage) для подачи запросов в SUT. Перегрузки дляSendAsync
принимают HTML-форму (IHtmlFormElement
) и следующие:- Кнопка "Отправить" в форме (
IHtmlElement
) - Коллекция значений формы (
IEnumerable<KeyValuePair<string, string>>
) - Кнопка "Отправить" (
IHtmlElement
) и значения формы (IEnumerable<KeyValuePair<string, string>>
)
- Кнопка "Отправить" в форме (
AngleSharp — это сторонняя библиотека синтаксического анализа, используемая для демонстрационных целей в этой статье и пример приложения. AngleSharp не поддерживается и не требуется для интеграционного тестирования приложений ASP.NET Core. Можно использовать и другие средства синтаксического анализа, такие как Html Agility Pack (HAP). Другой подход заключается в написании кода для непосредственной работы с токеном проверки запроса антифальсификационной системы и токеном cookie антифальсификации. Дополнительные сведения см. в этой статье в разделе AngleSharp против Application Parts
проверки антифальсификационных средств.
Поставщик базы данных EF-Core в памяти может быть использован для ограниченного и базового тестирования, однако поставщик SQLite рекомендуется для тестирования в памяти.
См. раздел "Расширение запуска с помощью фильтров запуска", в котором показано, как настроить промежуточное программное обеспечение с использованием IStartupFilter, что полезно, если тест требует пользовательской службы или промежуточного ПО.
Настройка клиента с помощью WithWebHostBuilder
Если в методе теста требуется дополнительная настройка, WithWebHostBuilder создает новый объект WebApplicationFactory
с IWebHostBuilder, который дополнительно настраивается в конфигурации.
Пример кода вызывает WithWebHostBuilder
, чтобы заменить настроенные службы тестовыми заглушками. Дополнительные сведения и примеры использования см. в статье "Внедрение макетов служб ".
Метод теста Post_DeleteMessageHandler_ReturnsRedirectToRoot
в примере приложения демонстрирует использование WithWebHostBuilder
. Этот тест выполняет удаление записи из базы данных, инициируя отправку формы в тестируемой системе.
Поскольку другой тест в классе IndexPageTests
выполняет операцию, удаляющую все записи из базы данных и может выполняться до метода Post_DeleteMessageHandler_ReturnsRedirectToRoot
, база данных заново инициализируется в этом методе тестирования, чтобы гарантировать наличие записи для удаления системой, находящейся под тестированием (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")
Внедрение Mock-сервисов
Службы можно переопределить в тесте с помощью вызова ConfigureTestServices в построителе узлов. Для ограничения использования переопределенных служб в самом тесте метод WithWebHostBuilder используется для получения построителя хоста. Это можно увидеть в следующих тестах:
- Get_QuoteService_ProvidesQuoteInPage
- Get_GithubProfilePageCanGetAGithubUser
- Получить_ЗащищеннуюСтраницуДляАутентифицированногоПользователя
Пример системы под тестированием (СУТ) включает ограниченную службу, которая возвращает высказывание. Цитата внедряется в скрытое поле на странице индекса при запросе страницы индекса.
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've an appointment in
London, and we'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'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 в сборке, содержащей интеграционные тесты, где ключ равен TEntryPoint
сборке System.Reflection.Assembly.FullName
. Если атрибут с правильным ключом не найден, WebApplicationFactory
возвращается к поиску файла решения (SLN) и добавляет имя сборки TEntryPoint
в каталог решения. Корневой каталог приложения (корневой путь к содержимому) используется для обнаружения представлений и файлов содержимого.
Отключение теневого копирования
Теневое копирование приводит к тому, что тесты выполняются в каталоге, отличном от выходного каталога. Если тесты зависят от загрузки файлов относительно Assembly.Location
и возникают проблемы, может потребоваться отключение теневого копирования.
Чтобы отключить теневое копирование при использовании xUnit, создайте файл xunit.runner.json
в каталоге тестового проекта с правильными настройками конфигурации:
{
"shadowCopy": false
}
Удаление объектов
После выполнения тестов реализации IClassFixture
, TestServer и HttpClient удаляются, когда xUnit удаляет WebApplicationFactory
. Если объекты, создаваемые разработчиком, требуется удалить, удалите их в реализации IClassFixture
. Для получения дополнительной информации см. Реализацию метода Dispose.
Пример интеграционных тестов
Пример приложения состоит из двух приложений:
Приложение | Каталог проекта | Описание |
---|---|---|
Программа для обмена сообщениями (SUT) | src/RazorPagesProject |
Позволяет пользователю добавлять, удалять сообщения (по одному или все) и анализировать их. |
Тестирование приложения. | tests/RazorPagesProject.Tests |
Используется для тестирования интеграции ИСП. |
Тесты можно выполнять с помощью встроенных функций тестирования интегрированной среды разработки, таких как Visual Studio. В Visual Studio Code или в командной строке выполните следующую команду в каталоге tests/RazorPagesProject.Tests
:
dotnet test
Организация приложений для обмена сообщениями (ТС)
Тестируемая система (ТС) — это система сообщений Razor Pages со следующими характеристиками:
- Страница индекса приложения (
Pages/Index.cshtml
иPages/Index.cshtml.cs
) предоставляет методы модели пользовательского интерфейса и страницы для управления добавлением, удалением и анализом сообщений (средние слова для каждого сообщения). - Сообщение описывается классом (
Message
) с двумя свойствамиData/Message.cs
:Id
(ключ) иText
(сообщение). СвойствоText
является обязательным и ограничено 200 символами. - Сообщения хранятся с помощью базы данных Entity Framework в памяти†.
- Приложение содержит уровень доступа к данным (DAL) в классе
AppDbContext
контекста базы данных (Data/AppDbContext.cs
). - Если база данных пуста при запуске приложения, то хранилище сообщений инициализируется тремя сообщениями.
- Приложение включает
/SecurePage
, доступ к которому может получить только пользователь, прошедший проверку подлинности.
† Статья EF( Test with InMemory) объясняет, как использовать базу данных в памяти для тестов с помощью MSTest. В этом разделе используется платформа тестирования xUnit. Концепции тестирования и реализации тестов в разных платформах тестирования похожи, но не идентичны.
Несмотря на то, что приложение не использует шаблон репозитория и не является эффективным примером шаблона "единица работы" (UoW), Razor Pages поддерживает такие шаблоны разработки. Дополнительные сведения см. в разделах Проектирование уровня сохраняемости инфраструктуры и Логика контроллера тестирования (пример реализует шаблон репозитория).
Организация приложения для тестирования
Тестовое приложение — это консольное приложение в папке tests/RazorPagesProject.Tests
.
Каталог тестового приложения | Описание |
---|---|
AuthTests |
Содержит методы теста для:
|
BasicTests |
Содержит метод теста для маршрутизации и типа содержимого. |
IntegrationTests |
Содержит интеграционные тесты для страницы индекса с помощью настраиваемого класса WebApplicationFactory . |
Helpers/Utilities |
|
Используемая платформа тестирования — 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 требуется следующее:
- Тестовый проект используется для хранения и выполнения тестов. Тестовый проект содержит ссылку на SUT.
- Тестовый проект создает тестовый веб-хост для ТС и использует клиент тестового сервера для обработки запросов и ответов, взаимодействуя с ТС.
- Средство запуска тестов используется для выполнения тестов и создания отчета о результатах тестов.
Интеграционные тесты придерживаются последовательности событий из обычных шагов теста Подготовка, Выполнение и Проверка.
- Веб-хостинг ТС настроен.
- Создается клиент тестового сервера для отправки запросов к приложению.
- Выполняется шаг "Упорядочить" — тестовое приложение подготавливает запрос.
- Выполняется шаг теста Act: клиент отправляет запрос и получает ответ.
- Выполняется шаг теста Assert: фактический ответ проверяется как проход или сбой на основе ожидаемого ответа.
- Процесс продолжается до тех пор, пока не будут выполнены все тесты.
- Результаты теста сообщаются.
Как правило, тестовый веб-узел настраивается отлично от обычного веб-узла приложения для тестовых запусков. Например, для тестов может использоваться другая база данных или другие параметры приложения.
Компоненты инфраструктуры, такие как тестовый веб-узел и сервер тестирования в памяти (TestServer), предоставляются или управляются пакетом Microsoft.AspNetCore.Mvc.Testing. Использование этого пакета упрощает создание и выполнение тестов.
Пакет Microsoft.AspNetCore.Mvc.Testing
выполняет следующие задачи:
- Копирует файл зависимостей (
.deps
) из SUT в каталог тестового проектаbin
. - Задает корневой каталог содержимого в корне проекта ТС, чтобы при выполнении тестов были найдены статические файлы и страницы или представления.
- Предоставляет класс WebApplicationFactory для упрощения начальной загрузки ТС с
TestServer
.
В документации модульные тесты описано, как настроить тестовый проект и средство запуска тестов, а также представлены подробные инструкции по выполнению тестов и рекомендаций по именованию тестов и тестовых классов.
Отделяйте модульные тесты от тестов интеграции в разные проекты. Разделение тестов:
- Помогает убедиться, что компоненты тестирования инфраструктуры не случайно включены в модульные тесты.
- Позволяет контролировать выполнение набора тестов.
В сущности, между конфигурацией для тестов приложений Razor Pages и приложений MVC нет никаких отличий. Единственное отличие заключается в том, как именуются тесты. В приложении Razor Pages тесты конечных точек страницы обычно именуются по классу модели страницы (например, IndexPageTests
для тестирования интеграции компонентов на странице индекса). В приложении MVC тесты обычно упорядочены по классам контроллеров и именуются по контроллеру, который они проверяют (например, HomeControllerTests
для тестирования интеграции компонентов на контроллере Home).
Проверка необходимых требований к приложению
Тестовый проект должен выполнять следующие требования.
- Обратитесь к пакету
Microsoft.AspNetCore.Mvc.Testing
. - Указывать веб-пакет SDK в файле проекта (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
Выполнение необходимых требований можно посмотреть в примере приложения. Изучите файл tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
. В примере приложения используются платформа тестирования xUnit и библиотека средства синтаксического анализа AngleSharp, поэтому он также ссылается на:
В приложениях, которые используют xunit.runner.visualstudio
версии 2.4.2 или более поздней версии, тестовый проект должен ссылаться на пакет Microsoft.NET.Test.Sdk
.
В тестах также используется Entity Framework Core. См. файл проекта в GitHub.
Среда тестируемой системы
Если среда ТС не задана, то по умолчанию используется среда "Разработка".
Базовые тесты со стандартной WebApplicationFactory
Откройте неявно определенный Program
класс для тестового проекта, выполнив одно из следующих действий:
Раскрыть внутренние типы веб-приложения для тестового проекта. Это можно сделать в файле проекта SUT (
.csproj
):<ItemGroup> <InternalsVisibleTo Include="MyTestProject" /> </ItemGroup>
Program
Сделайте класс общедоступным с помощью объявления частичного класса:var builder = WebApplication.CreateBuilder(args); // ... Configure services, routes, etc. app.Run(); + public partial class Program { }
В примере приложения используется подход к частичному классу
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
, который автоматически следует перенаправлениям и обрабатывает куки-файлы.
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> и создавая одну или несколько пользовательских фабрик.
Наследуйте от
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, выполните следующие действия.- Добавьте пакет NuGet
Microsoft.EntityFrameworkCore.SqlServer
в файл проекта. - Вызовите
UseInMemoryDatabase
.
- Добавьте пакет NuGet
Используйте настраиваемый
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
.Обычный тест использует
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 теста, тестовое приложение должно:
- Сделать запрос к странице.
- Проанализировать файл cookie для защиты от подделки и запросить маркер проверки из ответа.
- Выполните запрос POST с маркером защиты от подделки cookie и маркером проверки запроса на месте.
Вспомогательные методы расширения SendAsync
(Helpers/HttpClientExtensions.cs
) и вспомогательный метод GetDocumentAsync
(Helpers/HtmlHelpers.cs
) в примере приложения используют средство синтаксического анализа AngleSharp для обработки защиты от подделки с помощью следующих методов:
-
GetDocumentAsync
: получает HttpResponseMessage и возвращаетIHtmlDocument
.GetDocumentAsync
использует фабрику, которая подготавливает виртуальный ответ на основе исходногоHttpResponseMessage
. Дополнительные сведения см. в документации по AngleSharp. - Методы расширения
SendAsync
дляHttpClient
формируют HttpRequestMessage и вызывают SendAsync(HttpRequestMessage) для отправки запросов к ТС. Перегрузки дляSendAsync
принимают HTML-форму (IHtmlFormElement
) и следующие элементы:- Кнопка "Отправить" в форме (
IHtmlElement
) - Коллекция значений формы (
IEnumerable<KeyValuePair<string, string>>
) - Кнопка "Отправить" (
IHtmlElement
) и значения формы (IEnumerable<KeyValuePair<string, string>>
)
- Кнопка "Отправить" в форме (
AngleSharp — это сторонняя библиотека синтаксического анализа, используемая для демонстрационных целей в этой статье и пример приложения. AngleSharp не поддерживается и не требуется для интеграционного тестирования приложений ASP.NET Core. Можно использовать и другие средства синтаксического анализа, такие как Html Agility Pack (HAP). Другой подход заключается в написании кода для непосредственной работы с маркером проверки запроса системы защиты от подделки и файлом cookie защиты от подделки. Дополнительные сведения см. в статье AngleSharp против Application Parts
проверки подлинности.
Поставщик базы данных EF-Core в памяти может использоваться для ограниченного и базового тестирования, однако для тестирования в памяти рекомендуется использовать поставщик SQLite.
См. раздел "Расширение запуска с помощью фильтров запуска", в котором показано, как настроить промежуточное программное обеспечение с помощью IStartupFilter, что полезно, если тест требует пользовательской службы или промежуточного программного обеспечения.
Настройка клиента с помощью WithWebHostBuilder
Если в методе теста требуется дополнительная настройка, WithWebHostBuilder создает новый объект WebApplicationFactory
с IWebHostBuilder, который дополнительно настраивается в конфигурации.
Пример кода используется для вызова WithWebHostBuilder
для замены настроенных служб тестовыми заглушками. Дополнительные сведения и примеры использования см. в статье "Внедрение макетов служб ".
Метод теста Post_DeleteMessageHandler_ReturnsRedirectToRoot
в примере приложения демонстрирует использование WithWebHostBuilder
. Этот тест выполняет удаление записи из базы данных, активируя отправку формы в системе тестирования (ТС).
Поскольку другой тест в классе IndexPageTests
выполняет операцию, которая удаляет все записи из базы данных и может выполняться до метода Post_DeleteMessageHandler_ReturnsRedirectToRoot
, база данных повторно инициализируется в этом методе теста, чтобы обеспечить наличие записи для её удаления системой, находящейся под тестированием. Выбор первой кнопки удаления в форме messages
в ТС имитируется в запросе к ТС:
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Параметры клиента
Обратитесь к странице WebApplicationFactoryClientOptions для значений по умолчанию и доступных параметров при создании экземпляров HttpClient
.
Создайте класс WebApplicationFactoryClientOptions
и передайте его в метод CreateClient():
public class IndexPageTests :
IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Program>
_factory;
public IndexPageTests(
CustomWebApplicationFactory<Program> factory)
{
_factory = factory;
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
}
ПРИМЕЧАНИЕ. Чтобы избежать предупреждений о перенаправлении HTTPS в логах при использовании промежуточного ПО для перенаправления HTTPS, задайте BaseAddress = new Uri("https://localhost")
Вставка служб имитации
Службы можно переопределить в тесте с помощью вызова ConfigureTestServices на этапе построения хоста. Чтобы определить область видимости переопределенных служб для самого теста, используется метод WithWebHostBuilder для получения построителя хоста. Это можно увидеть в следующих тестах:
- Get_QuoteService_ПредоставляетЦитатуНаСтранице
- Get_GithubProfilePageCanGetAGithubUser
- Получить_СтраницаБезопаснаВозвращаетсяДляАутентифицированногоПользователя
Пример ТС включает службу в рамках заданной области, которая возвращает заявку. Цитата внедряется в скрытое поле на странице индекса при запросе страницы индекса.
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Program.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
При запуске приложения ТС создается следующая разметка:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we'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'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 в сборке, содержащей интеграционные тесты с ключом, равным сборке TEntryPoint
System.Reflection.Assembly.FullName
. Если атрибут с правильным ключом не найден, WebApplicationFactory
возвращается к поиску файла решения (SLN) и добавляет имя сборки TEntryPoint
в каталог решения. Корневой каталог приложения (корневой путь к содержимому) используется для обнаружения представлений и файлов содержимого.
Отключение теневого копирования
Теневое копирование приводит к тому, что тесты выполняются в каталоге, отличном от выходного каталога. Если тесты зависят от загрузки файлов относительно Assembly.Location
и возникают проблемы, может потребоваться отключение теневого копирования.
Чтобы отключить теневое копирование при использовании xUnit, создайте файл xunit.runner.json
в каталоге вашего тестового проекта с правильной конфигурацией установки:
{
"shadowCopy": false
}
Удаление объектов
После выполнения тестов реализации IClassFixture
, TestServer и HttpClient удаляются, когда xUnit удаляет WebApplicationFactory
. Если объекты, создаваемые разработчиком, требуется удалить, удалите их в реализации IClassFixture
. Дополнительные сведения см. в разделе Реализация метода Dispose.
Пример интеграционных тестов
Пример приложения состоит из двух приложений:
Приложение | Каталог проекта | Описание |
---|---|---|
Приложение для сообщений (SUT) | src/RazorPagesProject |
Позволяет пользователю добавлять, удалять сообщения (по одному или все) и анализировать их. |
Тестирование приложения. | tests/RazorPagesProject.Tests |
Используется для тестирования интеграции испытуемой системы. |
Тесты можно выполнять с помощью встроенных функций тестирования интегрированной среды разработки, таких как Visual Studio. В Visual Studio Code или в командной строке выполните следующую команду в каталоге tests/RazorPagesProject.Tests
:
dotnet test
Организация мессенджера (SUT)
Тестируемая система (ТС) представляет собой систему сообщений Pages Razor, обладающую следующими характеристиками:
- Страница индекса приложения (
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 |
Содержит методы теста для:
|
BasicTests |
Содержит метод теста для маршрутизации и типа содержимого. |
IntegrationTests |
Содержит интеграционные тесты для страницы индекса с помощью настраиваемого класса WebApplicationFactory . |
Helpers/Utilities |
|
Используемая платформа тестирования — 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.
Дополнительные ресурсы
ASP.NET Core