Pages

Sunday, March 9, 2014

First step in test driving ASP.NET Web API application

This is a lengthy post. The sample code from this post is on github here

When I started learning TDD, I used to participate in lot of Kata and dojo sessions. In those sessions we would test drive simple algorithms. We would mostly end up building couple of simple classes that were test driven. That used to feel great I would always come back to my desk with great enthusiasm. But then, when I used to look at my next task, I was left clueless. Most of the times, the task would be about adding a new feature to an existing website or web service which is complicated than couple of classes we would build in dojo session. I would struggle to decide what test should I write first.

Over time and after learning from lot of mistakes, I think I have now found a rhythm. These days I follow Red-Green-Refactor starting at a higher acceptance test level, but I would talk about that in detail in some other day. In this post I want to talk about how do I start test driving new features in an ASP.NET Web API application. 

Web API is all about building REST services. And first thing we should be ideally designing when building REST services is resource. Once resource are finalised, we should be giving a though to URLs where those resources would be available or would created using. Let's take an example. Suppose we are building a REST service that lets us manage customers. So first thing we do is define a resource named Customer and then define what operations we need on this resource. e.g. you would want be able to POST on some URL in order to create customer etc. Following expands this information

Create Customer 

POST /api/Customer

Get Customer

GET /api/Customer?id={CUSTOMER_ID}

Update Customer

PUT /api/Customer?id={CUSTOMER_ID}

Delete Customer

Not supported - Deleting a customer is not allowed. 

At this point I am ready to dive into the code. So far I am doing all the design on a piece of paper. So what test should be write first? A route tests looks very logical at this stage.

Route tests

Route tests verify that the routes that we need are defined and are correctly handled by the controller we know if the right controller to handle that route. The test can also verify whether correct action on controller is invoked or not. So let's write our first test

        [Test]
        public void GetCustomerIsHandledByCustomerController()
        {
            var httpConfiguration = new HttpConfiguration(new HttpRouteCollection());
            WebApiConfig.Register(httpConfiguration);

            var request = new HttpRequestMessage(HttpMethod.Get, "http://dummylocalhost/api/Customer?id=1001");
            request.Properties[HttpPropertyKeys.HttpRouteDataKey] = httpConfiguration.Routes.GetRouteData(request);

            var controllerBuilder = new TestControllerBuilder(request, httpConfiguration);

            Assert.That(controllerBuilder.GetControllerName(), Is.EqualTo("CustomerController"));
            Assert.That(controllerBuilder.GetActionName(), Is.EqualTo("Get"));
        }

In the above test, we are calling WebApiConfig.Register method. This method is added by default when you created your Web Api project and contains all the route definitions on the instance of HttpConfiguration class that is passed into it. For the purpose of the test, we create an instance of HttpConfiguration and pass it in so that we get HttpConfiguration with all routes configured as per our application logic. 

We then create an instance of HttpRequestMessage whose URL points at "api/Customer?id=1001" and it uses HTTP GET. This is according to the resource and URL design we did in the first section. We then pass this into a TestControllerBuilder class. The code inside this class is interesting. What this class does is, takes in both http request and http configuration and tells which controller and which method on that controller would be invoked to handle this request. We then assert that controller and action names are correct. Here is the code for TestControllerBuilder

public class TestControllerBuilder
    {
        private readonly ApiControllerActionSelector actionSelector;
        private readonly HttpControllerContext controllerContext;
        private readonly HttpControllerDescriptor controlleDescriptor;
        private readonly HttpRequestMessage requestMessage;
        private readonly HttpConfiguration httpConfiguration;

        public TestControllerBuilder(HttpRequestMessage request, HttpConfiguration httpConfiguration)
        {
            var routeData = request.Properties[HttpPropertyKeys.HttpRouteDataKey] as IHttpRouteData;
            controllerContext = new HttpControllerContext(httpConfiguration, routeData, request);
            IHttpControllerSelector controllerSelector = httpConfiguration.Services.GetHttpControllerSelector();
            controlleDescriptor = controllerSelector.SelectController(request);
            controllerContext.ControllerDescriptor = controlleDescriptor;
            actionSelector = new ApiControllerActionSelector();
            this.httpConfiguration = httpConfiguration;
            requestMessage = request;
        }

        public string GetActionName()
        {
            var actionDescriptor = actionSelector.SelectAction(controllerContext);
            return actionDescriptor.ActionName;
        }

        public string GetControllerName()
        {
            var controllerType = controlleDescriptor.ControllerType;
            return controllerType.Name;
        }

        public HttpControllerContext HttpControllerContext
        {
            get { return controllerContext; }
        }
}

