Unit Testing
Unit tests check one controller or service in isolation — no IIS, no database. With dependency injection and IHttpActionResult, Web API 2 controllers are easy to test.
On this page · 6 min · Advanced
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Create a test project for a Web API 2 project
- Mock dependencies with Moq
- Assert on IHttpActionResult types
- Simulate invalid ModelState
Explanation#
Add a test project (xUnit or MSTest) that targets .NET Framework 4.8 and references the API project, Moq and Microsoft.AspNet.WebApi.Core.
Create the controller with new and pass mocks: new ProductsController(mockService.Object). This only works because the controller depends on interfaces.
Each helper returns a specific type: Ok(x) → OkNegotiatedContentResult<T>, NotFound() → NotFoundResult, BadRequest(ModelState) → InvalidModelStateResult, CreatedAtRoute → CreatedAtRouteNegotiatedContentResult<T>. Assert.IsType checks them.
Data Annotations do not run when you call an action directly. Simulate invalid input with controller.ModelState.AddModelError(...).
Moq’s Verify checks interactions: that CreateAsync was never called when input was invalid.
Test services the same way, with mocked repositories. Leave real HTTP and database checks to integration tests.
Syntax#
var service = new Mock<IProductService>();
service.Setup(s => s.GetAsync(1)).ReturnsAsync(new ProductDto { Id = 1 });
var result = await new ProductsController(service.Object).Get(1);
Assert.IsType<OkNegotiatedContentResult<ProductDto>>(result);
Code example: Testing a products controller#
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Results;
using Moq;
using Xunit;
using ShopApi.Models;
using ShopApi.Services;
// ── The controller under test (in the API project) ──
public class ProductsController : ApiController
{
private readonly IProductService _service;
public ProductsController(IProductService service) { _service = service; }
[HttpGet, Route("api/products/{id:int}", Name = "GetProduct")]
public async Task<IHttpActionResult> Get(int id)
{
var product = await _service.GetAsync(id);
if (product == null) return NotFound();
return Ok(product);
}
[HttpPost, Route("api/products")]
public async Task<IHttpActionResult> Post(ProductDto dto)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
var created = await _service.CreateAsync(dto);
return CreatedAtRoute("GetProduct", new { id = created.Id }, created);
}
}
// ── The tests (in the test project) ──
public class ProductsControllerTests
{
private readonly Mock<IProductService> _service = new Mock<IProductService>();
private ProductsController CreateController() => new ProductsController(_service.Object);
[Fact]
public async Task Get_ExistingId_ReturnsOkWithProduct()
{
_service.Setup(s => s.GetAsync(1)).ReturnsAsync(new ProductDto { Id = 1, Name = "Pen" });
var result = await CreateController().Get(1);
var ok = Assert.IsType<OkNegotiatedContentResult<ProductDto>>(result);
Assert.Equal("Pen", ok.Content.Name);
}
[Fact]
public async Task Get_UnknownId_ReturnsNotFound()
{
_service.Setup(s => s.GetAsync(99)).ReturnsAsync((ProductDto)null);
var result = await CreateController().Get(99);
Assert.IsType<NotFoundResult>(result);
}
[Fact]
public async Task Post_InvalidModel_ReturnsBadRequest_AndDoesNotSave()
{
var controller = CreateController();
controller.ModelState.AddModelError("Name", "The Name field is required.");
var result = await controller.Post(new ProductDto());
Assert.IsType<InvalidModelStateResult>(result);
_service.Verify(s => s.CreateAsync(It.IsAny<ProductDto>()), Times.Never);
}
[Fact]
public async Task Post_ValidModel_ReturnsCreatedAtRoute()
{
_service.Setup(s => s.CreateAsync(It.IsAny<ProductDto>()))
.ReturnsAsync(new ProductDto { Id = 5, Name = "Bag" });
var result = await CreateController().Post(new ProductDto { Name = "Bag", Price = 650m });
var created = Assert.IsType<CreatedAtRouteNegotiatedContentResult<ProductDto>>(result);
Assert.Equal("GetProduct", created.RouteName);
Assert.Equal(5, created.RouteValues["id"]);
}
}
dotnet test runs the four tests in about a second, with no IIS and no database: Passed! - Failed: 0, Passed: 4, Skipped: 0, Total: 4.
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- The controller depends only on IProductService, so the tests pass a Moq mock.
- Setup defines what the mock returns for a given call; ReturnsAsync wraps it in a Task.
- Assert.IsType checks which IHttpActionResult the action chose, and gives access to its Content.
- AddModelError simulates what model binding would do for invalid input.
- Verify(..., Times.Never) proves invalid input never reaches the service.
Real-world use case#
A team runs 600 controller and service unit tests in under 10 seconds on every commit. They catch wrong status codes and missing validation long before a tester or customer does.
Common mistakes#
Calling the action directly skips model binding. Add model errors yourself, or test validation through integration tests.
A test that only checks the mock returns what you set up proves nothing. Assert on the controller’s decisions.
If the controller creates its own DbContext, you cannot unit test it. Inject interfaces.
Best practices#
- One behaviour per test, named Method_Scenario_Expected
- Mock only direct dependencies
- Assert on result types and key values
- Keep unit tests fast and database-free
Remember this#
Ok → OkNegotiatedContentResult<T>. NotFound → NotFoundResult.
BadRequest(ModelState) → InvalidModelStateResult.
Validation attributes do not run in unit tests — AddModelError.
Practice exercise#
Your task: Write a test proving that Get returns NotFoundResult when the service throws nothing but returns null for id 0, and a test that the service is called exactly once.
Need a hint?
Show solution
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Results;
using Moq;
using Xunit;
using ShopApi.Models;
using ShopApi.Services;
public class LookupController : ApiController
{
private readonly IProductService _service;
public LookupController(IProductService service) { _service = service; }
public async Task<IHttpActionResult> Get(int id)
{
var product = await _service.GetAsync(id);
if (product == null) return NotFound();
return Ok(product);
}
}
public class LookupControllerTests
{
[Fact]
public async Task Get_IdZero_ReturnsNotFound_AndCallsServiceOnce()
{
var service = new Mock<IProductService>();
service.Setup(s => s.GetAsync(0)).ReturnsAsync((ProductDto)null);
var result = await new LookupController(service.Object).Get(0);
Assert.IsType<NotFoundResult>(result);
service.Verify(s => s.GetAsync(0), Times.Once);
}
}
Mini quiz#
What type does Ok(product) return in Web API 2?
Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions
My notes#
Saved in this browser.