Writing Unit Tests for ASP.NET Web API Controller

In this blog post, I will write unit tests for a ASP.NET Web API controller in the EFMVC reference application. Let me introduce the EFMVC app, If you haven't heard about EFMVC. EFMVC is a simple app, developed as a reference implementation for demonstrating ASP.NET MVC, EF Code First, ASP.NET Web API, Domain-Driven Design (DDD), Test-Driven Development (DDD). The current version is built with ASP.NET MVC 4, EF Code First 5, ASP.NET Web API, Autofac, AutoMapper, Nunit and Moq. All unit tests were written with Nunit and Moq. You can download the latest version of the reference app from http://efmvc.codeplex.com/

Unit Test for HTTP Get

Let’s write a unit test class for verifying the behaviour of a ASP.NET Web API controller named CategoryController. Let’s define mock implementation for Repository class, and a Command Bus that is used for executing write operations. 

  1. [TestFixture]
  2. public class CategoryApiControllerTest
  3. {
  4. private Mock<ICategoryRepository> categoryRepository;
  5. private Mock<ICommandBus> commandBus;
  6. [SetUp]
  7. public void SetUp()
  8. {
  9.     categoryRepository = new Mock<ICategoryRepository>();
  10.     commandBus = new Mock<ICommandBus>();
  11. }

The code block below provides the unit test for a HTTP Get operation.

  1. [Test]
  2. public void Get_All_Returns_AllCategory()
  3. {
  4.     // Arrange   
  5.     IEnumerable<CategoryWithExpense> fakeCategories = GetCategories();
  6.     categoryRepository.Setup(x => x.GetCategoryWithExpenses()).Returns(fakeCategories);
  7.     CategoryController controller = new CategoryController(commandBus.Object, categoryRepository.Object)
  8.     {
  9.         Request = new HttpRequestMessage()
  10.                 {
  11.                     Properties = { { HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration() } }
  12.                 }
  13.     };
  14.     // Act
  15.     var categories = controller.Get();
  16.     // Assert
  17.     Assert.IsNotNull(categories, "Result is null");
  18.     Assert.IsInstanceOf(typeof(IEnumerable<CategoryWithExpense>),categories, "Wrong Model");        
  19.     Assert.AreEqual(3, categories.Count(), "Got wrong number of Categories");
  20. }       

The GetCategories method is provided below:

  1. private static IEnumerable<CategoryWithExpense> GetCategories()
  2. {
  3.     IEnumerable<CategoryWithExpense> fakeCategories = new List<CategoryWithExpense> {
  4.     new CategoryWithExpense {CategoryId=1, CategoryName = "Test1", Description="Test1Desc", TotalExpenses=1000},
  5.     new CategoryWithExpense {CategoryId=2, CategoryName = "Test2", Description="Test2Desc",TotalExpenses=2000},
  6.     new CategoryWithExpense { CategoryId=3, CategoryName = "Test3", Description="Test3Desc",TotalExpenses=3000}  
  7.     }.AsEnumerable();
  8.     return fakeCategories;
  9. }

In the unit test method Get_All_Returns_AllCategory, we specify setup on the mocked type ICategoryrepository, for a call to GetCategoryWithExpenses method returns dummy data. We create an instance of the ApiController, where we have specified the Request property of the ApiController since the Request property is used to create a new HttpResponseMessage that will provide the appropriate HTTP status code along with response content data. Unit Tests are using for specifying the behaviour of components so that we have specified that Get operation will use the model type IEnumerable<CategoryWithExpense> for sending the Content data.

The implementation of HTTP Get in the CategoryController is provided below:

  1. public IQueryable<CategoryWithExpense> Get()
  2. {
  3.     var categories = categoryRepository.GetCategoryWithExpenses().AsQueryable();
  4.     return categories;
  5. }

Unit Test for HTTP Post

The following are the behaviours we are going to implement for the HTTP Post:

  1. A successful HTTP Post  operation should return HTTP status code Created
  2. An empty Category should return HTTP status code BadRequest
  3. A successful HTTP Post operation should provide correct Location header information in the response for the newly created resource.

Writing unit test for HTTP Post is required more information than we write for HTTP Get. In the HTTP Post implementation, we will call to Url.Link for specifying the header Location of Response as shown in below code block.

  1. var response = Request.CreateResponse(HttpStatusCode.Created, category);
  2. string uri = Url.Link("DefaultApi", new { id = category.CategoryId });
  3. response.Headers.Location = new Uri(uri);
  4. return response;

While we are executing Url.Link from unit tests, we have to specify HttpRouteData information from the unit test method. Otherwise, Url.Link will get a null value.

The code block below shows the unit tests for specifying the behaviours for the HTTP Post operation.

  1. [Test]
  2. public void Post_Category_Returns_CreatedStatusCode()
  3. {
  4.     // Arrange   
  5.     commandBus.Setup(c => c.Submit(It.IsAny<CreateOrUpdateCategoryCommand>())).Returns(new CommandResult(true));
  6.     Mapper.CreateMap<CategoryFormModel, CreateOrUpdateCategoryCommand>();     
  7.     var httpConfiguration = new HttpConfiguration();
  8.     WebApiConfig.Register(httpConfiguration);
  9.     var httpRouteData = new HttpRouteData(httpConfiguration.Routes["DefaultApi"],
  10.         new HttpRouteValueDictionary { { "controller", "category" } });
  11.     var controller = new CategoryController(commandBus.Object, categoryRepository.Object)
  12.     {
  13.         Request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/api/category/")
  14.         {
  15.             Properties =
  16.             {
  17.                 { HttpPropertyKeys.HttpConfigurationKey, httpConfiguration },
  18.                 { HttpPropertyKeys.HttpRouteDataKey, httpRouteData }
  19.             }
  20.         }
  21.     };
  22.     // Act
  23.     CategoryModel category = new CategoryModel();
  24.     category.CategoryId = 1;
  25.     category.CategoryName = "Mock Category";
  26.     var response = controller.Post(category);          
  27.     // Assert
  28.     Assert.AreEqual(HttpStatusCode.Created, response.StatusCode);
  29.     var newCategory = JsonConvert.DeserializeObject<CategoryModel>(response.Content.ReadAsStringAsync().Result);
  30.     Assert.AreEqual(string.Format("http://localhost/api/category/{0}", newCategory.CategoryId), response.Headers.Location.ToString());
  31. }
  32. [Test]
  33. public void Post_EmptyCategory_Returns_BadRequestStatusCode()
  34. {
  35.     // Arrange   
  36.     commandBus.Setup(c => c.Submit(It.IsAny<CreateOrUpdateCategoryCommand>())).Returns(new CommandResult(true));
  37.     Mapper.CreateMap<CategoryFormModel, CreateOrUpdateCategoryCommand>();
  38.     var httpConfiguration = new HttpConfiguration();
  39.     WebApiConfig.Register(httpConfiguration);
  40.     var httpRouteData = new HttpRouteData(httpConfiguration.Routes["DefaultApi"],
  41.         new HttpRouteValueDictionary { { "controller", "category" } });
  42.     var controller = new CategoryController(commandBus.Object, categoryRepository.Object)
  43.     {
  44.         Request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/api/category/")
  45.         {
  46.             Properties =
  47.             {
  48.                 { HttpPropertyKeys.HttpConfigurationKey, httpConfiguration },
  49.                 { HttpPropertyKeys.HttpRouteDataKey, httpRouteData }
  50.             }
  51.         }
  52.     };
  53.     // Act
  54.     CategoryModel category = new CategoryModel();
  55.     category.CategoryId = 0;
  56.     category.CategoryName = "";
  57.     // The ASP.NET pipeline doesn't run, so validation don't run.
  58.     controller.ModelState.AddModelError("", "mock error message");
  59.     var response = controller.Post(category);
  60.     // Assert
  61.     Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
  62.  
  63. }

In the above code block, we have written two unit methods, Post_Category_Returns_CreatedStatusCode and Post_EmptyCategory_Returns_BadRequestStatusCode. The unit test method Post_Category_Returns_CreatedStatusCode  verifies the behaviour 1 and 3, that we have defined in the beginning of the section “Unit Test for HTTP Post”. The unit test method Post_EmptyCategory_Returns_BadRequestStatusCode verifies the behaviour 2. For extracting the data from response, we call Content.ReadAsStringAsync().Result of HttpResponseMessage object and deserializeit it with Json Convertor.

The implementation of HTTP Post in the CategoryController is provided below:

  1. // POST /api/category
  2. public HttpResponseMessage Post(CategoryModel category)
  3. {
  4.  
  5.     if (ModelState.IsValid)
  6.     {
  7.         var command = new CreateOrUpdateCategoryCommand(category.CategoryId, category.CategoryName, category.Description);
  8.         var result = commandBus.Submit(command);
  9.         if (result.Success)
  10.         {                  
  11.             var response = Request.CreateResponse(HttpStatusCode.Created, category);
  12.             string uri = Url.Link("DefaultApi", new { id = category.CategoryId });
  13.             response.Headers.Location = new Uri(uri);
  14.             return response;
  15.         }
  16.     }
  17.     else
  18.     {
  19.         return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
  20.     }
  21.     throw new HttpResponseException(HttpStatusCode.BadRequest);
  22. }

The unit test implementation for HTTP Put and HTTP Delete are very similar to the unit test we have written for  HTTP Get.

The complete unit tests for the CategoryController is given below:

  1. [TestFixture]
  2. public class CategoryApiControllerTest
  3. {
  4. private Mock<ICategoryRepository> categoryRepository;
  5. private Mock<ICommandBus> commandBus;
  6. [SetUp]
  7. public void SetUp()
  8. {
  9.     categoryRepository = new Mock<ICategoryRepository>();
  10.     commandBus = new Mock<ICommandBus>();
  11. }
  12. [Test]
  13. public void Get_All_Returns_AllCategory()
  14. {
  15.     // Arrange   
  16.     IEnumerable<CategoryWithExpense> fakeCategories = GetCategories();
  17.     categoryRepository.Setup(x => x.GetCategoryWithExpenses()).Returns(fakeCategories);
  18.     CategoryController controller = new CategoryController(commandBus.Object, categoryRepository.Object)
  19.     {
  20.         Request = new HttpRequestMessage()
  21.                 {
  22.                     Properties = { { HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration() } }
  23.                 }
  24.     };
  25.     // Act
  26.     var categories = controller.Get();
  27.     // Assert
  28.     Assert.IsNotNull(categories, "Result is null");
  29.     Assert.IsInstanceOf(typeof(IEnumerable<CategoryWithExpense>),categories, "Wrong Model");        
  30.     Assert.AreEqual(3, categories.Count(), "Got wrong number of Categories");
  31. }       
  32. [Test]
  33. public void Get_CorrectCategoryId_Returns_Category()
  34. {
  35.     // Arrange   
  36.     IEnumerable<CategoryWithExpense> fakeCategories = GetCategories();
  37.     categoryRepository.Setup(x => x.GetCategoryWithExpenses()).Returns(fakeCategories);
  38.     CategoryController controller = new CategoryController(commandBus.Object, categoryRepository.Object)
  39.     {
  40.         Request = new HttpRequestMessage()
  41.         {
  42.             Properties = { { HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration() } }
  43.         }
  44.     };
  45.     // Act
  46.     var response = controller.Get(1);
  47.     // Assert
  48.     Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
  49.     var category = JsonConvert.DeserializeObject<CategoryWithExpense>(response.Content.ReadAsStringAsync().Result);
  50.     Assert.AreEqual(1, category.CategoryId, "Got wrong number of Categories");
  51. }
  52. [Test]
  53. public void Get_InValidCategoryId_Returns_NotFound()
  54. {
  55.     // Arrange   
  56.     IEnumerable<CategoryWithExpense> fakeCategories = GetCategories();
  57.     categoryRepository.Setup(x => x.GetCategoryWithExpenses()).Returns(fakeCategories);
  58.     CategoryController controller = new CategoryController(commandBus.Object, categoryRepository.Object)
  59.     {
  60.         Request = new HttpRequestMessage()
  61.         {
  62.             Properties = { { HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration() } }
  63.         }
  64.     };
  65.     // Act
  66.     var response = controller.Get(5);
  67.     // Assert
  68.     Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode);
  69.           
  70. }
  71. [Test]
  72. public void Post_Category_Returns_CreatedStatusCode()
  73. {
  74.     // Arrange   
  75.     commandBus.Setup(c => c.Submit(It.IsAny<CreateOrUpdateCategoryCommand>())).Returns(new CommandResult(true));
  76.     Mapper.CreateMap<CategoryFormModel, CreateOrUpdateCategoryCommand>();     
  77.     var httpConfiguration = new HttpConfiguration();
  78.     WebApiConfig.Register(httpConfiguration);
  79.     var httpRouteData = new HttpRouteData(httpConfiguration.Routes["DefaultApi"],
  80.         new HttpRouteValueDictionary { { "controller", "category" } });
  81.     var controller = new CategoryController(commandBus.Object, categoryRepository.Object)
  82.     {
  83.         Request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/api/category/")
  84.         {
  85.             Properties =
  86.             {
  87.                 { HttpPropertyKeys.HttpConfigurationKey, httpConfiguration },
  88.                 { HttpPropertyKeys.HttpRouteDataKey, httpRouteData }
  89.             }
  90.         }
  91.     };
  92.     // Act
  93.     CategoryModel category = new CategoryModel();
  94.     category.CategoryId = 1;
  95.     category.CategoryName = "Mock Category";
  96.     var response = controller.Post(category);          
  97.     // Assert
  98.     Assert.AreEqual(HttpStatusCode.Created, response.StatusCode);
  99.     var newCategory = JsonConvert.DeserializeObject<CategoryModel>(response.Content.ReadAsStringAsync().Result);
  100.     Assert.AreEqual(string.Format("http://localhost/api/category/{0}", newCategory.CategoryId), response.Headers.Location.ToString());
  101. }
  102. [Test]
  103. public void Post_EmptyCategory_Returns_BadRequestStatusCode()
  104. {
  105.     // Arrange   
  106.     commandBus.Setup(c => c.Submit(It.IsAny<CreateOrUpdateCategoryCommand>())).Returns(new CommandResult(true));
  107.     Mapper.CreateMap<CategoryFormModel, CreateOrUpdateCategoryCommand>();
  108.     var httpConfiguration = new HttpConfiguration();
  109.     WebApiConfig.Register(httpConfiguration);
  110.     var httpRouteData = new HttpRouteData(httpConfiguration.Routes["DefaultApi"],
  111.         new HttpRouteValueDictionary { { "controller", "category" } });
  112.     var controller = new CategoryController(commandBus.Object, categoryRepository.Object)
  113.     {
  114.         Request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/api/category/")
  115.         {
  116.             Properties =
  117.             {
  118.                 { HttpPropertyKeys.HttpConfigurationKey, httpConfiguration },
  119.                 { HttpPropertyKeys.HttpRouteDataKey, httpRouteData }
  120.             }
  121.         }
  122.     };
  123.     // Act
  124.     CategoryModel category = new CategoryModel();
  125.     category.CategoryId = 0;
  126.     category.CategoryName = "";
  127.     // The ASP.NET pipeline doesn't run, so validation don't run.
  128.     controller.ModelState.AddModelError("", "mock error message");
  129.     var response = controller.Post(category);
  130.     // Assert
  131.     Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
  132.  
  133. }
  134. [Test]
  135. public void Put_Category_Returns_OKStatusCode()
  136. {
  137.     // Arrange   
  138.     commandBus.Setup(c => c.Submit(It.IsAny<CreateOrUpdateCategoryCommand>())).Returns(new CommandResult(true));
  139.     Mapper.CreateMap<CategoryFormModel, CreateOrUpdateCategoryCommand>();
  140.     CategoryController controller = new CategoryController(commandBus.Object, categoryRepository.Object)
  141.     {
  142.         Request = new HttpRequestMessage()
  143.         {
  144.             Properties = { { HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration() } }
  145.         }
  146.     };
  147.     // Act
  148.     CategoryModel category = new CategoryModel();
  149.     category.CategoryId = 1;
  150.     category.CategoryName = "Mock Category";
  151.     var response = controller.Put(category.CategoryId,category);
  152.     // Assert
  153.     Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);   
  154. }
  155. [Test]
  156. public void Delete_Category_Returns_NoContentStatusCode()
  157. {
  158.     // Arrange         
  159.     commandBus.Setup(c => c.Submit(It.IsAny<DeleteCategoryCommand >())).Returns(new CommandResult(true));
  160.     CategoryController controller = new CategoryController(commandBus.Object, categoryRepository.Object)
  161.     {
  162.         Request = new HttpRequestMessage()
  163.         {
  164.             Properties = { { HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration() } }
  165.         }
  166.     };
  167.     // Act          
  168.     var response = controller.Delete(1);
  169.     // Assert
  170.     Assert.AreEqual(HttpStatusCode.NoContent, response.StatusCode);
  171.  
  172. }
  173. private static IEnumerable<CategoryWithExpense> GetCategories()
  174. {
  175.     IEnumerable<CategoryWithExpense> fakeCategories = new List<CategoryWithExpense> {
  176.     new CategoryWithExpense {CategoryId=1, CategoryName = "Test1", Description="Test1Desc", TotalExpenses=1000},
  177.     new CategoryWithExpense {CategoryId=2, CategoryName = "Test2", Description="Test2Desc",TotalExpenses=2000},
  178.     new CategoryWithExpense { CategoryId=3, CategoryName = "Test3", Description="Test3Desc",TotalExpenses=3000}  
  179.     }.AsEnumerable();
  180.     return fakeCategories;
  181. }
  182. }

 The complete implementation for the Api Controller, CategoryController is given below:

  1. public class CategoryController : ApiController
  2. {
  3.  
  4.     private readonly ICommandBus commandBus;
  5.     private readonly ICategoryRepository categoryRepository;
  6.     public CategoryController(ICommandBus commandBus, ICategoryRepository categoryRepository)
  7.     {
  8.         this.commandBus = commandBus;
  9.         this.categoryRepository = categoryRepository;
  10.     }
  11. public IQueryable<CategoryWithExpense> Get()
  12. {
  13.     var categories = categoryRepository.GetCategoryWithExpenses().AsQueryable();
  14.     return categories;
  15. }
  16.  
  17. // GET /api/category/5
  18. public HttpResponseMessage Get(int id)
  19. {
  20.     var category = categoryRepository.GetCategoryWithExpenses().Where(c => c.CategoryId == id).SingleOrDefault();
  21.     if (category == null)
  22.     {
  23.         return Request.CreateResponse(HttpStatusCode.NotFound);
  24.     }
  25.     return Request.CreateResponse(HttpStatusCode.OK, category);
  26. }
  27.  
  28. // POST /api/category
  29. public HttpResponseMessage Post(CategoryModel category)
  30. {
  31.  
  32.     if (ModelState.IsValid)
  33.     {
  34.         var command = new CreateOrUpdateCategoryCommand(category.CategoryId, category.CategoryName, category.Description);
  35.         var result = commandBus.Submit(command);
  36.         if (result.Success)
  37.         {                  
  38.             var response = Request.CreateResponse(HttpStatusCode.Created, category);
  39.             string uri = Url.Link("DefaultApi", new { id = category.CategoryId });
  40.             response.Headers.Location = new Uri(uri);
  41.             return response;
  42.         }
  43.     }
  44.     else
  45.     {
  46.         return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
  47.     }
  48.     throw new HttpResponseException(HttpStatusCode.BadRequest);
  49. }
  50.  
  51. // PUT /api/category/5
  52. public HttpResponseMessage Put(int id, CategoryModel category)
  53. {
  54.     if (ModelState.IsValid)
  55.     {
  56.         var command = new CreateOrUpdateCategoryCommand(category.CategoryId, category.CategoryName, category.Description);
  57.         var result = commandBus.Submit(command);
  58.         return Request.CreateResponse(HttpStatusCode.OK, category);
  59.     }
  60.     else
  61.     {
  62.         return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
  63.     }
  64.     throw new HttpResponseException(HttpStatusCode.BadRequest);
  65. }
  66.  
  67.     // DELETE /api/category/5
  68.     public HttpResponseMessage Delete(int id)
  69.     {
  70.         var command = new DeleteCategoryCommand { CategoryId = id };
  71.         var result = commandBus.Submit(command);
  72.         if (result.Success)
  73.         {
  74.             return new HttpResponseMessage(HttpStatusCode.NoContent);
  75.         }
  76.             throw new HttpResponseException(HttpStatusCode.BadRequest);
  77.     }
  78. }

Source Code

The EFMVC app can download from http://efmvc.codeplex.com/ . The unit test project can be found from the project EFMVC.Tests and Web API project can be found from EFMVC.Web.API.

Published Tuesday, July 30, 2013 10:15 AM by shiju

Comments

# re: Writing Unit Tests for ASP.NET Web API Controller

Wednesday, July 31, 2013 4:35 PM by mxmissile

Not seeing this stuff in the repository... efmvc.codeplex.com/.../latest

# re: Writing Unit Tests for ASP.NET Web API Controller

Wednesday, July 31, 2013 11:45 PM by shiju

@mxmissile - Please download it from efmvc.codeplex.com/.../74870

# re: Writing Unit Tests for ASP.NET Web API Controller

Saturday, August 31, 2013 1:28 PM by abatishchev

Rare case when api controller testing mentions model validation . Thank you!

Leave a Comment

(required) 
(required) 
(optional)
(required)