12

I am attempting to write a test for a Web API method that uses HttpContext.Current.Request.Files and after exhaustive searching and experimentation I cannot figure out how to mock for it. The method being tested looks like this:

[HttpPost]
public HttpResponseMessage Post()
{
    var requestFiles = HttpContext.Current.Request.Files;
    var file = requestFiles.Get(0);
    //do some other stuff...
}

I realize that there are other questions similar to this, but they do not address this specific situation.

If I attempt to mock the context, I run into issues with the Http* object hierarchy. Say I set up various mock objects (using Moq) like this:

var mockFiles = new Mock<HttpFileCollectionBase>();
mockFiles.Setup(s => s.Count).Returns(1);
var mockFile = new Mock<HttpPostedFileBase>();
mockFile.Setup(s => s.InputStream).Returns(new MemoryStream());
mockFiles.Setup(s => s.Get(It.IsAny<int>())).Returns(mockFile.Object);
var mockRequest = new Mock<HttpRequestBase>();
mockRequest.Setup(s => s.Files).Returns(mockFiles.Object);
var mockContext = new Mock<HttpContextBase>();
mockContext.Setup(s => s.Request).Returns(mockRequest.Object);

Attempting to assign it to the current context...

HttpContext.Current = mockContext.Object;

...results in a compiler error/redline because it Cannot convert source type 'System.Web.HttpContextBase' to target type 'System.Web.HttpContext'.

I have also tried drilling into various context objects that come with the constructed controller object, but can't find one that a) is the return object of an HttpContext.Current call in the controller method body and b) provides access to standard HttpRequest properties, like Files.

var requestMsg = controller.Request;   //returns HttpRequestMessage
var context = controller.ControllerContext;  //returns HttpControllerContext
var requestContext = controller.RequestContext;   //read-only returns HttpRequestContext

It is also important to note that I cannot change the controller that I'm testing at all, so I cannot change the constructor to allow the context to be injected.

Is there any way to mock HttpContext.Current.Request.Files for unit testing in Web API?

Update
Though I'm not sure this will be accepted by the team, I am experimenting with changing the Post method to use Request.Content, as suggested by Martin Liversage. It currently looks something like this:

public async Task<HttpResponseMessage> Post()
{
    var uploadFileStream = new MultipartFormDataStreamProvider(@"C:\temp");
    await Request.Content.ReadAsMultipartAsync(uploadFileStream);
    //do the stuff to get the file
    return ActionContext.Request.CreateResponse(HttpStatusCode.OK, "it worked!");
}

My test looks similar to this:

var byteContent = new byte[]{};
var content = new MultipartContent { new ByteArrayContent(byteContent) };
content.Headers.Add("Content-Disposition", "form-data");
var controllerContext = new HttpControllerContext 
{
    Request = new HttpRequestMessage
        {
            Content = new MultipartContent { new ByteArrayContent(byteContent) }
        }
};

Now I'm getting an error on ReadAsMultipartAsync:

System.IO.IOException: Error writing MIME multipart body part to output stream. ---> System.InvalidOperationException: The stream provider of type 'MultipartFormDataStreamProvider' threw an exception. ---> System.InvalidOperationException: Did not find required 'Content-Disposition' header field in MIME multipart body part.

Community
  • 1
  • 1
AJ.
  • 15,284
  • 19
  • 85
  • 143
  • 1
    As an aside, if you truly can't change your code to remove the direct coupling on the `HttpContext.Current` dependency, there is a long winded way to do [this via reflection](http://stackoverflow.com/a/31177399/314291) – StuartLC Jul 09 '15 at 16:55

3 Answers3

28

Web API has been built to support unit testing by allowing you to mock various context objects. However, by using HttpContext.Current you are using "old-style" System.Web code that uses the HttpContext class which makes it impossible to unit test your code.

To allow your code to be unit testable you have to stop using HttpContext.Current. In Sending HTML Form Data in ASP.NET Web API: File Upload and Multipart MIME you can see how to upload files using Web API. Ironically, this code also uses HttpContext.Current to get access to the MapPath but in Web API you should use HostingEnvironment.MapPath that also works outside IIS. Mocking the later is also problematic but for now I am focusing on your question about mocking the request.

Not using HttpContext.Current allows you to unit test your controller by assigning the ControllerContext property of the controller:

