Unit Testing Controllers in ASP.NET Web API 2
by Mike Wasson
This topic describes some specific techniques for unit testing controllers in Web API 2. Before reading this topic, you might want to read the tutorial Unit Testing ASP.NET Web API 2, which shows how to add a unit-test project to your solution.
Software versions used in the tutorial
- Visual Studio 2017
- Web API 2
- Moq 4.5.30
[!NOTE] I used Moq, but the same idea applies to any mocking framework. Moq 4.5.30 (and later) supports Visual Studio 2017, Roslyn and .NET 4.5 and later versions.
A common pattern in unit tests is “arrange-act-assert”:
- Arrange: Set up any prerequisites for the test to run.
- Act: Perform the test.
- Assert: Verify that the test succeeded.
In the arrange step, you will often use mock or stub objects. That minimizes the number of dependencies, so the test is focused on testing one thing.
Here are some things that you should unit test in your Web API controllers:
- The action returns the correct type of response.
- Invalid parameters return the correct error response.
- The action calls the correct method on the repository or service layer.
- If the response includes a domain model, verify the model type.
These are some of the general things to test, but the specifics depend on your controller implementation. In particular, it makes a big difference whether your controller actions return HttpResponseMessage or IHttpActionResult. For more information about these result types, see Action Results in Web Api 2.
Testing Actions that Return HttpResponseMessage
Here is an example of a controller whose actions return HttpResponseMessage.
[!code-csharpMain]
1: public class ProductsController : ApiController
2: {
3: IProductRepository _repository;
4:
5: public ProductsController(IProductRepository repository)
6: {
7: _repository = repository;
8: }
9:
10: public HttpResponseMessage Get(int id)
11: {
12: Product product = _repository.GetById(id);
13: if (product == null)
14: {
15: return Request.CreateResponse(HttpStatusCode.NotFound);
16: }
17: return Request.CreateResponse(product);
18: }
19:
20: public HttpResponseMessage Post(Product product)
21: {
22: _repository.Add(product);
23:
24: var response = Request.CreateResponse(HttpStatusCode.Created, product);
25: string uri = Url.Link("DefaultApi", new { id = product.Id });
26: response.Headers.Location = new Uri(uri);
27:
28: return response;
29: }
30: }
Notice the controller uses dependency injection to inject an IProductRepository
. That makes the controller more testable, because you can inject a mock repository. The following unit test verifies that the Get
method writes a Product
to the response body. Assume that repository
is a mock IProductRepository
.
[!code-csharpMain]
1: [TestMethod]
2: public void GetReturnsProduct()
3: {
4: // Arrange
5: var controller = new ProductsController(repository);
6: controller.Request = new HttpRequestMessage();
7: controller.Configuration = new HttpConfiguration();
8:
9: // Act
10: var response = controller.Get(10);
11:
12: // Assert
13: Product product;
14: Assert.IsTrue(response.TryGetContentValue<Product>(out product));
15: Assert.AreEqual(10, product.Id);
16: }
It’s important to set Request and Configuration on the controller. Otherwise, the test will fail with an ArgumentNullException or InvalidOperationException.
Testing Link Generation
The Post
method calls UrlHelper.Link to create links in the response. This requires a little more setup in the unit test:
[!code-csharpMain]
1: [TestMethod]
2: public void PostSetsLocationHeader()
3: {
4: // Arrange
5: ProductsController controller = new ProductsController(repository);
6:
7: controller.Request = new HttpRequestMessage {
8: RequestUri = new Uri("http://localhost/api/products")
9: };
10: controller.Configuration = new HttpConfiguration();
11: controller.Configuration.Routes.MapHttpRoute(
12: name: "DefaultApi",
13: routeTemplate: "api/{controller}/{id}",
14: defaults: new { id = RouteParameter.Optional });
15:
16: controller.RequestContext.RouteData = new HttpRouteData(
17: route: new HttpRoute(),
18: values: new HttpRouteValueDictionary { { "controller", "products" } });
19:
20: // Act
21: Product product = new Product() { Id = 42, Name = "Product1" };
22: var response = controller.Post(product);
23:
24: // Assert
25: Assert.AreEqual("http://localhost/api/products/42", response.Headers.Location.AbsoluteUri);
26: }
The UrlHelper class needs the request URL and route data, so the test has to set values for these. Another option is mock or stub UrlHelper. With this approach, you replace the default value of ApiController.Url with a mock or stub version that returns a fixed value.
Let’s rewrite the test using the Moq framework. Install the Moq
NuGet package in the test project.
[!code-csharpMain]
1: [TestMethod]
2: public void PostSetsLocationHeader_MockVersion()
3: {
4: // This version uses a mock UrlHelper.
5:
6: // Arrange
7: ProductsController controller = new ProductsController(repository);
8: controller.Request = new HttpRequestMessage();
9: controller.Configuration = new HttpConfiguration();
10:
11: string locationUrl = "http://location/";
12:
13: // Create the mock and set up the Link method, which is used to create the Location header.
14: // The mock version returns a fixed string.
15: var mockUrlHelper = new Mock<UrlHelper>();
16: mockUrlHelper.Setup(x => x.Link(It.IsAny<string>(), It.IsAny<object>())).Returns(locationUrl);
17: controller.Url = mockUrlHelper.Object;
18:
19: // Act
20: Product product = new Product() { Id = 42 };
21: var response = controller.Post(product);
22:
23: // Assert
24: Assert.AreEqual(locationUrl, response.Headers.Location.AbsoluteUri);
25: }
In this version, you don’t need to set up any route data, because the mock UrlHelper returns a constant string.
Testing Actions that Return IHttpActionResult
In Web API 2, a controller action can return IHttpActionResult, which is analogous to ActionResult in ASP.NET MVC. The IHttpActionResult interface defines a command pattern for creating HTTP responses. Instead of creating the response directly, the controller returns an IHttpActionResult. Later, the pipeline invokes the IHttpActionResult to create the response. This approach makes it easier to write unit tests, because you can skip a lot of the setup that is needed for HttpResponseMessage.
Here is an example controller whose actions return IHttpActionResult.
[!code-csharpMain]
1: public class Products2Controller : ApiController
2: {
3: IProductRepository _repository;
4:
5: public Products2Controller(IProductRepository repository)
6: {
7: _repository = repository;
8: }
9:
10: public IHttpActionResult Get(int id)
11: {
12: Product product = _repository.GetById(id);
13: if (product == null)
14: {
15: return NotFound();
16: }
17: return Ok(product);
18: }
19:
20: public IHttpActionResult Post(Product product)
21: {
22: _repository.Add(product);
23: return CreatedAtRoute("DefaultApi", new { id = product.Id }, product);
24: }
25:
26: public IHttpActionResult Delete(int id)
27: {
28: _repository.Delete(id);
29: return Ok();
30: }
31:
32: public IHttpActionResult Put(Product product)
33: {
34: // Do some work (not shown).
35: return Content(HttpStatusCode.Accepted, product);
36: }
37: }
This example shows some common patterns using IHttpActionResult. Let’s see how to unit test them.
Action returns 200 (OK) with a response body
The Get
method calls Ok(product)
if the product is found. In the unit test, make sure the return type is OkNegotiatedContentResult and the returned product has the right ID.
[!code-csharpMain]
1: [TestMethod]
2: public void GetReturnsProductWithSameId()
3: {
4: // Arrange
5: var mockRepository = new Mock<IProductRepository>();
6: mockRepository.Setup(x => x.GetById(42))
7: .Returns(new Product { Id = 42 });
8:
9: var controller = new Products2Controller(mockRepository.Object);
10:
11: // Act
12: IHttpActionResult actionResult = controller.Get(42);
13: var contentResult = actionResult as OkNegotiatedContentResult<Product>;
14:
15: // Assert
16: Assert.IsNotNull(contentResult);
17: Assert.IsNotNull(contentResult.Content);
18: Assert.AreEqual(42, contentResult.Content.Id);
19: }
Notice that the unit test doesn’t execute the action result. You can assume the action result creates the HTTP response correctly. (That’s why the Web API framework has its own unit tests!)
Action returns 404 (Not Found)
The Get
method calls NotFound()
if the product is not found. For this case, the unit test just checks if the return type is NotFoundResult.
[!code-csharpMain]
1: [TestMethod]
2: public void GetReturnsNotFound()
3: {
4: // Arrange
5: var mockRepository = new Mock<IProductRepository>();
6: var controller = new Products2Controller(mockRepository.Object);
7:
8: // Act
9: IHttpActionResult actionResult = controller.Get(10);
10:
11: // Assert
12: Assert.IsInstanceOfType(actionResult, typeof(NotFoundResult));
13: }
Action returns 200 (OK) with no response body
The Delete
method calls Ok()
to return an empty HTTP 200 response. Like the previous example, the unit test checks the return type, in this case OkResult.
[!code-csharpMain]
1: [TestMethod]
2: public void DeleteReturnsOk()
3: {
4: // Arrange
5: var mockRepository = new Mock<IProductRepository>();
6: var controller = new Products2Controller(mockRepository.Object);
7:
8: // Act
9: IHttpActionResult actionResult = controller.Delete(10);
10:
11: // Assert
12: Assert.IsInstanceOfType(actionResult, typeof(OkResult));
13: }
Action returns 201 (Created) with a Location header
The Post
method calls CreatedAtRoute
to return an HTTP 201 response with a URI in the Location header. In the unit test, verify that the action sets the correct routing values.
[!code-csharpMain]
1: [TestMethod]
2: public void PostMethodSetsLocationHeader()
3: {
4: // Arrange
5: var mockRepository = new Mock<IProductRepository>();
6: var controller = new Products2Controller(mockRepository.Object);
7:
8: // Act
9: IHttpActionResult actionResult = controller.Post(new Product { Id = 10, Name = "Product1" });
10: var createdResult = actionResult as CreatedAtRouteNegotiatedContentResult<Product>;
11:
12: // Assert
13: Assert.IsNotNull(createdResult);
14: Assert.AreEqual("DefaultApi", createdResult.RouteName);
15: Assert.AreEqual(10, createdResult.RouteValues["id"]);
16: }
Action returns another 2xx with a response body
The Put
method calls Content
to return an HTTP 202 (Accepted) response with a response body. This case is similar to returning 200 (OK), but the unit test should also check the status code.
[!code-csharpMain]
1: [TestMethod]
2: public void PutReturnsContentResult()
3: {
4: // Arrange
5: var mockRepository = new Mock<IProductRepository>();
6: var controller = new Products2Controller(mockRepository.Object);
7:
8: // Act
9: IHttpActionResult actionResult = controller.Put(new Product { Id = 10, Name = "Product" });
10: var contentResult = actionResult as NegotiatedContentResult<Product>;
11:
12: // Assert
13: Assert.IsNotNull(contentResult);
14: Assert.AreEqual(HttpStatusCode.Accepted, contentResult.StatusCode);
15: Assert.IsNotNull(contentResult.Content);
16: Assert.AreEqual(10, contentResult.Content.Id);
17: }
Additional Resources
- Mocking Entity Framework when Unit Testing ASP.NET Web API 2
- Writing tests for an ASP.NET Web API service (blog post by Youssef Moussaoui).
- Debugging ASP.NET Web API with Route Debugger
|