Тестирование ПО промежуточного слоя ASP.NET Core

Автор: Крис Росс (Chris Ross)

ПО промежуточного слоя можно протестировать изолированно с TestServer. Оно предоставляет следующие возможности.

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

Преимущества.

  • Запросы отправляются в памяти, а не сериализуются по сети.
  • Это позволяет избежать дополнительных проблем, таких как управление портами и сертификаты HTTPS.
  • Исключения в ПО промежуточного слоя могут напрямую возвращаться к вызывающему тесту.
  • Можно настроить структуры данных сервера, например HttpContext, непосредственно в тесте.

Настройка TestServer

В тестовом проекте создайте тест.

  • Создайте и запустите узел, который использует TestServer.

  • Добавьте все необходимые службы, используемые ПО промежуточного слоя.

  • Добавьте в проект ссылку на пакет NuGet Microsoft.AspNetCore.TestHost.

  • Настройте конвейер обработки для использования ПО промежуточного слоя для теста.

    [Fact]
    public async Task MiddlewareTest_ReturnsNotFoundForRequest()
    {
        using var host = await new HostBuilder()
            .ConfigureWebHost(webBuilder =>
            {
                webBuilder
                    .UseTestServer()
                    .ConfigureServices(services =>
                    {
                        services.AddMyServices();
                    })
                    .Configure(app =>
                    {
                        app.UseMiddleware<MyMiddleware>();
                    });
            })
            .StartAsync();
    
        ...
    }
    

Примечание.

Рекомендации по добавлению пакетов в приложения .NET см. в разделе Способы установки пакетов NuGet в статье Рабочий процесс использования пакета (документация по NuGet). Проверьте правильность версий пакета на сайте NuGet.org.

Отправка запросов с помощью HttpClient

Отправьте запрос с помощью HttpClient.

[Fact]
public async Task MiddlewareTest_ReturnsNotFoundForRequest()
{
    using var host = await new HostBuilder()
        .ConfigureWebHost(webBuilder =>
        {
            webBuilder
                .UseTestServer()
                .ConfigureServices(services =>
                {
                    services.AddMyServices();
                })
                .Configure(app =>
                {
                    app.UseMiddleware<MyMiddleware>();
                });
        })
        .StartAsync();

    var response = await host.GetTestClient().GetAsync("/");

    ...
}

Утвердите результат. Сначала следует сделать утверждение, противоположное ожидаемому результату. Первое выполнение с ложноположительным утверждением подтверждает, что тест завершается ошибкой, если ПО промежуточного слоя выполняется правильно. Запустите тест и убедитесь, что тест не пройден.

В следующем примере ПО промежуточного слоя должно вернуть код состояния 404 (Не найдено) при запросе корневой конечной точки. Выполните первый тестовый запуск с Assert.NotEqual( ... );, который должен завершиться ошибкой.

[Fact]
public async Task MiddlewareTest_ReturnsNotFoundForRequest()
{
    using var host = await new HostBuilder()
        .ConfigureWebHost(webBuilder =>
        {
            webBuilder
                .UseTestServer()
                .ConfigureServices(services =>
                {
                    services.AddMyServices();
                })
                .Configure(app =>
                {
                    app.UseMiddleware<MyMiddleware>();
                });
        })
        .StartAsync();

    var response = await host.GetTestClient().GetAsync("/");

    Assert.NotEqual(HttpStatusCode.NotFound, response.StatusCode);
}

Измените утверждение, чтобы проверить ПО промежуточного слоя в нормальных условиях работы. В последнем тесте используется Assert.Equal( ... );. Запустите тест еще раз, чтобы убедиться, что он пройден.

