Mocking multipart/form-data HttpRequestMessage for testing ASP.NET ApiController


I recently had a problem where I needed to unit test an ApiController action that read data off of the controller’s request object, in other words, something that looks like this:

The real-world action receives a multipart/form-data request (a form), which contains an uploaded file as one of its fields. Reading the form fields and file data from the ‘Request’ object is not particularly difficult, but figuring out how to mock the HttpRequestMessage was a non-trivial exercise, which is why I thought I’d share it here.

Note: there is a distinct difference between how this gets done for ASP.NET Web API and how it can be done for MVC (which is better supported on StackOverflow, etc from what I could find)

The final solution is actually reasonably straightforward, but it took me a long time to figure out, so here goes

Another note: the unit testing framework used is NUnit and the assumption here is that you already have your test environment set up, with a class to contain the tests for your ApiController

First, I created a helper method in my test class to build the mocked MultiPartFormDataContent

Note: The ‘boundary’ can be anything you choose as long as it doesn’t appear in the form data (see W3.org for more info on multipart/form-data)

Now that you have the MultiPartFormDataContent mock set up, the controller under test can then be created like this (for example):

And voila! Controller action relying on ‘Request’ data can now be tested.

15 thoughts on “Mocking multipart/form-data HttpRequestMessage for testing ASP.NET ApiController

  1. This is a great write-up and was really helpful to get me started so thank you for that. However, I’m wondering if you do anything with clean-up to avoid test.file from being written into the file system every time a test runs. I have multiple tests that will run asynchronously so I’m generating the file name for each test by using Guid.NewGuid(), which means each test then creates a new file in the file system. I’ve got a cleanup method with a TestFixtureTearDown, but I’m getting an error when trying to delete the files because the files are being used by other processes.

    Thanks!

    1. Hi Andrew,

      I’ve been meaning to write to you, but keep forgetting! I no longer have access to the project where I used this mock, but as far as I can recall, we didn’t really concern ourselves about the file cleanup (for better or for worse). Have you solved the problem? If so, I would love to incorporate your solution into this post.

      Cheers!

  2. Hi William,

    Nice writeup, it helped me in unit testing the controller.
    To avoid test file creation and cleanup we changed:
    From:
    using (var outFile = new StreamWriter(testFile))
    {
    outFile.WriteLine(“bleh bleh bleh”);
    }

    var fileStream = new FileStream(testFile, FileMode.Open, FileAccess.Read);
    To:
    var streamContent = new StreamContent(new MemoryStream(new byte[0]));

    It does the job!

  3. Except that, by default, it won’t recognize the POST data as multipart/form-data, even if you specify that in the Content-Type and use [FromBody] in your Post call. It will never get past the first exception – either the Request will be null, or you’ll get an error saying there’s no formatter for multipart/form-data. And even if you add one into the Register function in WebApiConfig like I found on https://stackoverflow.com/questions/30544009/no-mediatypeformatter-is-available-to-read-an-object-of-type-advertisement-in/30544420 – it will then tell you that the POST method is not supported. I wasted so much time with getting a multipart/form-data posting to POST. Ultimately I formatted it as JSON and used Content-Type of “application/json;charset=UTF-8” and posted using xhr.send(JSON.stringify(myJsonObject)); Then I could get it on the Post side by setting up an MVC model and putting that, along with [FromBody], as a parameter in my Post function.

    1. Hi Tom, I’m not entirely sure I understand which part of the write-up you are referring to. This worked EXACTLY as I described and successfully at that. Please elaborate.

      1. Well, to your credit, you don’t have the JavaScript side of things on how to post to Task Create(). But I assumed because you are checking for a multipart/form-data that I would be using FormData, append to that, and setting that as the Content-Type on the Request Header, and send via XMLHttpRequest:

        const fd = new FormData();
        fd.append(‘file’, input.files[0]); // also tried fd.append(‘file’, input.files[0], input.files[0].name);
        fd.append(‘type’, input.files[0].type);
        fd.append(‘name’, input.files[0].name);

        const xhr = new XMLHttpRequest();
        xhr.open(‘POST’, ‘/api/FileUpload/Post’, true);
        xhr.setRequestHeader(‘Content-Type’, ‘multipart/form-data’ );
        xhr.withCredentials = true;
        xhr.onreadystatechange = (response) =>
        if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(response.target.responseText);
        }
        }
        xhr.send(fd);

        So I did that and found it would always throw the exception found in that first “if” statement, no matter what I did.

        if (!Request.Content.IsMimeMultipartContent())
        {
        return Error(HttpStatusCode.UnsupportedMediaType,
        “Mime type must be multipart”);
        }

        I found also, if I left off that “if” statement, it would then throw an exception saying I didn’t have a formatter/provider that could read a multipart/form-data post (similar to the issue at https://stackoverflow.com/questions/30544009/no-mediatypeformatter-is-available-to-read-an-object-of-type-advertisement-in/49684140 ) and I tried what it says in the accepted answer, to add

        config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue(“multipart/form-data”));

        to the Register function in my WebApiConfig and this did not work, for me.

        So I moved on. It’s much easier to create a JSON object on the JavaScript side, transform the file to a base64 string (which I did with this: https://stackoverflow.com/questions/37134433/convert-input-file-to-byte-array/49676679#49676679 ), create a model on the MVC side, then bind to that model:

        const jsonObj = {
        file: base64string,
        type: input.files[0].type,
        name: input.files[0].name,
        }

        const xhr = new XMLHttpRequest();
        xhr.open(‘POST’, ‘/api/FileUpload/Post’, true);
        xhr.setRequestHeader(‘Content-Type’, ‘application/json;charset=UTF-8’ );
        xhr.withCredentials = true;
        xhr.onreadystatechange = (response) =>
        if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(response.target.responseText);
        }
        }
        xhr.send(JSON.stringify(jsonObj));

        and post it where you’d have to modify your handler like this:

        public async Task Create([FromBody]MyModel myModelObj)

        And have a model like this:

        public class MyModel {
        public string file { get; set; }
        public string type { get; set; }
        public string name { get; set; }
        }

        Because then I could retrieve my objects like:

        string strBase64File = myModelObj.file;
        string strFileType = myModelObj.type;
        string strFileName = myModelObj.name;

      2. Cheers for such a comprehensive response Tom, I no longer have access to the code base where we implemented this mock and unfortunately I can’t remember exactly how it all fit together but we didn’t use any JS in our test suite (it was all NUnit/C#).

        Having said that, I’m sure your input will be helpful to other visitors, so thank you very much for going through the effort to explain your process and research, sounds like you were also neck deep in a world of pain!

      3. You have no idea! Ran across a ton of posts with multipart/form-data and I’m convinced that only works with PHP after running into everything I tried. And yeah, hopefully what I posted will save others from the same. Cheers!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s