4

I have an action that returns either a FileContentResult or a NotModifiedResult, which is a custom result type that returns HTTP 304 to indicate that the requested resource has not been modified, like this:

[ReplaceMissingPicture(Picture = "~/Content/Images/nothumbnail.png", MimeType = "image/png")]
public ActionResult Thumbnail(int id)
{
    var item = Service.GetItem(id);

    var requestTag = Request.Headers["If-None-Match"] ?? string.Empty;
    var tag = Convert.ToBase64String(item.Version.ToArray());

    if (tag == requestTag)
    {
        return new NotModifiedResult();
    }

    if (item.Thumbnail != null)
    {
        var thumbnail = item.Thumbnail.ToArray();
        var mime = item.PictureMime;

        Response.AppendHeader("ETag", tag);

        return File(thumbnail, mime);
    }
    else
    {
        return null;
    }
}

This action needs to access the Response object, which is of course not present during testing, so that makes this action untestable. I could add conditional statements around it, so that it runs during testing, but then I can't test for the headers being set correctly.

What would be a solution to this problem?

FYI, the ReplaceMissingPicture filter returns a specific resource in case null was returned from this action, to keep the MapPath() call out of the controller for the very same reason.

Dave Van den Eynde
  • 17,020
  • 7
  • 59
  • 90
  • what about controllerInstance.HttpContext.Response.Headers["ETag"] in a test method ? – Ahmed Khalaf Sep 20 '09 at 16:25
  • You specifically have to mock that if you want an HttpContext during unit testing. The whole point here is to make your action method so that it doesn't rely on HttpContext. – Dave Van den Eynde Sep 20 '09 at 20:04

2 Answers2

1

The first step would be to create an interface which simplifies the services you need:-

  public interface IHeaders
  {
       public string GetRequestHeader(string headerName);
       public void AppendResponseHeader(string headerName, string headerValue);
  }

Now create a default implementation:-

 public Headers : IHeaders
 {
       public string GetRequestHeader(string headerName)
       {
            return HttpContext.Current.Request[headerName];
       }
       public void AppendResponseHeader(string headerName, string headerValue)
       {
            HttpContext.Current.Response.AppendHeader(headerName, headerValue);
       } 
 }

Now add a new field to your Controller:-

     private IHeaders myHeadersService;

add new constructor to you controller:-

     public MyController(IHeaders headersService) 
     {
         myHeadersService = headersService;
     }

modify or add the default constructor:-

    public MyController()
    {
         myHeadersService = new Headers();
    }

now in your Action code use myHeadersService instead of the Response and Request objects.

In your tests create your own implementation of the IHeaders interface to emulate/test the Action code and pass that implementation when constructing the Controller.

AnthonyWJones
  • 187,081
  • 35
  • 232
  • 306
1

How about creating a subclass of FileResult--say ETagFileResult--that in its ExecuteResult() method sets the ETag header, and then defaults to the base class implementation? You can test that class with a mocked context (as you presumably are with your NotModifiedResult) to be sure that it's doing the right thing. And remove the entire complication from the testing of the controller.

Failing that, it's possible to set a mocked context on the controller in your test (after instantiating the class, before calling the action method). See this question, for instance. But that seems like more work.

(Also, by the way, it looks like you're quoting the tag value twice there: once when tag is set, and once more when you actually set the header....)

Community
  • 1
  • 1
Sixten Otto
  • 14,816
  • 3
  • 48
  • 60