Writing tests for an ASP.NET Web API service

It's important to test any service you write to make sure that it's behaving the way you expect it to. In this blog post, I'll go through the main ways of testing a Web API service, exploring the benefits and drawbacks of each option so that you can test your service effectively.

Web API testing can be broadly categorized into one of three groups:

  • Unit testing a controller in isolation
  • Submitting a request to an in-memory HttpServer and testing the response you get back
  • Submitting a request to a running server over the network and testing the response you get back

Unit Testing Controllers

The first and simplest way of testing a Web API service is to unit test individual controllers. This means you'll first create an instance of the controller. And then call the Web API action you want to test with the parameters you want. Finally, you'll test that the action did what it was supposed to do, like updating a database for example and that it returned the expected value.

To illustrate the different ways of testing Web API services, let's use a simple example. Let's say you have an action that gets a movie by its ID. The action signature might look like this:

    1: public class MoviesController : ApiController
    2: {
    3:     public Movie GetMovie(int id);
    4: }

Let's give this method the following contract. If the movie ID exists in our database, it should return the corresponding movie instance. But if there isn't a movie with a matching ID, it should return a response with a 404 Not Found status code. An example of a unit test for this action might look like this:

    1: [Fact]
    2: public void GetMovie_ThrowsNotFound_WhenMovieNotFound()
    3: {
    4:     var emptyDatabase = new EmptyMoviesDbContext();
    5:     var controller = new MoviesController(emptyDatabase);
    6:     HttpResponseException responseException = Assert.Throws<HttpResponseException>(() => controller.GetMovie(1));
    7:     Assert.Equal(HttpStatusCode.NotFound, responseException.Response.StatusCode);
    8: }

As a general principle, you should always try to test as little as possible and unit testing controllers is as simple as it gets.

Submitting requests against an in-memory HttpServer

Unit testing controllers is great, and you should be trying to do so whenever you can. But it does have its limitations. First off, Web API sets up state on the controller like the Request or the Configuration properties that may be needed for your action to function properly. It also sets properties on the request that are used by certain methods. Commonly used methods in the framework like Request.CreateResponse work fine in a normal Web API pipeline, but will not work when unit testing a controller unless you configure some additional properties.

Secondly, unit testing a controller doesn't cover everything else that might go wrong with your service. If you're using custom message handlers, routing, filters, parameter binders, or formatters, none of that is accounted for when unit testing. And even if you're using all the defaults, the request might never make it to your action or might result in your action being called with the wrong parameters. Unit testing doesn't help at all with this kind of issue.

And thirdly, unit testing doesn't always help you figure out what the HTTP response looks like. Maybe you really care about a certain HTTP header being set on the response or maybe you care about your response sending back the right status code to the client. If your action returns an HttpResponseMessage or throws an HttpResponseException like our example above, then you may be able to inspect and test the response. But otherwise, you won't get any insight into what response will actually be received by the client.

The recommended way to deal with all these issues is to set up an HttpServer, create a request you want to test, and submit it to the server. You can then test the response you get back and make sure it matches your expectations. One of the advantages of Web API's architecture is that you can do this without ever having to use the network. You can create an in-memory HttpServer and simply pass requests to it. It will simulate the processing of the request and return the same response you would have gotten if it were a live server.

Here's what the same unit test we wrote earlier would look like:

    1: [Fact]
    2: public void GetMovie_ReturnsNotFound_WhenMovieNotFound()
    3: {
    4:     HttpConfiguration config = new HttpConfiguration();
    5:     config.Routes.MapHttpRoute("Default", "{controller}/{id}");
    6:     HttpServer server = new HttpServer(config);
    7:     using (HttpMessageInvoker client = new HttpMessageInvoker(server))
    8:     {
    9:         using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://localhost/Movies/-1"))
   10:         using (HttpResponseMessage response = client.SendAsync(request, CancellationToken.None).Result)
   11:         {
   12:             Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
   13:         }
   14:     };
   15: }

If you wanted to test the response body instead, you could use these lines:

    1: ObjectContent content = Assert.IsType<ObjectContent>(response.Content);
    2: Assert.Equal(expectedValue, content.Value);
    3: Assert.Equal(expectedFormatter, content.Formatter);

Submitting requests against a running HttpServer

The last way to write a Web API test is to start a running server that's listening to a network port and send a request to that server. Usually, that involves starting up WebAPI's self-host server like this:

    1: [Fact]
    2: public void GetMovie_ReturnsNotFound_WhenMovieNotFound()
    3: {
    4:     HttpSelfHostConfiguration config = new HttpSelfHostConfiguration("https://localhost/");
    5:     config.Routes.MapHttpRoute("Default", "{controller}/{id}");
    6:     using (HttpSelfHostServer server = new HttpSelfHostServer(config))
    7:     using (HttpClient client = new HttpClient())
    8:     {
    9:         server.OpenAsync().Wait();
   10:         using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://localhost/Movies/-1"))
   11:         using (HttpResponseMessage response = client.SendAsync(request).Result)
   12:         {
   13:             Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
   14:         }
   15:         server.CloseAsync().Wait();
   16:     };
   17: }

To test the response body instead, you could write:

    1: Assert.Equal(expectedResponseBody, response.Content.ReadAsStringAsync().Result);

If at all possible, you should try avoiding writing these kinds of tests. Instead of just testing the service, you're testing a whole lot more - you're testing the client, you're testing the operating system's networking stack, and you're testing the host for your service. Whenever you test more than you have to, you expose yourself to potential issues at other layers that can make test maintenance and debugging a nightmare. For example, you might now have to run your tests with administrator privileges for the self host server to open successfully.

Now with all that said, there may be cases where this kind of test is the most appropriate. If you need to test that a client and a server can communicate, it's usually better to test the client and the server individually. You can test that the client is sending the request you expect it to send, and then you can test that the server returns the expected response given that request. But there may be cases where the hosting actually matters and you need to make sure that the client request actually makes it to the Web API server correctly. The best example that comes to mind is if you're using SSL/TLS and you want to make sure that the connection is working. You might then write one test against a running server to check that the connection is working, and write the rest of your tests as unit tests or against an in-memory server to check that the service is handling requests the way you'd expect it to.