web-dev-qa-db-fra.com

Comment pouvez-vous tester de manière unitaire le routage de l'API Web ASP.NET?

J'essaie d'écrire des tests unitaires pour m'assurer que les demandes adressées à mon API Web sont acheminées vers l'action de contrôleur d'API attendue avec les arguments attendus.

J'ai essayé de créer un test en utilisant la classe HttpServer, mais je reçois 500 réponses du serveur et aucune information pour déboguer le problème.

Existe-t-il un moyen de créer un test unitaire pour le routage d'un site d'API Web ASP.NET?

Idéalement, je voudrais créer une demande à l'aide de HttpClient et demander au serveur de gérer la demande et de la passer par le processus de routage attendu.

31
Paul Turner

J'ai écrit un article de blog sur le test des itinéraires et sur à peu près ce que vous demandez:

http://www.strathweb.com/2012/08/testing-routes-in-asp-net-web-api/

J'espère que ça aide.

L'avantage supplémentaire est que j'ai utilisé la réflexion pour fournir des méthodes d'action - donc au lieu d'utiliser des routes avec des chaînes, vous les ajoutez de manière fortement typée. Avec cette approche, si les noms de vos actions changent un jour, les tests ne seront pas compilés, vous pourrez donc facilement repérer les erreurs.

25
Filip W

Le meilleur moyen de tester vos itinéraires pour votre application API Web ASP.NET est le test d'intégration de vos points de terminaison.

Voici un exemple de test d'intégration simple pour votre application API Web ASP.NET. Cela ne teste pas principalement vos itinéraires, mais il les teste de manière invisible. Aussi, j'utilise ici XUnit, Autofac et Moq.

[Fact, NullCurrentPrincipal]
public async Task 
    Returns_200_And_Role_With_Key() {

    // Arrange
    Guid key1 = Guid.NewGuid(),
         key2 = Guid.NewGuid(),
         key3 = Guid.NewGuid(),
         key4 = Guid.NewGuid();

    var mockMemSrv = ServicesMockHelper
        .GetInitialMembershipService();

    mockMemSrv.Setup(ms => ms.GetRole(
            It.Is<Guid>(k =>
                k == key1 || k == key2 || 
                k == key3 || k == key4
            )
        )
    ).Returns<Guid>(key => new Role { 
        Key = key, Name = "FooBar"
    });

    var config = IntegrationTestHelper
        .GetInitialIntegrationTestConfig(GetInitialServices(mockMemSrv.Object));

    using (var httpServer = new HttpServer(config))
    using (var client = httpServer.ToHttpClient()) {

        var request = HttpRequestMessageHelper
            .ConstructRequest(
                httpMethod: HttpMethod.Get,
                uri: string.Format(
                    "https://localhost/{0}/{1}", 
                    "api/roles", 
                    key2.ToString()),
                mediaType: "application/json",
                username: Constants.ValidAdminUserName,
                password: Constants.ValidAdminPassword);

        // Act
        var response = await client.SendAsync(request);
        var role = await response.Content.ReadAsAsync<RoleDto>();

        // Assert
        Assert.Equal(key2, role.Key);
        Assert.Equal("FooBar", role.Name);
    }
}

Il y a quelques assistants externes que j'utilise pour ce test. L'un d'eux est le NullCurrentPrincipalAttribute. Comme votre test s'exécutera sous votre identité Windows, le Thread.CurrentPrincipal sera défini avec cette identité. Donc, si vous utilisez une sorte d'autorisation dans votre application, il est préférable de s'en débarrasser en premier lieu:

public class NullCurrentPrincipalAttribute : BeforeAfterTestAttribute {

    public override void Before(MethodInfo methodUnderTest) {

        Thread.CurrentPrincipal = null;
    }
}

Ensuite, je crée une maquette MembershipService. Il s'agit d'une configuration spécifique à l'application. Donc, cela sera modifié pour votre propre implémentation.

Le GetInitialServices crée le conteneur Autofac pour moi.

private static IContainer GetInitialServices(
    IMembershipService memSrv) {

    var builder = IntegrationTestHelper
        .GetEmptyContainerBuilder();

    builder.Register(c => memSrv)
        .As<IMembershipService>()
        .InstancePerApiRequest();

    return builder.Build();
}

La méthode GetInitialIntegrationTestConfig initialise juste ma configuration.

internal static class IntegrationTestHelper {

    internal static HttpConfiguration GetInitialIntegrationTestConfig() {

        var config = new HttpConfiguration();
        RouteConfig.RegisterRoutes(config.Routes);
        WebAPIConfig.Configure(config);

        return config;
    }

    internal static HttpConfiguration GetInitialIntegrationTestConfig(IContainer container) {

        var config = GetInitialIntegrationTestConfig();
        AutofacWebAPI.Initialize(config, container);

        return config;
    }
}

Le RouteConfig.RegisterRoutes la méthode enregistre essentiellement mes itinéraires. J'ai également une petite méthode d'extension pour créer un HttpClient sur le HttpServer.

internal static class HttpServerExtensions {

    internal static HttpClient ToHttpClient(
        this HttpServer httpServer) {

        return new HttpClient(httpServer);
    }
}

Enfin, j'ai une classe statique appelée HttpRequestMessageHelper qui a un tas de méthodes statiques pour construire une nouvelle instance de HttpRequestMessage.

internal static class HttpRequestMessageHelper {

    internal static HttpRequestMessage ConstructRequest(
        HttpMethod httpMethod, string uri) {

        return new HttpRequestMessage(httpMethod, uri);
    }

