Testing ASP.NET Web API

This post explains how you can unit test a web API controller method that is using multipart form data. I am creating this post because I wanted to show that testing multipart form data is far too complicated. If at any point you think this is a ridiculous implementation, that’s ok because it is a ridiculous implementation, but I am working within the constraints of the beta web API to prove a point. This is me proving that point.

Let’s start by explaining my scenario. I’m building a web API with a DocumentController where you can GET and POST documents (files) to the server. I store the existence of these files and some metadata in using RavenDB embedded as well as store the actual file on the server. The verb in question here is the post. In the post I want the client to be able to send me a file, I create a RavenDB document storing information about the file, name the file as [RavenDBKey].[OriginalExtension] on my server, then return the RavenDB document that was created as a result of this post.

To say that in an example…

If I upload avatar.png to my Post method on my DocumentController I want RavenDb to create a key and ultimately name the file “123.png” on the server because who am I to say (as the client) that I am providing a unique file name. Once the file has been created I want it to send me back the document it created along with an HTTP status code of 201 (Created).

As you will find out if you try to do this, you need to be using multipart form data just as you would if you were doing it in something like ASP.NET MVC. This is fine, web API has classes for this. You can first create a MultipartFormDataStreamProvider with a path to the root of where you are putting these files on the server.

var streamProvider = new MultipartFormDataStreamProvider(serverFilesDirectory);

With this stream provider you can now read the request’s content as a multipart stream and tell it to use this new stream provider:

return Request.Content.ReadAsMultipartAsync(streamProvider);

Using code like this will actually return a task that saves the file in the directory your provided. Yes you read that right. Calling ReadAsMultipartAsync and passing a stream provider will save the files in your multipart request. I personally find this to be strange behavior.

To be fair, when I say strange behavior I mean unexpected behavior. There is no indication of when that saving occurred. What name did it use for the file? Why didn’t I have a say in that? This behavior would not work for my original scenario of having to rename the file after I create a RavenDB document. So I moved on and investigated the MultipartFormDataStreamProvider.

If you inherit from MultipartFormDataStreamProvider you can override the method GetLocalFileName. This method gives you a chance to return the file name you would like to use when the stream provider creates the file through its multipart magic async call. Even if I create this new derived class that implements GetLocalFileName that means I need to know what the file name is at that time. I don’t know that because I need to create a RavenDB document to get that ID, but I don’t want to create the RavenDB document until I know the file is acceptable. So now we see our first dilemma we can solve with code. If I just had code that I could execute to get that file name at the point I need to return it I might be ok. Here is the derived class that implements the GetLocalFileName and calls out to a function that is passed into my constructor.

public class RenamingMultipartFormDataStreamProvider : MultipartFormDataStreamProvider
{
    public string Root { get; set; }
    public Func<PostedFile, string> OnGetLocalFileName { get; set; }

    public RenamingMultipartFormDataStreamProvider(
        string root,
        Func<PostedFile, string> onGetLocalFileName) : base(root)
    {
        Root = root;
        OnGetLocalFileName = onGetLocalFileName;
    }

