Tester les middlewares ASP.NET Core

Par Chris Ross

Les intergiciels peuvent être testés isolément avec TestServer. Vous pouvez ainsi :

  • Instanciez un pipeline d’application contenant uniquement les composants que vous devez tester.
  • Envoyez des demandes personnalisées pour vérifier le comportement de l’intergiciel.

Avantages :

  • Les requêtes sont envoyées en mémoire au lieu d’être sérialisées sur le réseau.
  • Cela évite d’autres problèmes, tels que la gestion des ports et les certificats HTTPS.
  • Les exceptions dans l’intergiciel peuvent être transmises directement au test appelant.
  • Il est possible de personnaliser les structures de données du serveur, telles que HttpContext, directement dans le test.

Configurez TestServer

Dans le projet de test, créez un test :

  • Générez et démarrez un hôte qui utilise TestServer.

  • Ajoutez tous les services requis que l’intergiciel utilise.

  • Ajoutez une référence de package au projet pour le package NuGet Microsoft.AspNetCore.TestHost.

  • Configurez le pipeline de traitement pour utiliser l’intergiciel pour le test.

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

Notes

Pour obtenir des conseils sur l’ajout de packages à des applications .NET, consultez les articles figurant sous Installer et gérer des packages dans Flux de travail de la consommation des packages (documentation NuGet). Vérifiez les versions du package sur NuGet.org.

Envoyer des requêtes avec HttpClient

Envoyez une requête à l’aide de 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("/");

    ...
}

Affirmez le résultat. Tout d’abord, faites une assertion à l’opposé du résultat attendu. Une exécution initiale avec une assertion fausse positive confirme que le test échoue lorsque l’intergiciel fonctionne correctement. Exécutez le test et confirmez que le test échoue.

Dans l’exemple suivant, l’intergiciel doit retourner un code d’état 404 (Introuvable) lorsque le point de terminaison racine est demandé. Effectuez la première série de tests avec Assert.NotEqual( ... );, qui doit échouer :

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

Modifiez l’assertion pour tester l’intergiciel dans des conditions de fonctionnement normales. Le test final utilise Assert.Equal( ... );. Réexécutez le test pour confirmer qu’il réussit.

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

Envoyer des requêtes avec HttpContext

Une application de test peut également envoyer une requête à l’aide de SendAsync(Action<HttpContext>, CancellationToken). Dans l’exemple suivant, plusieurs vérifications sont effectuées lorsque https://example.com/A/Path/?and=query est traité par l’intergiciel :

[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 permet la configuration directe d’un objet HttpContext plutôt que d’utiliser les abstractions HttpClient. Utilisez SendAsync pour manipuler des structures disponibles uniquement sur le serveur, telles que HttpContext.Items ou HttpContext.Features.

Comme dans l’exemple précédent qui testait une réponse 404 - Introuvable, vérifiez l’inverse pour chaque instruction Assert du test précédent. La vérification confirme que le test échoue correctement lorsque l’intergiciel fonctionne normalement. Une fois que vous avez confirmé que le test faux positif fonctionne, définissez les instructions finales Assert pour les conditions et valeurs attendues du test. Réexécutez-le pour confirmer que le test aboutit.

Ajouter des itinéraires de requête

Des itinéraires supplémentaires peuvent être ajoutés selon la configuration à l’aide du test 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);

Des itinéraires supplémentaires peuvent également être ajoutés à l’aide de l’approche server.SendAsync.

Limitations de TestServer

TestServer :

  • A été créé pour répliquer les comportements du serveur afin de tester l’intergiciel.
  • N’essaie pas de répliquer tous les comportements HttpClient.
  • Tente de donner au client l’accès à autant de contrôle que possible sur le serveur et avec autant de visibilité que possible sur ce qui se passe sur le serveur. Par exemple, il peut lever des exceptions qui ne sont normalement pas levées par HttpClient pour communiquer directement l’état du serveur.
  • Ne définit pas certains en-têtes spécifiques au transport par défaut, car ceux-ci ne sont généralement pas pertinents pour les intergiciels. Pour plus d'informations, consultez la section suivante.
  • Ignore la position Stream passée via StreamContent. HttpClient envoie l’intégralité du flux à partir de la position de départ, même lorsque le positionnement est défini. Pour plus d’informations, consultez ce problème GitHub.

Les en-têtes Content-Length et Transfer-Encoding

TestServer ne définit pas d’en-têtes de requête ou de réponse liés au transport, tels que Content-Length ou Transfer-Encoding. Les applications doivent éviter de dépendre de ces en-têtes, car leur utilisation varie selon le client, le scénario et le protocole. Si Content-Length et Transfer-Encoding sont nécessaires pour tester un scénario spécifique, ils peuvent être spécifiés dans le test lors de la composition de HttpRequestMessage ou HttpContext. Pour plus d’informations, consultez les problèmes GitHub suivants :