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 difficult 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

First, here is the StocksController code that we’ll be testing. Given a stock symbol (such as MSFT), this queries the database for the stock data and return it. Otherwise it returns an error. For any exceptions, it returns a 500 error response. Here’s the 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 has two dependencies:

  • 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.

Next, 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.

Finally, if the controller method is async (which they usually are), you’ll need to unit test this async method by adding async Task to the unit test method signature and then awaiting the method under test.

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