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.
- [TestFixture]
- public class CategoryApiControllerTest
- {
- private Mock<ICategoryRepository> categoryRepository;
- private Mock<ICommandBus> commandBus;
- [SetUp]
- public void SetUp()
- {
- categoryRepository = new Mock<ICategoryRepository>();
- commandBus = new Mock<ICommandBus>();
- }
The code block below provides the unit test for a HTTP Get operation.
- [Test]
- public void Get_All_Returns_AllCategory()
- {
- // Arrange
- IEnumerable<CategoryWithExpense> fakeCategories = GetCategories();
- categoryRepository.Setup(x => x.GetCategoryWithExpenses()).Returns(fakeCategories);
- CategoryController controller = new CategoryController(commandBus.Object, categoryRepository.Object)
- {
- Request = new HttpRequestMessage()
- {
- Properties = { { HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration() } }
- }
- };
- // Act
- var categories = controller.Get();
- // Assert
- Assert.IsNotNull(categories, "Result is null");
- Assert.IsInstanceOf(typeof(IEnumerable<CategoryWithExpense>),categories, "Wrong Model");
- Assert.AreEqual(3, categories.Count(), "Got wrong number of Categories");
- }
The GetCategories method is provided below:
- private static IEnumerable<CategoryWithExpense> GetCategories()
- {
- IEnumerable<CategoryWithExpense> fakeCategories = new List<CategoryWithExpense> {
- new CategoryWithExpense {CategoryId=1, CategoryName = "Test1", Description="Test1Desc", TotalExpenses=1000},
- new CategoryWithExpense {CategoryId=2, CategoryName = "Test2", Description="Test2Desc",TotalExpenses=2000},
- new CategoryWithExpense { CategoryId=3, CategoryName = "Test3", Description="Test3Desc",TotalExpenses=3000}
- }.AsEnumerable();
- return fakeCategories;
- }
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:
- public IQueryable<CategoryWithExpense> Get()
- {
- var categories = categoryRepository.GetCategoryWithExpenses().AsQueryable();
- return categories;
- }
Unit Test for HTTP Post
The following are the behaviours we are going to implement for the HTTP Post:
- A successful HTTP Post operation should return HTTP status code Created
- An empty Category should return HTTP status code BadRequest
- 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.
- var response = Request.CreateResponse(HttpStatusCode.Created, category);
- string uri = Url.Link("DefaultApi", new { id = category.CategoryId });
- response.Headers.Location = new Uri(uri);
- 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.
- [Test]
- public void Post_Category_Returns_CreatedStatusCode()
- {
- // Arrange
- commandBus.Setup(c => c.Submit(It.IsAny<CreateOrUpdateCategoryCommand>())).Returns(new CommandResult(true));
- Mapper.CreateMap<CategoryFormModel, CreateOrUpdateCategoryCommand>();
- var httpConfiguration = new HttpConfiguration();
- WebApiConfig.Register(httpConfiguration);
- var httpRouteData = new HttpRouteData(httpConfiguration.Routes["DefaultApi"],
- new HttpRouteValueDictionary { { "controller", "category" } });
- var controller = new CategoryController(commandBus.Object, categoryRepository.Object)
- {
- Request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/api/category/")
- {
- Properties =
- {
- { HttpPropertyKeys.HttpConfigurationKey, httpConfiguration },
- { HttpPropertyKeys.HttpRouteDataKey, httpRouteData }
- }
- }
- };
- // Act
- CategoryModel category = new CategoryModel();
- category.CategoryId = 1;
- category.CategoryName = "Mock Category";
- var response = controller.Post(category);
- // Assert
- Assert.AreEqual(HttpStatusCode.Created, response.StatusCode);
- var newCategory = JsonConvert.DeserializeObject<CategoryModel>(response.Content.ReadAsStringAsync().Result);
- Assert.AreEqual(string.Format("http://localhost/api/category/{0}", newCategory.CategoryId), response.Headers.Location.ToString());
- }
- [Test]
- public void Post_EmptyCategory_Returns_BadRequestStatusCode()
- {
- // Arrange
- commandBus.Setup(c => c.Submit(It.IsAny<CreateOrUpdateCategoryCommand>())).Returns(new CommandResult(true));
- Mapper.CreateMap<CategoryFormModel, CreateOrUpdateCategoryCommand>();
- var httpConfiguration = new HttpConfiguration();
- WebApiConfig.Register(httpConfiguration);
- var httpRouteData = new HttpRouteData(httpConfiguration.Routes["DefaultApi"],
- new HttpRouteValueDictionary { { "controller", "category" } });
- var controller = new CategoryController(commandBus.Object, categoryRepository.Object)
- {
- Request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/api/category/")
- {
- Properties =
- {
- { HttpPropertyKeys.HttpConfigurationKey, httpConfiguration },
- { HttpPropertyKeys.HttpRouteDataKey, httpRouteData }
- }
- }
- };
- // Act
- CategoryModel category = new CategoryModel();
- category.CategoryId = 0;
- category.CategoryName = "";
- // The ASP.NET pipeline doesn't run, so validation don't run.
- controller.ModelState.AddModelError("", "mock error message");
- var response = controller.Post(category);
- // Assert
- Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
- }
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:
- // POST /api/category
- public HttpResponseMessage Post(CategoryModel category)
- {
- if (ModelState.IsValid)
- {
- var command = new CreateOrUpdateCategoryCommand(category.CategoryId, category.CategoryName, category.Description);
- var result = commandBus.Submit(command);
- if (result.Success)
- {
- var response = Request.CreateResponse(HttpStatusCode.Created, category);
- string uri = Url.Link("DefaultApi", new { id = category.CategoryId });
- response.Headers.Location = new Uri(uri);
- return response;
- }
- }
- else
- {
- return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
- }
- throw new HttpResponseException(HttpStatusCode.BadRequest);
- }
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:
- [TestFixture]
- public class CategoryApiControllerTest
- {
- private Mock<ICategoryRepository> categoryRepository;
- private Mock<ICommandBus> commandBus;
- [SetUp]
- public void SetUp()
- {
- categoryRepository = new Mock<ICategoryRepository>();
- commandBus = new Mock<ICommandBus>();
- }
- [Test]
- public void Get_All_Returns_AllCategory()
- {
- // Arrange
- IEnumerable<CategoryWithExpense> fakeCategories = GetCategories();
- categoryRepository.Setup(x => x.GetCategoryWithExpenses()).Returns(fakeCategories);
- CategoryController controller = new CategoryController(commandBus.Object, categoryRepository.Object)
- {
- Request = new HttpRequestMessage()
- {
- Properties = { { HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration() } }
- }
- };
- // Act
- var categories = controller.Get();
- // Assert
- Assert.IsNotNull(categories, "Result is null");
- Assert.IsInstanceOf(typeof(IEnumerable<CategoryWithExpense>),categories, "Wrong Model");
- Assert.AreEqual(3, categories.Count(), "Got wrong number of Categories");
- }
- [Test]
- public void Get_CorrectCategoryId_Returns_Category()
- {
- // Arrange
- IEnumerable<CategoryWithExpense> fakeCategories = GetCategories();
- categoryRepository.Setup(x => x.GetCategoryWithExpenses()).Returns(fakeCategories);
- CategoryController controller = new CategoryController(commandBus.Object, categoryRepository.Object)
- {
- Request = new HttpRequestMessage()
- {
- Properties = { { HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration() } }
- }
- };
- // Act
- var response = controller.Get(1);
- // Assert
- Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
- var category = JsonConvert.DeserializeObject<CategoryWithExpense>(response.Content.ReadAsStringAsync().Result);
- Assert.AreEqual(1, category.CategoryId, "Got wrong number of Categories");
- }
- [Test]
- public void Get_InValidCategoryId_Returns_NotFound()
- {
- // Arrange
- IEnumerable<CategoryWithExpense> fakeCategories = GetCategories();
- categoryRepository.Setup(x => x.GetCategoryWithExpenses()).Returns(fakeCategories);
- CategoryController controller = new CategoryController(commandBus.Object, categoryRepository.Object)
- {
- Request = new HttpRequestMessage()
- {
- Properties = { { HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration() } }
- }
- };
- // Act
- var response = controller.Get(5);
- // Assert
- Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode);
- }
- [Test]
- public void Post_Category_Returns_CreatedStatusCode()
- {
- // Arrange
- commandBus.Setup(c => c.Submit(It.IsAny<CreateOrUpdateCategoryCommand>())).Returns(new CommandResult(true));
- Mapper.CreateMap<CategoryFormModel, CreateOrUpdateCategoryCommand>();
- var httpConfiguration = new HttpConfiguration();
- WebApiConfig.Register(httpConfiguration);
- var httpRouteData = new HttpRouteData(httpConfiguration.Routes["DefaultApi"],
- new HttpRouteValueDictionary { { "controller", "category" } });
- var controller = new CategoryController(commandBus.Object, categoryRepository.Object)
- {
- Request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/api/category/")
- {
- Properties =
- {
- { HttpPropertyKeys.HttpConfigurationKey, httpConfiguration },
- { HttpPropertyKeys.HttpRouteDataKey, httpRouteData }
- }
- }
- };
- // Act
- CategoryModel category = new CategoryModel();
- category.CategoryId = 1;
- category.CategoryName = "Mock Category";
- var response = controller.Post(category);
- // Assert
- Assert.AreEqual(HttpStatusCode.Created, response.StatusCode);
- var newCategory = JsonConvert.DeserializeObject<CategoryModel>(response.Content.ReadAsStringAsync().Result);
- Assert.AreEqual(string.Format("http://localhost/api/category/{0}", newCategory.CategoryId), response.Headers.Location.ToString());
- }
- [Test]
- public void Post_EmptyCategory_Returns_BadRequestStatusCode()
- {
- // Arrange
- commandBus.Setup(c => c.Submit(It.IsAny<CreateOrUpdateCategoryCommand>())).Returns(new CommandResult(true));
- Mapper.CreateMap<CategoryFormModel, CreateOrUpdateCategoryCommand>();
- var httpConfiguration = new HttpConfiguration();
- WebApiConfig.Register(httpConfiguration);
- var httpRouteData = new HttpRouteData(httpConfiguration.Routes["DefaultApi"],
- new HttpRouteValueDictionary { { "controller", "category" } });
- var controller = new CategoryController(commandBus.Object, categoryRepository.Object)
- {
- Request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/api/category/")
- {
- Properties =
- {
- { HttpPropertyKeys.HttpConfigurationKey, httpConfiguration },
- { HttpPropertyKeys.HttpRouteDataKey, httpRouteData }
- }
- }
- };
- // Act
- CategoryModel category = new CategoryModel();
- category.CategoryId = 0;
- category.CategoryName = "";
- // The ASP.NET pipeline doesn't run, so validation don't run.
- controller.ModelState.AddModelError("", "mock error message");
- var response = controller.Post(category);
- // Assert
- Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
- }
- [Test]
- public void Put_Category_Returns_OKStatusCode()
- {
- // Arrange
- commandBus.Setup(c => c.Submit(It.IsAny<CreateOrUpdateCategoryCommand>())).Returns(new CommandResult(true));
- Mapper.CreateMap<CategoryFormModel, CreateOrUpdateCategoryCommand>();
- CategoryController controller = new CategoryController(commandBus.Object, categoryRepository.Object)
- {
- Request = new HttpRequestMessage()
- {
- Properties = { { HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration() } }
- }
- };
- // Act
- CategoryModel category = new CategoryModel();
- category.CategoryId = 1;
- category.CategoryName = "Mock Category";
- var response = controller.Put(category.CategoryId,category);
- // Assert
- Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
- }
- [Test]
- public void Delete_Category_Returns_NoContentStatusCode()
- {
- // Arrange
- commandBus.Setup(c => c.Submit(It.IsAny<DeleteCategoryCommand >())).Returns(new CommandResult(true));
- CategoryController controller = new CategoryController(commandBus.Object, categoryRepository.Object)
- {
- Request = new HttpRequestMessage()
- {
- Properties = { { HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration() } }
- }
- };
- // Act
- var response = controller.Delete(1);
- // Assert
- Assert.AreEqual(HttpStatusCode.NoContent, response.StatusCode);
- }
- private static IEnumerable<CategoryWithExpense> GetCategories()
- {
- IEnumerable<CategoryWithExpense> fakeCategories = new List<CategoryWithExpense> {
- new CategoryWithExpense {CategoryId=1, CategoryName = "Test1", Description="Test1Desc", TotalExpenses=1000},
- new CategoryWithExpense {CategoryId=2, CategoryName = "Test2", Description="Test2Desc",TotalExpenses=2000},
- new CategoryWithExpense { CategoryId=3, CategoryName = "Test3", Description="Test3Desc",TotalExpenses=3000}
- }.AsEnumerable();
- return fakeCategories;
- }
- }
The complete implementation for the Api Controller, CategoryController is given below:
- public class CategoryController : ApiController
- {
- private readonly ICommandBus commandBus;
- private readonly ICategoryRepository categoryRepository;
- public CategoryController(ICommandBus commandBus, ICategoryRepository categoryRepository)
- {
- this.commandBus = commandBus;
- this.categoryRepository = categoryRepository;
- }
- public IQueryable<CategoryWithExpense> Get()
- {
- var categories = categoryRepository.GetCategoryWithExpenses().AsQueryable();
- return categories;
- }
- // GET /api/category/5
- public HttpResponseMessage Get(int id)
- {
- var category = categoryRepository.GetCategoryWithExpenses().Where(c => c.CategoryId == id).SingleOrDefault();
- if (category == null)
- {
- return Request.CreateResponse(HttpStatusCode.NotFound);
- }
- return Request.CreateResponse(HttpStatusCode.OK, category);
- }
- // POST /api/category
- public HttpResponseMessage Post(CategoryModel category)
- {
- if (ModelState.IsValid)
- {
- var command = new CreateOrUpdateCategoryCommand(category.CategoryId, category.CategoryName, category.Description);
- var result = commandBus.Submit(command);
- if (result.Success)
- {
- var response = Request.CreateResponse(HttpStatusCode.Created, category);
- string uri = Url.Link("DefaultApi", new { id = category.CategoryId });
- response.Headers.Location = new Uri(uri);
- return response;
- }
- }
- else
- {
- return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
- }
- throw new HttpResponseException(HttpStatusCode.BadRequest);
- }
- // PUT /api/category/5
- public HttpResponseMessage Put(int id, CategoryModel category)
- {
- if (ModelState.IsValid)
- {
- var command = new CreateOrUpdateCategoryCommand(category.CategoryId, category.CategoryName, category.Description);
- var result = commandBus.Submit(command);
- return Request.CreateResponse(HttpStatusCode.OK, category);
- }
- else
- {
- return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
- }
- throw new HttpResponseException(HttpStatusCode.BadRequest);
- }
- // DELETE /api/category/5
- public HttpResponseMessage Delete(int id)
- {
- var command = new DeleteCategoryCommand { CategoryId = id };
- var result = commandBus.Submit(command);
- if (result.Success)
- {
- return new HttpResponseMessage(HttpStatusCode.NoContent);
- }
- throw new HttpResponseException(HttpStatusCode.BadRequest);
- }
- }
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.