    protected override string GetLocalFileName(HttpContentHeaders headers)
    {
        string filePath = headers.ContentDisposition.FileName;

        // Multipart requests with the file name seem to always include quotes.
        if (filePath.StartsWith(@"""") && filePath.EndsWith(@""""))
            filePath = filePath.Substring(1, filePath.Length - 2);

        var filename = Path.GetFileName(filePath);
        var extension = Path.GetExtension(filePath);
        var contentType = headers.ContentType.MediaType;

        return OnGetLocalFileName(new PostedFile
            {
                Name = filename,
                Extension = extension,
                ContentType = contentType
            });
    }
}

You can see that my code just looks at the name that the stream provider would eventually use to save the file and tells the OnGetLocalFileName function some information about that file but ultimately leaves it up to the creator of the class to provide the file name. The PostedFile class is just there to hold the three parameters I send in the example above. A little freaky but this gives the caller a chance to inject the name they want.

That leads me to create OnGetLocalFileName code that looks like this:

m =>
{
    document.OriginalFileName = m.Name;
    document.ContentType = m.ContentType;

    Session.Store(document);
    Session.SaveChanges();

    var actualFileName = string.Format("{0}{1}", document.Id, m.Extension);
    document.LocalFileName = actualFileName;

    return actualFileName;
}

The document variable is a type I created that just holds info about this document we are posting. Session is a RavenDB session. This is just setting some properties on that RavenDB document, saving changes to get a unique ID and returning back a unique file name on the server.

The second part I need to be doing in this document post is making sure I send back a copy of what RavenDB document I just created as a result of posting this file so that the client knows how to reference the document in the future. This needs to be done in an async task that returns HttpResponseMessage<Document>. This step is documented pretty well in other places. Here is an example what that might look like at this stage.

return task = Request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith(task =>
{
    var filename = Path.Combine(_serverSettings.DataDir, document.LocalFileName);
    var fileInfo = new FileInfo(filename);
    if (fileInfo.Exists)
    {
        document.Length = fileInfo.Length;
    }
    return new HttpResponseMessage(HttpStatusCode.Created);
});

This code is saying to go use that streamProvider that we setup and do that magic to create the file however you do it, and when you’re done execute this closure so that I can send back the HttpStatusCode.Created and the full RavenDB document when everything is all happy.

That’s it! This is a working solution that allows you to post a file and the server will create a RavenDB document and save the file that you posted then return back the document it created. That was incredibly painful but it works.

Where it doesn’t work is in unit testing.

Let’s say I want to test to make sure that when I post a file, the response is the RavenDB document that was created as a result of the post.

If you look at the documentation for an ApiController.Request it says “The setter is not intended to be used other than for unit testing purposes”. Odd way of doing things… but I’ll go with it. At least they considered unit testing in the design, right?

_controller.Request = new HttpRequestMessage(HttpMethod.Post, new Uri("http://localhost/unit.test"));

Then for my content I can use

var content = new MultipartFormDataContent();

and add StreamContent with a MemoryStream to some dummy content with a file name. Then ultimately attach this content to the request. I can then call my _controller.Post(); in my unit test and it will use that Request I just created. Then I can assert on the RavenDB document getting created properly.

This test fails.

It is failing because The ReadAsMultipartAsync was never completing to get to the code in my ContinueWith block. The ReadAsMultipartAsync call was actually throwing an exception about the format of my request. After trying to provide the right request that would make this thing happy I gave up and went back to thinking about what I am really trying to test. I am really trying to test two things:

  1. The RavenDB document is created and returns the proper local file name for the file given.
  2. Making sure that the RavenDB document that was created is returned in the 201 response.

But maybe it's more important to point out what I don't want to test. I don’t care about testing that the file is being saved on the server. The MultipartFormDataStreamProvider is such a black box that I just have to assume that it will be able to do that. So I decided to revisit the architecture of my test so that I can test the right things in the post method.

If the MultipartFormDataStreamProvider class would allow me to change the behavior of whatever it is doing to save those files I could just create a descendant of it to change that behavior. There is no clear way to do that. The class does implement the interface IMultipartStreamProvider which sounded promising too. This interface does not have the method GetLocalFileName which is what I need in my test, so I cannot use this interface. This means I’m going to need to generalize this multipart code into an interface so that I have something I can test with that just ignores the magic in MultipartFormDataStreamProvider. Knowing the dependencies is the key to extracting this out. The previous code that was not testable relied on:

  • The original HttpRequestMessage
  • A closure that gets called before the file is saved and required a PostedFile object with information about the file and returns the file name.
  • A closure that gets called after the file is saved and returns the HttpResponseMessage to the caller.

Here is what I came up with:

public interface IMultipartRequestHandler<TResponse>
{
    Task<HttpResponseMessage<TResponse>> ExecuteAsync(
        HttpRequestMessage request,
        Func<PostedFile, string> onGetLocalFileName,
        Func<Task, HttpResponseMessage<TResponse>> onAfterStreamCompletion);
}

Using generics I can support any type of response and not just my Document type response. I can inject a concrete implementation of that interface using constructor injection. Here is the concrete implementation:

public class MultipartRequestHandler<TResponse> : IMultipartRequestHandler<TResponse>
{
    private readonly string _root;

    public MultipartRequestHandler(ServerSettings settings)
    {
        _root = settings.DataDir;
    }

    public Task<HttpResponseMessage<TResponse>> ExecuteAsync(
        HttpRequestMessage request,
        Func<PostedFile, string> onGetLocalFileName,
        Func<Task, HttpResponseMessage<TResponse>> onAfterStreamCompletion)
    {
        var streamProvider = new RenamingMultipartFormDataStreamProvider(_root, onGetLocalFileName);
        return request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith(onAfterStreamCompletion);
    }
}

You’ll see that in this implementation I’m doing pretty much the same stuff I was doing in the original implementation but it just uses the dependencies passed in rather than using them directly.

If you are weirded out by having an instance of a concrete type in the ExecuteAsync method of MultipartRequestHandler you could always pull that out. In my case I’m going to create a fake class that implements IMultipartRequestHandler<TResponse> instead of trying to extract that stuff out and test things that I really don’t care about. Here is the class that I created for my unit tests

public class FakeMultipartRequestHandler<TResponse> : IMultipartRequestHandler<TResponse>
{
    public PostedFile PostedFile { get; set; }

    public Task<HttpResponseMessage<TResponse>> ExecuteAsync(HttpRequestMessage request, Func<PostedFile, string> onGetLocalFileName, Func<Task, HttpResponseMessage<TResponse>> onAfterStreamCompletion)
    {
        onGetLocalFileName(PostedFile);

        var task = Task.Factory.StartNew(() => { });
        return task.ContinueWith(onAfterStreamCompletion);
    }
}

You’ll notice that I’m focusing on what I want to test. Remember, I don’t want to test the magic that happens with that stream provider. I just want to make sure the code that I wrote will work. There is also a public property for the PostedFile so that my unit test can set that posted file to my expected values and I can assert that the code that happens in the closures does the right things with those values.

Now let’s jump back to the DocumentController and see how this changed the implementation.

public Task<HttpResponseMessage<Document>> Post()
{
    var document = new Document {DateAdded = DateTime.UtcNow};

    return _multipartRequestHandler.ExecuteAsync(
        Request,
        m =>
        {
            document.OriginalFileName = m.Name;
            document.ContentType = m.ContentType;

            Session.Store(document);
            Session.SaveChanges();

            var actualFileName = string.Format("{0}{1}", document.Id, m.Extension);
            document.LocalFileName = actualFileName;

            return actualFileName;
        },
        task =>
        {
            var filename = Path.Combine(_serverSettings.DataDir, document.LocalFileName);
            var fileInfo = new FileInfo(filename);
            if (fileInfo.Exists)
            {
                document.Length = fileInfo.Length;
            }
            return new HttpResponseMessage<Document>(document, HttpStatusCode.Created);
        });
}

You’ll notice it is cut down to one call to our ExecuteAsync method that passes those closures that we want to test. The only missing piece is injecting the _multipartRequestHandler dependency. I am using Ninject in this case so my Ninject module has code like this:

Bind(typeof(IMultipartRequestHandler<>))
    .To(typeof(MultipartRequestHandler<>));

Which is just binding any generic IMultipartRequestHandler<> type to the same generic type of the concrete implementation MultipartRequestHandler<>.

Then in my test I can use the fake handler and follow a proper AAA model

[TestMethod]
public void Post_ValidDocument_ReturnsDocument()
{
    // Arrange
    _multipartRequestHandler.PostedFile = new PostedFile
    {
        Name = "unit.test",
        Extension = ".test",
        ContentType = "unit/test"
    };

    // Act
    var response = _controller.Post();

    // Assert
    var task = response.Result.Content.ReadAsync();
    task.ContinueWith(p =>
    {
        Assert.IsTrue(p.Result.Id > 0);
        Assert.AreEqual("unit/test", p.Result.ContentType);
        Assert.AreEqual(p.Result.Id.ToString() + ".test", p.Result.LocalFileName);
        Assert.AreEqual("unit.test", p.Result.OriginalFileName);
    }).Wait();
}

This test passes, and that's awesome. I hope the point has been made. MultipartFormDataStreamProvider is a big box of confusion and heartache.