ASP.NET Core のミドルウェアのテスト

作成者: Chris Ross

ミドルウェアは、TestServer と切り離してテストできます。 これにより次の操作を行うことができます。

  • テストするコンポーネントのみを含むアプリ パイプラインをインスタンス化します。
  • カスタム要求を送信してミドルウェアの動作を検証します。

利点:

  • 要求はネットワークを介してシリアル化されるのではなく、メモリ内で送信されます。
  • これにより、ポート管理や HTTPS 証明書などの追加の問題が回避されます。
  • ミドルウェア内の例外は、呼び出し元のテスト フローに直接戻すことができます。
  • HttpContext などのサーバーのデータ構造をテストで直接カスタマイズすることができます。

TestServer の設定

テスト プロジェクトで、テストを作成します。

  • TestServer を使用するホストをビルドして起動します。

  • ミドルウェアが使用する必要なサービスを追加します。

  • Microsoft.AspNetCore.TestHost NuGet パッケージのパッケージ参照をプロジェクトに追加します。

  • テストにミドルウェアを使用するように処理パイプラインを構成します。

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

Note

.NET アプリへのパッケージの追加に関するガイダンスについては、「パッケージ利用のワークフロー」 (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 は、HttpClient の抽象化を使用するのではなく、HttpContext オブジェクトの直接構成を許可します。 SendAsync を使用すると、HttpContext.ItemsHttpContext.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 によって通常スローされない例外がスローされる場合があります。
  • 通常、ミドルウェアに関連しないため、既定ではトランスポート固有のヘッダーの一部は設定しません。 詳細については、次のセクションを参照してください。
  • StreamContent を介して渡された Stream 位置を無視します。 HttpClient は、位置が設定されている場合でも、ストリーム全体を開始位置から送信します。 詳細については、次を参照してください。この GitHub の問題します。

Content-Length ヘッダーと Transfer-Encoding ヘッダー

TestServer では、Content-LengthTransfer-Encoding などのトランスポート関連の要求ヘッダーまたは応答ヘッダーは設定 "されません"。 アプリケーションの使用方法はクライアント、シナリオ、プロトコルによって異なるため、これらのヘッダーに依存しないようにする必要があります。 特定のシナリオをテストするために Content-Length および Transfer-Encoding が必要な場合は、HttpRequestMessage または HttpContext を作成するときにテストで指定することができます。 詳しくは、次の GitHub の問題をご覧ください。