    internal static HttpRequestMessage ConstructRequest(
        HttpMethod httpMethod, string uri, string mediaType) {

        return ConstructRequest(
            httpMethod, 
            uri, 
            new MediaTypeWithQualityHeaderValue(mediaType));
    }

    internal static HttpRequestMessage ConstructRequest(
        HttpMethod httpMethod, string uri,
        IEnumerable<string> mediaTypes) {

        return ConstructRequest(
            httpMethod,
            uri,
            mediaTypes.ToMediaTypeWithQualityHeaderValues());
    }

    internal static HttpRequestMessage ConstructRequest(
        HttpMethod httpMethod, string uri, string mediaType, 
        string username, string password) {

        return ConstructRequest(
            httpMethod, uri, new[] { mediaType }, username, password);
    }

    internal static HttpRequestMessage ConstructRequest(
        HttpMethod httpMethod, string uri, 
        IEnumerable<string> mediaTypes,
        string username, string password) {

        var request = ConstructRequest(httpMethod, uri, mediaTypes);
        request.Headers.Authorization = new AuthenticationHeaderValue(
            "Basic",
            EncodeToBase64(
                string.Format("{0}:{1}", username, password)));

        return request;
    }

    // Private helpers
    private static HttpRequestMessage ConstructRequest(
        HttpMethod httpMethod, string uri,
        MediaTypeWithQualityHeaderValue mediaType) {

        return ConstructRequest(
            httpMethod, 
            uri, 
            new[] { mediaType });
    }

    private static HttpRequestMessage ConstructRequest(
        HttpMethod httpMethod, string uri,
        IEnumerable<MediaTypeWithQualityHeaderValue> mediaTypes) {

        var request = ConstructRequest(httpMethod, uri);
        request.Headers.Accept.AddTo(mediaTypes);

        return request;
    }

    private static string EncodeToBase64(string value) {

        byte[] toEncodeAsBytes = Encoding.UTF8.GetBytes(value);
        return Convert.ToBase64String(toEncodeAsBytes);
    }
}

J'utilise l'authentification de base dans mon application. Ainsi, cette classe a quelques méthodes qui construisent un HttpRequestMessege avec l'en-tête d'authentification.

À la fin, je fais mon Act et Assert pour vérifier les choses J'ai besoin. Cela peut être un échantillon exagéré, mais je pense que cela vous donnera une excellente idée.

Voici un excellent article de blog sur Test d'intégration avec HttpServer . En outre, voici un autre excellent article sur Test des routes dans l'API Web ASP.NET .

9
tugberk

salut lorsque vous allez tester vos itinéraires, l'objectif principal est de tester GetRouteData () avec ce test, vous vous assurez que le système d'itinéraire reconnaît correctement votre demande et que l'itinéraire correct est sélectionné.

[Theory]
[InlineData("http://localhost:5240/foo/route", "GET", false, null, null)]
[InlineData("http://localhost:5240/api/Cars/", "GET", true, "Cars", null)]
[InlineData("http://localhost:5240/api/Cars/123", "GET", true, "Cars", "123")]
public void DefaultRoute_Returns_Correct_RouteData(
     string url, string method, bool shouldfound, string controller, string id)
{
    //Arrange
    var config = new HttpConfiguration();

    WebApiConfig.Register(config);

    var actionSelector = config.Services.GetActionSelector();
    var controllerSelector = config.Services.GetHttpControllerSelector();

    var request = new HttpRequestMessage(new HttpMethod(method), url);
    config.EnsureInitialized();
    //Act
    var routeData = config.Routes.GetRouteData(request);
    //Assert
    // assert
    Assert.Equal(shouldfound, routeData != null);
    if (shouldfound)
    {
        Assert.Equal(controller, routeData.Values["controller"]);
        Assert.Equal(id == null ? (object)RouteParameter.Optional : (object)id, routeData.
        Values["id"]);
    }
}

c'est important mais cela ne suffit pas, même vérifier que la bonne route est sélectionnée et que les bonnes données de route sont extraites ne garantit pas que le contrôleur et l'action corrects sont sélectionnés, c'est une méthode pratique si vous ne réécrivez pas la valeur par défaut IHttpActionSelector et IHttpControllerSelector services avec les vôtres.

 [Theory]
        [InlineData("http://localhost:12345/api/Cars/123", "GET", typeof(CarsController), "GetCars")]
        [InlineData("http://localhost:12345/api/Cars", "GET", typeof(CarsController), "GetCars")]
        public void Ensure_Correct_Controller_and_Action_Selected(string url,string method,
                                                    Type controllerType,string actionName) {
            //Arrange
            var config = new HttpConfiguration();
            WebApiConfig.Register(config);

            var controllerSelector = config.Services.GetHttpControllerSelector();
            var actionSelector = config.Services.GetActionSelector();

            var request = new HttpRequestMessage(new HttpMethod(method),url);

            config.EnsureInitialized();

            var routeData = config.Routes.GetRouteData(request);
            request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData;
            request.Properties[HttpPropertyKeys.HttpConfigurationKey] = config;
            //Act
            var ctrlDescriptor = controllerSelector.SelectController(request);
            var ctrlContext = new HttpControllerContext(config, routeData, request)
            {
                ControllerDescriptor = ctrlDescriptor
            };
            var actionDescriptor = actionSelector.SelectAction(ctrlContext);
            //Assert
            Assert.NotNull(ctrlDescriptor);
            Assert.Equal(controllerType, ctrlDescriptor.ControllerType);
            Assert.Equal(actionName, actionDescriptor.ActionName);
        }
    }
2
Alejandro Serret