Thursday, September 4, 2014

ASP.NET MVC Unit Testing Part 3: Faking the Environment (HttpContext)

So I have my fake data, but I also need to address additional dependencies that are not as obvious.  These dependencies arise from the use of User and Request, which are properties of the HttpContext.  Because you apparently can't assign HttpContext directily, you have to create a fake ControllerContext.




Faking the Environment


So the User object was now causing me grief.  It was easy to understand why once I dug into it a little bit.  ( I just now realized that this doens't need to be an injection, I should be able to just manipulate my fake HttpContext... but I digress... ) Well regardless of how the injection is handled, first we have to make a fake user with a fake userid, username, and roles.  This stack overflow question got me what I needed. I wrapped it up in a static method that would let me pass in a userid and list of roles:

class FakeUser
{
    public static IPrincipal GetFakeUser(int uidstring[] roles)
    {
        string username = "TestUser";
        string userid = uid.ToString();
 
        List<Claim> claims = new List<Claim>{
         new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"username),
         new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"userid)};
        var genericIdentity = new GenericIdentity("");
        genericIdentity.AddClaims(claims);
 
        return new GenericPrincipal(genericIdentityroles);
    }
 
    public static IPrincipal GetFakeUser(int uid)
    {
        return GetFakeUser(uidnew string[] { "TestRole" });
    }
}


This made it really easy for me to test behaviors based on ownership (using userid) and role membership.  It wasn't enough.  I hit another obstacle when I ran into this call:


var redirectUrl = new UrlHelper(Request.RequestContext).Action("UnitFocus""UnitPersonnel"new { unitId = Unit_Id });


NullReferenceException... wtf? It's the Request.RequestContext that killed me.  This is part of the HttpContext which doesn't exist when you new up a controller in a unit test.  I was going to have to create a fake ControllerContext.  Unfortunately, what little bit they did in OdeToFood was not nearly robust enough to help me, so again I had to go Googling until I found something.  And find something I did.  I found a Fake HttpRequest on GitHub, which was the major piece I was missing.  I created a barebones fake HttpResponse before I realized that the same guy had already created a pretty good one of those too, (and a whole Fake HttpContext using them all...), but I got it working well enough anyway:

class FakeControllerContext : ControllerContext
 {
     HttpContextBase _context = new FakeHttpContext();
 
     ControllerContext a = new ControllerContext();
     
 
     public override System.Web.HttpContextBase HttpContext
     {
         get
         {
             return _context;
         }
         set
         {
             _context = value;
         }
     }
 }
 
 class FakeHttpContext : HttpContextBase
 {
     HttpRequestBase _request = new FakeHttpRequest("testrelativeurl"new System.Uri("http://testurl"), new System.Uri("http://testurlreferrer"));
     HttpResponseBase _response = new FakeHttpResponse();
 
     public override HttpRequestBase Request
     {
         get
         {
             return _request;
         }
     }
 
     public override HttpResponseBase Response
     {
         get
         {
             return _response;
         }
     }
 
     public override object GetService(Type serviceType)
     {
         return null;
     }
 }
 
 public class FakeHttpRequest : HttpRequestBase
 {
     private readonly HttpCookieCollection _cookies;
     private readonly NameValueCollection _formParams;
     private readonly NameValueCollection _queryStringParams;
     private readonly NameValueCollection _serverVariables;
     private readonly string _relativeUrl;
     private readonly Uri _url;
     private readonly Uri _urlReferrer;
     private readonly string _httpMethod;
     private System.Web.Routing.RequestContext _requestContext;
 
     public FakeHttpRequest(string relativeUrlstring methodNameValueCollection formParamsNameValueCollection queryStringParams,
                            HttpCookieCollection cookies)
     {
         _httpMethod = method;
         _relativeUrl = relativeUrl;
         _formParams = formParams;
         _queryStringParams = queryStringParams;
         _cookies = cookies;
         _serverVariables = new NameValueCollection();
         _requestContext = new System.Web.Routing.RequestContext();
     }
 
     public FakeHttpRequest(string relativeUrlstring methodUri urlUri urlReferrerNameValueCollection formParamsNameValueCollection queryStringParams,
                            HttpCookieCollection cookies)
         : this(relativeUrlmethodformParamsqueryStringParamscookies)
     {
         _url = url;
         _urlReferrer = urlReferrer;
     }
 
     public FakeHttpRequest(string relativeUrlUri urlUri urlReferrer)
         : this(relativeUrlHttpVerbs.Get.ToString("g"), urlurlReferrernullnullnull)
     {
     }
 
     public override NameValueCollection ServerVariables
     {
         get
         {
             return _serverVariables;
         }
     }
 
     public override NameValueCollection Form
     {
         get { return _formParams; }
     }
 
     public override NameValueCollection QueryString
     {
         get { return _queryStringParams; }
     }
 
     public override HttpCookieCollection Cookies
     {
         get { return _cookies; }
     }
 
     public override string AppRelativeCurrentExecutionFilePath
     {
         get { return _relativeUrl; }
     }
 
     public override Uri Url
     {
         get
         {
             return _url;
         }
     }
 
     public override Uri UrlReferrer
     {
         get
         {
             return _urlReferrer;
         }
     }
 
     public override string PathInfo
     {
         get { return String.Empty; }
     }
 
     public override string ApplicationPath
     {
         get
         {
             return "";
         }
     }
 
     public override string HttpMethod
     {
         get
         {
             return _httpMethod;
         }
     }
 
     public override System.Web.Routing.RequestContext RequestContext
     {
         get
         {
             _requestContext.HttpContext = new FakeHttpContext();               
             _requestContext.RouteData = new System.Web.Routing.RouteData();
             _requestContext.RouteData.RouteHandler = new MvcRouteHandler();
 
             var defaultDictionary = new System.Web.Routing.RouteValueDictionary(new { controller = "Home"action = "Index"id = new {} });
             var route = new System.Web.Routing.Route("{controller}/{action}/{id}"defaultDictionarynew MvcRouteHandler());
             _requestContext.RouteData.Values.Add("controller""unitpersonnel");
             _requestContext.RouteData.Values.Add("action""addlistitem");
 
             _requestContext.RouteData.Route = route;
 
             return _requestContext;
         }
         set
         {
             _requestContext = value;
         }
     }
 }
 
 public class FakeHttpResponse : HttpResponseBase
 {
     public override string ApplyAppPathModifier(string virtualPath)
     {
         return virtualPath;
     }
     
 }

Some of the routing stuff is probably unnecessary, it was part of the overall troubleshooting process.  I eventually got it working when I added the routes within the test project.  This code runs from the TestClass contructor:

try
{
    RouteConfig.RegisterRoutes(RouteTable.Routes);
}
catch
{
    //gulp
}

I had to wrap it in a try-catch block because it was running it multiple times per run (probably because every test class contructor ran it...) and throwing a duplicate route exception.  Again, probably a better way to handle it out there, but this is what I got working for me...

Next I'll go over the actual tests and the lessons learned.


No comments:

Post a Comment