var content = new ByteArrayContent( /* bytes in the file */ );
content.Headers.Add("Content-Disposition", "form-data");
var controllerContext = new HttpControllerContext {
  Request = new HttpRequestMessage {
    Content = new MultipartContent { content }
  }
};
var controller = new MyController();
controller.ControllerContext = controllerContext;
Martin Liversage
  • 96,855
  • 20
  • 193
  • 238
  • Thank you for your response. Please see my update above about content-disposition. – AJ. Jul 02 '15 at 21:35
  • @AJ: I have updated the code. You have to set the `Content-Disposition` header of the inner content, not on the multi-part content. – Martin Liversage Jul 03 '15 at 07:52
  • @MartinLiversage - I'm trying to get this to work, and not having much luck - if you have time, any chance you can take a look at this please? http://stackoverflow.com/questions/44073646/httprequestmessage-content-disposition-null-when-unit-testing – Darren Wainwright May 19 '17 at 16:15
  • Corrected URL for the Sending HTML Form Data article. https://docs.microsoft.com/en-us/aspnet/web-api/overview/advanced/sending-html-form-data-part-2 – Gavilan Comun Jun 18 '19 at 10:33
  • This does not answer the question! – A X Jul 07 '19 at 22:17
  • This works. If you actually need to deal with more file information set the content disposition to include the filename: ("Content-Disposition", @"form-data; filename=""filename.jpg""") and make sure you set the content type: content.Headers.Add("Content-Type","image/png") – Reginald Blue Sep 17 '19 at 19:35
2

The accepted answer is perfect for the OP's question. I wanted to add my solution here, which derives from Martin's, as this is the page I was directed to when simply searching on how to Mock out the Request object for Web API so I can add headers my Controller is looking for. I had a difficult time finding the simple answer:

   var controllerContext = new HttpControllerContext();
   controllerContext.Request = new HttpRequestMessage();
   controllerContext.Request.Headers.Add("Accept", "application/xml");

   MyController controller = new MyController(MockRepository);
   controller.ControllerContext = controllerContext;

And there you are; a very simple way to create controller context with which you can "Mock" out the Request object and supply the correct headers for your Controller method.

iGanja
  • 2,231
  • 2
  • 22
  • 30
-1

I mocked just the posted file. I believe all files can also be mocked this way.

This was in my controller

private HttpPostedFileBase _postedFile;

/// <summary>
/// For mocking HttpPostedFile
/// </summary>
public HttpPostedFileBase PostedFile
{
    get
    {
        if (_postedFile != null) return _postedFile;
        if (HttpContext.Current == null)
        {
            throw new InvalidOperationException("HttpContext not available");
        }
        return new HttpContextWrapper(HttpContext.Current).Request.Files[0];
    }
    set { _postedFile = value; }
}

[HttpPost]
public MyResponse Upload()
{
    if (!ValidateInput(this.PostedFile))
    {
        return new MyResponse
        {
            Status = "Input validation failed"
        };
    }
}

private bool ValidateInput(HttpPostedFileBase file)
{
    if (file.ContentLength == 0)
        return false;

    if (file.ContentType != "test")
        return false;

    if (file.ContentLength > (1024 * 2048))
        return false;

    return true
}

This was in my Unit test case

public void Test1()
{
    var controller = new Mock<MyContoller>();
    controller.Setup(x => x.Upload).Returns(new CustomResponse());

    controller.Request = new HttpRequestMessage();
    controller.Request.Content = new StreamContent(GetContent());
    controller.PostedFile = GetPostedFile();

    var result = controller.Upload().Result;
}

private HttpPostedFileBase GetPostedFile()
{
    var postedFile = new Mock<HttpPostedFileBase>();
    using (var stream = new MemoryStream())
    using (var bmp = new Bitmap(1, 1))
    {
        var graphics = Graphics.FromImage(bmp);
        graphics.FillRectangle(Brushes.Black, 0, 0, 1, 1);
        bmp.Save(stream, ImageFormat.Jpeg);
        postedFile.Setup(pf => pf.InputStream).Returns(stream);
        postedFile.Setup(pf => pf.ContentLength).Returns(1024);
        postedFile.Setup(pf => pf.ContentType).Returns("bmp");
    }
    return postedFile.Object;
}

Although, I was not able to successfully mock the HTTPContext. But, I was able to mock the file upload.

Abhishek
  • 1
  • 1