There is not much happening here. You first build ControllerContext using HttpConfiguration and Request. We then retrieve the HttpControllerSelector which in most cases is DefaultHttpControllerSelector. You pass in the request to this guy and he would tell you what controller and action would be used to serve this request.

The test would initially throw an exception saying "404 NOT FOUND". This is down to implementation of DefaultHttpControllerSelector. If no controller matching the route is found then an HTTP exception is thrown.

Go ahead and add a new Api controller named CustomerController with a Get(string id) method. run the test and it would pass. From this point onwards, you can either test drive the complete implementation of "Get Customer Api" or add route tests for the other customer services defined in first section.

The sample code is available on git here

But there is lot of  boilerplate code here!!!

If you do not use NUnit then you may not be interested in this pat.

If you have looked at my previous post Fluent controller builder for unit testing Web API controllers then you would know that I am not a big fan of boilerplate code and I love fluent API. Lets see what we can do to hide the ugly side of this boilerplate code and bring in some nice looking fluent API.

NUnit has a nice system of custom constraints. Custom constraints convert tedious asserts into simple and nice reading ones. For our example, I have built two constraints namely ControllerEqualityConstraint and ActionEqualityConstraint as below.


internal class ControllerEqualityConstraint : Constraint
    {
        private readonly string controller;

        public ControllerEqualityConstraint(string controller)
        {
            this.controller = controller;
        }

        public override bool Matches(object item)
        {
            var request = (HttpRequestMessage)item;

            if (request != null)
            {
                var httpConfiguration = new HttpConfiguration(new HttpRouteCollection());
                WebApiConfig.Register(httpConfiguration);
                request.Properties[HttpPropertyKeys.HttpRouteDataKey] = httpConfiguration.Routes.GetRouteData(request);

                var controllerBuilder = new TestControllerBuilder(request, httpConfiguration);
                return controller == controllerBuilder.GetControllerName();
            }

            return false;
        }

        public override void WriteDescriptionTo(MessageWriter writer)
        {
            writer.Write(controller);
        }
    }
public class ActionEqualityConstraint : Constraint
    {
        private readonly string action;

        public ActionEqualityConstraint(string action)
        {
            this.action = action;
        }

        public override bool Matches(object item)
        {
            var request = (HttpRequestMessage)item;

            if (request != null)
            {
                var httpConfiguration = new HttpConfiguration(new HttpRouteCollection());
                WebApiConfig.Register(httpConfiguration);
                request.Properties[HttpPropertyKeys.HttpRouteDataKey] = httpConfiguration.Routes.GetRouteData(request);

                var controllerBuilder = new TestControllerBuilder(request, httpConfiguration);
                return action == controllerBuilder.GetActionName();
            }

            return false;
        }

        public override void WriteDescriptionTo(MessageWriter writer)
        {
            writer.Write(action);
        }
    }

I then put them together using a third class like below

public class IsHandledBy
    {
        public static IResolveConstraint Controller(string controller)
        {
            return new ControllerEqualityConstraint(controller);
        }

        public static IResolveConstraint Action(string action)
        {
            return new ActionEqualityConstraint(action);
        }
    }

With the above in place, my test now looks like this

[Test]
        public void GetCustomerIsHandledByCustomerController2()
        {
            var request = new HttpRequestMessage(HttpMethod.Get, "http://dummylocalhost/api/Customer?id=1001");

            Assert.That(request, IsHandledBy.Controller("CustomerController"));
            Assert.That(request, IsHandledBy.Action("Get"));
        }

Now does that not look nice? I would love to hear your feedback.

1 comment: