Подтвердить что ты не робот

Как вы можете unit test маршрутизировать веб-API ASP.NET?

Я пытаюсь написать некоторые модульные тесты, чтобы гарантировать, что запросы, сделанные в моем веб-API, перенаправлены на ожидаемое действие контроллера API с ожидаемыми аргументами.

Я попытался создать тест с использованием класса HttpServer, но я получаю 500 ответов от сервера и никакой информации для отладки проблемы.

Есть ли способ создать модульные тесты для маршрутизации веб-сайта ASP.NET Web API?

В идеале я хотел бы создать запрос с помощью HttpClient и обработать запрос сервера и передать его через ожидаемый процесс маршрутизации.

4b9b3361

Ответ 1

Я написал сообщение в блоге о маршрутах тестирования и в значительной степени о том, о чем вы спрашиваете:

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

Надеюсь, что это поможет.

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

Ответ 2

Лучший способ проверить свои маршруты для вашего приложения ASP.NET Web API - это интеграция, проверяющая конечные точки.

Вот простой тестовый пример интеграции для вашего приложения ASP.NET Web API. Это не в основном проверяет ваши маршруты, но это незаметно проверяет их. Кроме того, здесь я использую XUnit, Autofac и 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);
    }
}

Есть несколько внешних помощников, которые я использую для этого теста. Один из них - NullCurrentPrincipalAttribute. Когда ваш тест будет запущен под вашей Windows Identity, Thread.CurrentPrincipal будет установлен с этим идентификатором. Итак, если вы используете какую-то авторизацию в своем приложении, лучше всего избавиться от этого:

public class NullCurrentPrincipalAttribute : BeforeAfterTestAttribute {

    public override void Before(MethodInfo methodUnderTest) {

        Thread.CurrentPrincipal = null;
    }
}

Затем я создаю макет MembershipService. Это специфичная для приложения настройка. Таким образом, это будет изменено для вашей собственной реализации.

GetInitialServices создает для меня контейнер Autofac.

private static IContainer GetInitialServices(
    IMembershipService memSrv) {

    var builder = IntegrationTestHelper
        .GetEmptyContainerBuilder();

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

    return builder.Build();
}

Метод GetInitialIntegrationTestConfig просто инициализирует мою конфигурацию.

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

Метод RouteConfig.RegisterRoutes в основном регистрирует мои маршруты. У меня также есть небольшой метод расширения для создания HttpClient по HttpServer.

internal static class HttpServerExtensions {

    internal static HttpClient ToHttpClient(
        this HttpServer httpServer) {

        return new HttpClient(httpServer);
    }
}

Наконец, у меня есть статический класс под названием HttpRequestMessageHelper, который имеет кучу статических методов для создания нового экземпляра 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);
    }
}

Я использую Basic Authentication в своем приложении. Итак, этот класс имеет некоторые методы, которые строят HttpRequestMessege с заголовком Аутентификация.

В конце концов, я делаю свой Закон и Assert, чтобы проверить то, что мне нужно. Это может быть образец избыточного количества, но я думаю, что это даст вам отличную идею.

Вот отличная запись в блоге Тестирование интеграции с HttpServer. Кроме того, вот еще один отличный пост в Тестирование маршрутов в веб-интерфейсе ASP.NET.

Ответ 3

Для тестирования маршрутов веб-API ASP.NET вы можете использовать сторонний инструмент, например MvcRouteTester или MyWebApi. Вот пример кода, который поможет вам начать:

MyWebApi
    .Routes()
    .ShouldMap("api/WebApiController/SomeAction")
    .WithHttpMethod(HttpMethod.Post)
    .WithJsonContent(@"{""SomeInt"": 1, ""SomeString"": ""Test""}")
    .To<WebApiController>(c => c.SomeAction(new RequestModel
    {
        SomeInt = 1,
        SomeString = "Test"
    }));

Если вы хотите реализовать тестирование самостоятельно, вы можете посмотреть, как сделать так, чтобы внутренний URL-адрес API-интерфейса маршрутизации отображал запрос для вас. Тогда вам нужно сравнить только выбранный контроллер и действия. ССЫЛКА ЗДЕСЬ.

Ответ 4

hi, когда вы собираетесь тестировать свои маршруты, основной целью является тест GetRouteData() с этим тестом, который вы убедитесь, что система маршрута правильно распознает ваш запрос и выбран правильный маршрут.

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

это важно, но недостаточно, даже проверяя, что выбран правильный маршрут, и извлечены правильные данные маршрута, не гарантирует, что выбран правильный контроллер и действие это удобный метод, если вы не переписываете службы IHttpActionSelector и IHttpControllerSelector по умолчанию.

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