Sunday, August 30, 2015

Unit Testing (part 2) - Faking the HttpContext and HttpContextBase

This is the 2nd in a series of posts about unit testing:

Unit Testing (part 1) - Without using a mocking framework

Unit Testing (part 2) - Faking the HttpContext and HttpContextBase

Unit Testing (part 3) - Running Unit Tests & Code Coverage

Unit Testing (part 4) - Faking Entity Framework code first DbContext & DbSet

 

There are a lot of posts on the internet about faking the http context.  And when we have controllers / service functions / MVC routes that all make use of the HttpContext how do you fake it. 

This is my approach which works consistently across all my tests.  I went down a route initially of having different ways of handling a HttpContext vs a HttpContextBase.  And then I found way to have 1 approach so the tests are consistent.

 

First we need a way to fake a normal HttpContext.

    public class FakeHttpContext

    {

        public HttpContext CreateFakeHttpContext()

        {

            var httpRequest = new HttpRequest("", "http://localhost/", "");

            var stringWriter = new StringWriter();

            var httpResponce = new HttpResponse(stringWriter);

            var httpContext = new HttpContext(httpRequest, httpResponce);

            var sessionContainer = new HttpSessionStateContainer("id",

new SessionStateItemCollection(),

new HttpStaticObjectsCollection(),

10,

true,                                                    HttpCookieMode.AutoDetect,                                                    SessionStateMode.InProc, false);

            SessionStateUtility.AddHttpSessionStateToContext(httpContext, sessionContainer);

            return httpContext;

        }

    }

 

Secondly we need a way to fake HttpContextBase. 

MVC already have this covered with their implementation of HttpContextWrapper.  But unfortunately even this doesn’t go quite far enough especially when you want to unit test MVC routes. But we can extend it, enter our FakeHttpContextWrapper.  This inherits from Microsoft’s HttpContextWrapper.

What this is going to do is allow us to create our own fake classes by extending the Microsoft ones (so we don’t have to reinvent the wheel).  And this will now let us over-ride the properties we need for unit testing.

We first create a FakeHttpContextWrapper which contains our FakeHttpRequestWrapper, and could also contain a FakeHttpResponseWrapper if we needed it. I’ve commented out the FakeHttpResponseWrapper as I didn’t need it but it does work if you uncomment it.

    public class FakeHttpContextWrapper : HttpContextWrapper

    {

        FakeHttpRequestWrapper _request;

        //FakeHttpResponseWrapper _response;


       
public FakeHttpContextWrapper(HttpContext httpContext)

            : base(httpContext)

        {

            _request = new FakeHttpRequestWrapper(httpContext.Request);

            //_response = new FakeHttpResponseWrapper(httpContext.Response);

        }

 

public FakeHttpContextWrapper(HttpContext httpContext, string appPath = "/",       

                            string requestUrl = "~/", string clientIP = null)

            : base(httpContext)

        {

            _request = new FakeHttpRequestWrapper(httpContext.Request, appPath, requestUrl, clientIP);

            //_response = new FakeHttpResponseWrapper(httpContext.Response);

        }

 

        /// <summary>

        /// Over-ridden so we can return our FakeHttpRequestWrapper class instead

        /// </summary>

        public override HttpRequestBase Request

        {

            get

            {

                return _request;

            }

        }

 

        public override HttpResponseBase Response

        {

            get

            {

                //return _response;

                return base.Response;

            }

        }

    }

