ASP.NET Core – How to unit test an ApiController

The key to unit testing an ApiController class is to mock out all of its dependencies, including the controller’s HttpContext property, like this:

//arrange (note: only showing partial code here, see full example below) var stocksController = new StocksController(mockRepository.Object); stocksController.ControllerContext.HttpContext = new DefaultHttpContext() { RequestAborted = cancelTokenSource.Token //set any properties in here that your controller method uses }; //act var result = await stocksController.Get(symbol) as ObjectResult; //assert Assert.AreEqual(HttpStatusCode.BadRequest, (HttpStatusCode)result.StatusCode);
Code language: C# (cs)

If the controller method you’re testing uses anything from the HttpContext, then you’ll want to swap in your own value. Otherwise HttpContext will be null and you’ll get a NullReferenceException.

Fortunately Microsoft designed this to be unit testable by making the HttpContext property have a public setter. Just take a look at the remarks they put for the HttpContext property:

// Remarks: // The property setter is provided for unit test purposes only. public HttpContext HttpContext { get; set; }
Code language: C# (cs)

Nice. Without this public setter, it’d be very hard (if not impossible) to unit test a controller method that uses this HttpContext property.

Now i’ll show a full example of this StocksController code and its unit tests (the full code is available in this GitHub repo).

StocksController code and tests

I use a test-first approach when writing code. But instead of showing the tests first, I’ll show the completed StocksController code:

[ApiController] [Route("[controller]")] public class StocksController : ControllerBase { private readonly IStocksRepository StocksRepository; public StocksController(IStocksRepository stockRepository) { StocksRepository = stockRepository; } [HttpGet("{symbol}")] public async Task<IActionResult> Get(string symbol) { try { var stock = await StocksRepository.Get(symbol, HttpContext.RequestAborted); if (stock is NullStock) return BadRequest($"{symbol} stock doesn't exist"); return Ok(stock); } catch(TaskCanceledException) { return BadRequest("User cancelled"); } catch(Exception ex) { return StatusCode((int)HttpStatusCode.InternalServerError, $"Error when looking up {symbol} stock: {ex.Message}"); } } }
Code language: C# (cs)

If you had to write tests for this, how would you do it?

Whenever you are unit testing anything, the first thing to do is determine if you need to mock out its dependencies. In this case, StocksController is dependent on two things:

  • IStocksRepository
  • HttpContext.RequestAborted

To mock out the dependencies, you have to make it possible to swap in mocks in place of the real things.

In this case, the IStocksRepository dependency is passed into the constructor (dependency injection). Since it’s an interface, the best way to mock it out is to use a mocking framework (I’m using Moq).

Next, the method we’re unit testing here is dependent on HttpContext.RequestAborted. To mock this out, you can set the ControllerContext.HttpContext on the controller and set any properties that the code uses. In this case, it’s only using the RequestAborted property, so that’s the only thing we really need to set.

Finally, to build and configure these mocks, I always prefer to use a Build() method, not [TestInitialize]. Why? Because then I can pass in parameters to the build method.

The StocksController.Get() method is returning an IActionResult. I suggest asserting two things when unit testing an ApiController: the status code and the object (in this case, it’s returning a Stock object). You can do this by casting the returned result to an ObjectResult and checking ObjectResult.StatusCode and ObjectResult.Value (casted to the expected type – in this case, it needs to be casted to the Stock type).

Putting this all together, here are the StocksController unit tests. These test the happy path and a few error scenarios:

[TestClass()] public class StocksControllerTests { private StocksController Build(string symbol, Stock returns=null, Exception throws=null) { var cancelTokenSource = new CancellationTokenSource(); var mockRepo = new Mock<IStocksRepository>(); if (throws == null) { mockRepo.Setup(t => t.Get(symbol, cancelTokenSource.Token)).ReturnsAsync(returns); } else { mockRepo.Setup(t => t.Get(symbol, cancelTokenSource.Token)).ThrowsAsync(throws); } var stocksController = new StocksController(mockRepo.Object); stocksController.ControllerContext.HttpContext = new DefaultHttpContext() { RequestAborted = cancelTokenSource.Token }; return stocksController; } [TestMethod()] public async Task GetStockTest_WhenStockDoesntExist_ReturnsBadRequestError() { //arrange var symbol = "GMEEE"; var stocksController = Build(symbol, returns: new NullStock()); //act var result = await stocksController.Get(symbol) as ObjectResult; //assert Assert.AreEqual(HttpStatusCode.BadRequest, (HttpStatusCode)result.StatusCode); StringAssert.Contains(result.Value as string, symbol); } [TestMethod()] public async Task GetStockTest_WhenRequestCanceled_ReturnsBadRequestError() { //arrange var symbol = "GME"; var stocksController = Build(symbol, throws: new TaskCanceledException()); //act var result = await stocksController.Get(symbol) as ObjectResult; //assert Assert.AreEqual(HttpStatusCode.BadRequest, (HttpStatusCode)result.StatusCode); StringAssert.Contains(result.Value as string, "cancelled"); } [TestMethod()] public async Task GetStockTest_WhenRepoThrows_ReturnsServerError() { //arrange var symbol = "GME"; var stocksController = Build(symbol, throws: new NotImplementedException()); //act var result = await stocksController.Get(symbol) as ObjectResult; //assert Assert.AreEqual(HttpStatusCode.InternalServerError, (HttpStatusCode)result.StatusCode); } [TestMethod()] public async Task GetStockTest_ReturnsOKAndStock() { //arrange var symbol = "GME"; var expectedStock = new Stock() { Name = "Gamestonk", Symbol = symbol, Price = 10_000_000, QuoteTime = DateTimeOffset.Now }; var stocksController = Build(symbol, returns: expectedStock); //act var result = await stocksController.Get(symbol) as ObjectResult; //assert Assert.AreEqual(HttpStatusCode.OK, (HttpStatusCode)result.StatusCode); Assert.AreSame(expectedStock, result.Value as Stock); } }
Code language: C# (cs)

Leave a Comment