首页 文章

如何对ASP.NET Web API路由进行单元测试?

提问于
浏览
28

我正在尝试编写一些单元测试,以确保对我的Web API发出的请求被路由到具有预期参数的预期API控制器操作 .

我试图使用 HttpServer 类创建一个测试,但我从服务器得到500个响应,没有信息来调试问题 .

有没有办法为ASP.NET Web API站点的路由创建单元测试?

理想情况下,我想使用 HttpClient 创建一个请求,让服务器处理请求并将其传递给预期的路由进程 .

3 回答

  • 26

    我写了一篇关于测试路线的博客文章,并且做了很多你要问的事情:

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

    希望能帮助到你 .

    另外一个优点是我使用反射来提供动作方法 - 所以不要使用带有字符串的路径,而是以强类型方式添加它们 . 使用这种方法,如果您的操作名称发生变化,测试将无法编译,因此您可以轻松地发现错误 .

  • 8

    测试ASP.NET Web API应用程序路由的最佳方法是对 endpoints 进行集成测试 .

    这是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身份下运行,因此将使用此身份设置 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 方法基本上记录了我的路由 . 我还有一个小扩展方法来在 HttpServer 上创建一个 HttpClient .

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

    我在我的应用程序中使用基本身份验证 . 因此,这个类有一些方法可以使用Authentication头构造 HttpRequestMessege .

    最后,我做了我的Act和Assert来验证我需要的东西 . 这可能是一个矫枉过正的样本,但我认为这会给你一个好主意 .

    这是一篇关于Integration Testing with HttpServer的精彩博文 . 另外,这是Testing routes in ASP.NET Web API上的另一篇精彩帖子 .

  • 2

    嗨,当您要测试您的路线时,主要目标是使用此测试测试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"]);
        }
    }
    

    这很重要但是还不够,甚至检查选择了正确的路线并且提取了正确的路线数据并不能确保选择正确的控制器和动作如果不重写默认的 IHttpActionSelectorIHttpControllerSelector 服务,这是一个方便的方法与你自己的 .

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

相关问题