We then create a FakeHttpRequestWrapper which inherits from the Microsoft HttpRequestWrapper.  This is where the magic happens as this allows us to over-ride those getter properties ApplicationPath, AppRelativeCurrentExecutionFilePath and UserHostAddress so we can implement our own logic.

    /// <summary>

    /// This class allows us to set additional properties that are anoyingly null by

    /// default if you just used the HttpRequestWrapper class alone.

    /// We are over-ridding the ApplicationPath & AppRelativeCurrentExecutionFilePath

    /// as these are needed to unit test MVC routes

    /// </summary>

    public class FakeHttpRequestWrapper : HttpRequestWrapper

    {

        string appPath;

        string requestUrl;

        string clientIP;

 

        public FakeHttpRequestWrapper(HttpRequest httpRequest)

            : base(httpRequest)

        {

        }

 

        public FakeHttpRequestWrapper(HttpRequest httpRequest, string appPath = "/",

                                    string requestUrl = "~/", string clientIP = null)

            : base(httpRequest)

        {

            this.appPath = appPath;

            this.requestUrl = requestUrl;

            this.clientIP = clientIP;

        }

 

        public override string ApplicationPath

        {

            get

            {

                return appPath;

            }

        }

 

        public override string AppRelativeCurrentExecutionFilePath

        {

            get

            {

                return requestUrl;

            }

        }

 

        public override string UserHostAddress

        {

            get

            {

                return clientIP;

            }

        }

    }

I didn’t need to fake the response but this is how you’d do it if you need to:

    public class FakeHttpResponseWrapper : HttpResponseWrapper

    {

        public FakeHttpResponseWrapper(HttpResponse httpResponse)

            : base(httpResponse)

        {

        }

    }

In this unit test you can see 2 lines that deal with setting the HttpContext. 

[TestMethod]

public void ProductService_PopulateModel_IPDetectUSA()

{

  var clientIP = "1.1.1.2";

  var productService = unityContainer.Resolve<IProductService>();

  HttpContext.Current = new FakeHttpContext().CreateFakeHttpContext();

 

  var httpContextWrapper = new FakeHttpContextWrapper(httpContext:     

                                        HttpContext.Current,

                                        clientIP: clientIP);

  var model = productService.PopulateModel(httpContextWrapper);

}

The 1st line sets a normal HttpContext.Current using our FakeHttpContext () method. 

The 2nd line uses our FakeHttpContextWrapper method to wrap and extend the HttpContext.Current we set up on the 1st line which is something we can now pass to MVC controllers or routes.

 

And this is how you’d unit test an MVC route (the clientIP and requestUrl parameters are optional).

[TestMethod]

public void Route_Account_Login()

{

  var routes = new RouteCollection();

  RouteConfig.RegisterRoutes(routes);

   HttpContext.Current = new FakeHttpContext().CreateFakeHttpContext();

   var httpContextWrapper = new FakeHttpContextWrapper(httpContext:

       HttpContext.Current,

requestUrl: "~/account/login/");

   var routeData = routes.GetRouteData(httpContextWrapper);

 

   //Expected Results

   AssertAll.Execute(() => Assert.IsNotNull(routeData),

   () => Assert.AreEqual("Account", (string)routeData.Values["controller"], true),

   () => Assert.AreEqual("login", (string)routeData.Values["action"], true),

   () => Assert.IsTrue(string.IsNullOrWhiteSpace(routeData.Values["id"].ToString())));

}


 

And this is how you would test an MVC controller, using the same 2 lines in yellow just without the optional parameter this time.

[TestMethod]

public void ProductController_CheckView()

{

   var productService = unityContainer.Resolve<IProductService>();

   var productController = new ProductController(productService);

   HttpContext.Current = new FakeHttpContext().CreateFakeHttpContext();

   var httpContextWrapper = new FakeHttpContextWrapper(httpContext:

HttpContext.Current);

  // Pass the fake httpContext to the controller context

  productController.ControllerContext = new ControllerContext(httpContextWrapper, new

RouteData(), productController);

 

  // Call the controller method to test

  var controllerResult = productController.Index();

 

  // Check the view name is correct

  Assert.IsTrue(controllerResult is ViewResult, "ViewResult");

  var view = controllerResult as ViewResult;

  Assert.AreEqual("Index", view.ViewName, "ViewName");

}

No comments:

Post a Comment