다음을 통해 공유


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.

Comments

  • Anonymous
    January 28, 2013
    Great Post.. Thank you so much for sharing this

  • Anonymous
    February 09, 2013
    How do you handle test if you have a [authorize] attribute on your controller?

  • Anonymous
    February 11, 2013
    I'd like to point out that your "unit test" isn't a unit test after all since it relies on a database to be preset with expected values and interacts with other portions of the code (the data access layer).

  • Anonymous
    February 11, 2013
    Good point, Mike. If you were actually going to unit test an action that was hitting a database, you'd want to create a dependency on the data provider, mock the database, and pass it in. I'll try to update the post to make it clear that's the better approach. Dan, The best way I can think of to test [Authorize] would be to set Thread.CurrentPrincipal to whatever principal you want to test and then to submit a request to an in-memory server. The [Authorize] attribute should run against the principal you set.

  • Anonymous
    February 14, 2013
    @Mike, I've updated the unit test. Hope it's more to your liking : )

  • Anonymous
    July 07, 2013
    Is there a way to pass in a mock repository to the tested controller in the in-memory server testing scenario?

  • Anonymous
    July 26, 2013
    @Szilard, you can always use Web API's dependency injection support to pass a mock repository to a controller. Take a look at this: www.asp.net/.../using-the-web-api-dependency-resolver You can also use popular dependency injectors with Web API like Ninject and Unity

  • Anonymous
    October 16, 2013
    Thanks for the article. I must be missing something, this doesn't appear to be testing any specific controller? I mean, this test will run in a test project, how does it know anything about a controller?

  • Anonymous
    December 23, 2013
    Wow!! That's what I was looking for. Thank you so much.

  • Anonymous
    June 17, 2014
    Beautiful, exactly what I needed. Thanks!

  • Anonymous
    April 26, 2015
    A downside of HttpMessageInvoker is that it doesn't have HttpClient's extension methods. No PostAsync, for example. :-(

  • Anonymous
    August 18, 2015
    You can get the same functionality as PostAsync by setting your HttpRequestMessage with HttpMethod.Post. So it would look like: HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Movies/-1")