[Fact]
public async Task MiddlewareTest_ReturnsNotFoundForRequest()
{
    using var host = await new HostBuilder()
        .ConfigureWebHost(webBuilder =>
        {
            webBuilder
                .UseTestServer()
                .ConfigureServices(services =>
                {
                    services.AddMyServices();
                })
                .Configure(app =>
                {
                    app.UseMiddleware<MyMiddleware>();
                });
        })
        .StartAsync();

    var response = await host.GetTestClient().GetAsync("/");

    Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

Отправка запросов с помощью HttpContext

Тестовое приложение также может отправить запрос с помощью SendAsync(Action<HttpContext>, CancellationToken). В следующем примере выполняется несколько проверок, когда https://example.com/A/Path/?and=query обрабатывается в ПО промежуточного слоя:

[Fact]
public async Task TestMiddleware_ExpectedResponse()
{
    using var host = await new HostBuilder()
        .ConfigureWebHost(webBuilder =>
        {
            webBuilder
                .UseTestServer()
                .ConfigureServices(services =>
                {
                    services.AddMyServices();
                })
                .Configure(app =>
                {
                    app.UseMiddleware<MyMiddleware>();
                });
        })
        .StartAsync();

    var server = host.GetTestServer();
    server.BaseAddress = new Uri("https://example.com/A/Path/");

    var context = await server.SendAsync(c =>
    {
        c.Request.Method = HttpMethods.Post;
        c.Request.Path = "/and/file.txt";
        c.Request.QueryString = new QueryString("?and=query");
    });

    Assert.True(context.RequestAborted.CanBeCanceled);
    Assert.Equal(HttpProtocol.Http11, context.Request.Protocol);
    Assert.Equal("POST", context.Request.Method);
    Assert.Equal("https", context.Request.Scheme);
    Assert.Equal("example.com", context.Request.Host.Value);
    Assert.Equal("/A/Path", context.Request.PathBase.Value);
    Assert.Equal("/and/file.txt", context.Request.Path.Value);
    Assert.Equal("?and=query", context.Request.QueryString.Value);
    Assert.NotNull(context.Request.Body);
    Assert.NotNull(context.Request.Headers);
    Assert.NotNull(context.Response.Headers);
    Assert.NotNull(context.Response.Body);
    Assert.Equal(404, context.Response.StatusCode);
    Assert.Null(context.Features.Get<IHttpResponseFeature>().ReasonPhrase);
}

SendAsync разрешает прямую настройку объекта HttpContext вместо использования абстракций HttpClient. Используйте SendAsync для управления структурами, доступными только на сервере, например HttpContext.Items или HttpContext.Features.

Как и в предыдущем примере, где ожидалась ошибка 404 — не найдено, проверьте противоположность каждой инструкции Assert в предыдущем тесте. Проверка покажет, что тест действительно завершается ошибкой, если ПО промежуточного слоя работает нормально. Убедившись, что тест дает ложноположительный результат, установите окончательные инструкции Assert для ожидаемых условий и значений теста. Запустите тест еще раз, чтобы убедиться, что он пройден.

Добавление маршрутов запроса

Дополнительные маршруты можно добавить по конфигурации с помощью теста HttpClient:

	[Fact]
	public async Task TestWithEndpoint_ExpectedResponse ()
	{
		using var host = await new HostBuilder()
			.ConfigureWebHost(webBuilder =>
			{
				webBuilder
					.UseTestServer()
					.ConfigureServices(services =>
					{
						services.AddRouting();
					})
					.Configure(app =>
					{
						app.UseRouting();
						app.UseMiddleware<MyMiddleware>();
						app.UseEndpoints(endpoints =>
						{
							endpoints.MapGet("/hello", () =>
								TypedResults.Text("Hello Tests"));
						});
					});
			})
			.StartAsync();

		var client = host.GetTestClient();

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

		Assert.True(response.IsSuccessStatusCode);
		var responseBody = await response.Content.ReadAsStringAsync();
		Assert.Equal("Hello Tests", responseBody);

Дополнительные маршруты также можно добавить с помощью подхода server.SendAsync.

Ограничения TestServer

TestServer:

  • используется для репликации поведения сервера для проверки ПО промежуточного слоя.
  • Не пытайтесь выполнить репликацию всего поведения HttpClient.
  • Пытается предоставить клиенту максимально возможный контроль над сервером и обеспечить максимальную возможность отслеживания того, что происходит на сервере. Например, он может вызывать исключения, которые обычно не вызываются HttpClient, чтобы напрямую передавать состояние сервера.
  • Не задает некоторые связанные с транспортом заголовки по умолчанию, так как они обычно не относятся к ПО промежуточного слоя. Для получения дополнительных сведений см. следующий раздел.
  • Игнорирует позицию Stream, передаваемую через StreamContent. HttpClient отправляет весь поток, начиная с начальной позиции, даже если задано положение. Дополнительные сведения см. здесь на GitHub.

Заголовки Content-Length и Transfer-Encoding

TestServer не задает связанные с транспортировкой запросы или заголовки ответа, такие как Content-Length и Transfer-Encoding. Приложения не должны зависеть от этих заголовков, так как их использование определяется клиентом, сценарием и протоколом. Если Content-Length и Transfer-Encoding требуются для тестирования конкретного сценария, их можно указать в тесте при создании HttpRequestMessage или HttpContext. См. сведения см. в следующих проблемах GitHub: