Search code examples
asp.netasp.net-web-apitddasp.net-web-api-routingweb-api-contrib

Testing ASP.NET Web API POST and Parameterless GET Routes with WebApiContrib.Testing


I am trying to set up some route tests using the WebApiContrib.Testing library. My get tests (like this) work fine...

    [Test]
    [Category("Auth Api Tests")]
    public void TheAuthControllerAcceptsASingleItemGetRouteWithAHashString()
    {
        "~/auth/sjkfhiuehfkshjksdfh".ShouldMapTo<AuthController>(c => c.Get("sjkfhiuehfkshjksdfh"));
    }

I am rather lost on the post test - I currently have the following which fails with a NotImplementedException...

    [Test]
    [Category("Auth Api Tests")]
    public void TheAuthControllerAcceptsAPost()
    {
        "~/auth".ShouldMapTo<AuthController>(c => c.Post(new AuthenticationCredentialsModel()), "POST");
    }

Here's the setup and teardown for completeness...

    [SetUp]
    public void SetUpTest()
    {
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        WebApiConfig.Register(GlobalConfiguration.Configuration);
    }

    [TearDown]
    public void TearDownTest()
    {
        RouteTable.Routes.Clear();
        GlobalConfiguration.Configuration.Routes.Clear();
    }

The route I am trying to test is the default POST route, which maps to this method call...

    [AllowAnonymous]
    public HttpResponseMessage Post([FromBody] AuthenticationCredentialsModel model)
    { *** Some code here that doesn't really matter *** }

I am also getting a failure on this test that tests the standard GET route without parameters returns all of the items...

    [Test]
    [Category("VersionInfo Api Tests")]
    public void TheVersionInfoControllerAcceptsAMultipleItemGetRouteForAllItems()
    {
        "~/versioninfo".ShouldMapTo<VersionInfoController>(c => c.Get());
    }

Which is testing this method...

    public HttpResponseMessage Get()
    { *** Some code here that doesn't really matter *** }

This library was recommended by several articles I read, but I'm not sure now if I'm doing something wrong or if it's just quite limited and I'm better off rolling my own.


Solution

  • I fixed this in the end by writing my own, after reading a post by whyleee on another question here - Testing route configuration in ASP.NET WebApi (WebApiContrib.Testing didn't seem to work for me)

    I merged his post with some of the elements I liked syntactically from the WebApiContrib.Testing library to generate the following helper class.

    This allows me to write really lightweight tests like this...

    [Test]
    [Category("Auth Api Tests")]
    public void TheAuthControllerAcceptsASingleItemGetRouteWithAHashString()
    {
        "http://api.siansplan.com/auth/sjkfhiuehfkshjksdfh".ShouldMapTo<AuthController>("Get", "hash");
    }
    
    [Test]
    [Category("Auth Api Tests")]
    public void TheAuthControllerAcceptsAPost()
    {
        "http://api.siansplan.com/auth".ShouldMapTo<AuthController>("Post", HttpMethod.Post);
    }
    

    The helper class looks like this...

    using Moq;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net.Http;
    using System.Web.Http;
    using System.Web.Http.Controllers;
    using System.Web.Http.Dispatcher;
    using System.Web.Http.Hosting;
    using System.Web.Http.Routing;
    
    namespace SiansPlan.Api.Tests.Helpers
    {
        public static class RoutingTestHelper
        {
            /// <summary>
            /// Routes the request.
            /// </summary>
            /// <param name="config">The config.</param>
            /// <param name="request">The request.</param>
            /// <returns>Inbformation about the route.</returns>
            public static RouteInfo RouteRequest(HttpConfiguration config, HttpRequestMessage request)
            {
                // create context
                var controllerContext = new HttpControllerContext(config, new Mock<IHttpRouteData>().Object, request);
    
                // get route data
                var routeData = config.Routes.GetRouteData(request);
                RemoveOptionalRoutingParameters(routeData.Values);
    
                request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData;
                controllerContext.RouteData = routeData;
    
                // get controller type
                var controllerDescriptor = new DefaultHttpControllerSelector(config).SelectController(request);
                controllerContext.ControllerDescriptor = controllerDescriptor;
    
                // get action name
                var actionMapping = new ApiControllerActionSelector().SelectAction(controllerContext);
    
                var info = new RouteInfo(controllerDescriptor.ControllerType, actionMapping.ActionName);
    
                foreach (var param in actionMapping.GetParameters())
                {
                    info.Parameters.Add(param.ParameterName);
                }
    
                return info;
            }
    
            #region | Extensions |
    
            /// <summary>
            /// Determines that a URL maps to a specified controller.
            /// </summary>
            /// <typeparam name="TController">The type of the controller.</typeparam>
            /// <param name="fullDummyUrl">The full dummy URL.</param>
            /// <param name="action">The action.</param>
            /// <param name="parameterNames">The parameter names.</param>
            /// <returns></returns>
            public static bool ShouldMapTo<TController>(this string fullDummyUrl, string action, params string[] parameterNames)
            {
                return ShouldMapTo<TController>(fullDummyUrl, action, HttpMethod.Get, parameterNames);
            }
    
            /// <summary>
            /// Determines that a URL maps to a specified controller.
            /// </summary>
            /// <typeparam name="TController">The type of the controller.</typeparam>
            /// <param name="fullDummyUrl">The full dummy URL.</param>
            /// <param name="action">The action.</param>
            /// <param name="httpMethod">The HTTP method.</param>
            /// <param name="parameterNames">The parameter names.</param>
            /// <returns></returns>
            /// <exception cref="System.Exception"></exception>
            public static bool ShouldMapTo<TController>(this string fullDummyUrl, string action, HttpMethod httpMethod, params string[] parameterNames)
            {
                var request = new HttpRequestMessage(httpMethod, fullDummyUrl);
                var config = new HttpConfiguration();
                WebApiConfig.Register(config);
    
                var route = RouteRequest(config, request);
    
                var controllerName = typeof(TController).Name;
                if (route.Controller.Name != controllerName)
                    throw new Exception(String.Format("The specified route '{0}' does not match the expected controller '{1}'", fullDummyUrl, controllerName));
    
                if (route.Action.ToLowerInvariant() != action.ToLowerInvariant())
                    throw new Exception(String.Format("The specified route '{0}' does not match the expected action '{1}'", fullDummyUrl, action));
    
                if (parameterNames.Any())
                {
                    if (route.Parameters.Count != parameterNames.Count())
                        throw new Exception(
                            String.Format(
                                "The specified route '{0}' does not have the expected number of parameters - expected '{1}' but was '{2}'",
                                fullDummyUrl, parameterNames.Count(), route.Parameters.Count));
    
                    foreach (var param in parameterNames)
                    {
                        if (!route.Parameters.Contains(param))
                            throw new Exception(
                                String.Format("The specified route '{0}' does not contain the expected parameter '{1}'",
                                              fullDummyUrl, param));
                    }
                }
    
                return true;
            }
    
            #endregion
    
            #region | Private Methods |
    
            /// <summary>
            /// Removes the optional routing parameters.
            /// </summary>
            /// <param name="routeValues">The route values.</param>
            private static void RemoveOptionalRoutingParameters(IDictionary<string, object> routeValues)
            {
                var optionalParams = routeValues
                    .Where(x => x.Value == RouteParameter.Optional)
                    .Select(x => x.Key)
                    .ToList();
    
                foreach (var key in optionalParams)
                {
                    routeValues.Remove(key);
                }
            }
    
            #endregion
        }
    
        /// <summary>
        /// Route information
        /// </summary>
        public class RouteInfo
        {
            #region | Construction |
    
            /// <summary>
            /// Initializes a new instance of the <see cref="RouteInfo"/> class.
            /// </summary>
            /// <param name="controller">The controller.</param>
            /// <param name="action">The action.</param>
            public RouteInfo(Type controller, string action)
            {
                Controller = controller;
                Action = action;
                Parameters = new List<string>();
            }
    
            #endregion
    
            public Type Controller { get; private set; }
            public string Action { get; private set; }
            public List<string> Parameters { get; private set; }
        }
    }