tags:

views:

69

answers:

3

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.

A: 

a quick and dirty solution is to put the Response object among ViewData collection, which will be available in the returned view.

Ahmed Khalaf
Putting the Response object in the ViewData is in itself a nasty solution, and it won't work here because FileContentResult does not have a ViewData.
Dave Van den Eynde
mmm I should have paid attention to that, thanks Dave!
Ahmed Khalaf
A: 

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
That sounds like a respectable solution!
Dave Van den Eynde
What should this give us more than using ViewData or any other ready data collection ?
Ahmed Khalaf
The ViewData is not a good place, partly because it's not typesafe. Also, I'm not convinced that a FileContentResult has a ViewData at all.
Dave Van den Eynde
@Downvoter: Reason please?
AnthonyWJones
I think it was Ahmed.
Dave Van den Eynde
+1  A: 

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....)

Sixten Otto
That was initially my thought too, but instead have a result "wrapper" that's a more generic class, like TagResult<T>.
Dave Van den Eynde
Also, about the quoting: yes, this has been fixed in my app. I'll update the code example. TY.
Dave Van den Eynde
Yeah, the wrapper's not a bad idea at all: you'd be able to use that with both file results and regular HTML views.
Sixten Otto