Welcome to today’s post.
In today’s post I will be showing how to unit test ASP.NET Core Web API controllers.
The test framework I will be using for the testing is NUnit and the TDD testing library we will be using is Mock.
In a previous post I showed how to unit test .NET Core applications using NUnit. In another post I showed how to use Mock to unit test .NET Core applications. In both posts I introduced the use of unit testing and mock testing frameworks.
I will focus on unit testing two different types of Web API controller method: secured and unsecured methods.
Explaining Secured and Unsecured Web API Controller Methods
In this post I will show how to test two different types of API methods, the unsecured (unprotected) method, and the secured (protected) method. I will also show how to test an API controller method that uses response caching.
A secured method has the [Authorize] attribute above the method. With secured API methods, the HTTP request that is submitted must have the request header Authorization key set or the HTTP context has the IsAuthenticated property set to true.
In the debugger, the HttpContext.User.Identity.IsAuthenticated property when set to true as shown:
It is one of the conditions for a protected API method block to be accessible.
In a dependency injected ASP.NET Core application, the HTTP context is instantiated through the IHTTPContextAccessor service, but with a unit test application, the HTTP context can be initialized using HttpContext and a default context. This is done as follows within the unit test setup method:
private HttpContext httpContext;
…
[SetUp]
public void Setup()
{
DefaultHttpContext defaultHttpContext = new DefaultHttpContext();
httpContext = defaultHttpContext;
…
When we test API methods that depend on the Entity Framework Core data context, we can use a combination of an in-memory data provider and the Mock TDD test framework to construct our unit test with the AAA (Arrange, Act and Assert) pattern. When we test an API method, a knowledge of which classes are instantiated in the call stack within the API method will help us determine which classes we can mock for our unit tests.
Once we have determined which interfaces need to be mocked, they can be declared within the unit test class as properties of Mock objects:
private Mock<IUserContextService> mockUserContextService;
private Mock<ILogger<BookService>> mockLogger;
private Mock<IBookRepository> mockBookRepository;
private Mock<IMediaFactory> mockMediaFactory;
private Mock<ISpecificationFactory> mockSpecificationFactory;
private Mock<ITaskDurationMeasure> mockTaskDurationMeasure;
private Mock<IEventBus> mockEventBus;
private Mock<IUnitOfWork> mockUnitOfWork;
private Mock<IMemoryCache> mockMemoryCache;
private Mock<ILogger<BookController>> mockControllerLogger;
…
To setup the in-memory data context we use the following dependency injection service collection interfaces and data context:
private ServiceCollection services;
private DbContextOptions<ApplicationDbContext> opts;
private ApplicationDbContext context;
private ServiceProvider serviceProvider;
Our setup is used to instantiate each mock object as shown:
[SetUp]
public void Setup()
{
DefaultHttpContext defaultHttpContext = new DefaultHttpContext();
httpContext = defaultHttpContext;
services = new ServiceCollection();
opts = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: "In_Memory_Db")
.Options;
services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase("In_Memory_Db"));
serviceProvider = services.BuildServiceProvider();
context = serviceProvider.GetRequiredService<ApplicationDbContext>();
mockUserContextService = new Mock<IUserContextService>();
mockLogger = new Mock<ILogger<BookService>>();
mockBookRepository = new Mock<IBookRepository>();
mockSpecificationFactory = new Mock<ISpecificationFactory>();
mockTaskDurationMeasure = new Mock<ITaskDurationMeasure>();
mockMediaFactory = new Mock<IMediaFactory>();
mockEventBus = new Mock<IEventBus>();
mockUnitOfWork = new Mock<IUnitOfWork>();
mockMemoryCache = new Mock<IMemoryCache>();
mockControllerLogger = new Mock<ILogger<BookController>>();
…
}
The code section below sets up the in-memory data context:
services = new ServiceCollection();
opts = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: "In_Memory_Db")
.Options;
services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase("In_Memory_Db"));
serviceProvider = services.BuildServiceProvider();
context = serviceProvider.GetRequiredService<ApplicationDbContext>();
The following code section instantiates each mock object from the respective interface:
mockUserContextService = new Mock<IUserContextService>();
mockLogger = new Mock<ILogger<BookService>>();
mockBookRepository = new Mock<IBookRepository>();
mockSpecificationFactory = new Mock<ISpecificationFactory>();
…
mockUnitOfWork = new Mock<IUnitOfWork>();
mockMemoryCache = new Mock<IMemoryCache>();
mockControllerLogger = new Mock<ILogger<BookController>>();
In the next section, I will show how to test unsecured Web API controller methods.
Unit Testing Unsecured Web API Methods
I will first discuss how an unsecured API method is mocked, then introduce the secured method.
The unsecured version of the API method is shown below:
[HttpGet("api/[controller]/List")]
public async Task<List<BookViewModel>> List()
{
try
{
return await _bookService.GetBooks();
}
catch (Exception ex)
{
throw new GeneralException(ex.Message);
}
}
To test the above API method, we implement the following unit test method:
[Test]
public void ListBooksAPITest()
{
// Arrange
var bookService = new BookService(
context,
mockUserContextService.Object,
mockLogger.Object,
mockBookRepository.Object,
…
mockSpecificationFactory.Object,
mockTaskDurationMeasure.Object,
mockEventBus.Object);
var controller = new BookController(context,
mockControllerLogger.Object,
mockMemoryCache.Object,
…
bookService);
var book = new BookViewModel() {
ID = 3,
Author = "V.Prescott",
Title = "Boundary Rider",
YearPublished = 1974
};
context.Add(book);
context.SaveChanges();
mockBookRepository.Setup(r => r.GetAll()).Returns(() =>
{
return context.Books.ToListAsync();
});
// Act
var booklistResult = controller.List();
// Assert
var books = booklistResult.GetAwaiter().GetResult();
Assert.AreEqual(1, books.Count);
}
In the above unit test we created instances of the BookService, BookController and BookViewModel classes. We then added a record to in-memory data context using:
context.Add(book);
context.SaveChanges();
The next code section sets up and mocks the return value of the repository method GetAll():
mockBookRepository.Setup(r => r.GetAll()).Returns(() =>
{
return context.Books.ToListAsync();
});
The book service GetBooks() method which is shown below includes a call to the book repository which we have mocked:
public async Task<List<BookViewModel>> GetBooks()
{
return await _bookRepository.GetAll();
}
Before we run a suite of unit tests that share common setup parameters such as an in-memory data context, we will need to provide a TearDown method that will clear out and initialise any objects and variables that are shared between the tests within the suite. This will be run after each unit test completes:
[TearDown]
public void Cleanup()
{
// code to cleanup resources after each test
if (context.Books.Count() > 0)
{
context.Books.RemoveRange(context.Books);
context.SaveChanges();
}
}
In the next section, I will show how to test unsecured Web API controller methods.
Unit Testing Secured Web API Methods
As I mentioned earlier, I will show the equivalent secure method and how to unit test it with mocking:
[HttpGet("api/[controller]/List")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<List<BookViewModel>> List()
{
try
{
return await _bookService.GetBooks();
}
catch (Exception ex)
{
throw new GeneralException(ex.Message);
}
}
To be able to test the authenticated API method we don’t need to obtain an authentication token. We can use the following code section to create a default security principal, assign the principal to the current thread, then assign the principal to the HTTP context user:
var defaultPrincipal = new GenericPrincipal(
new GenericIdentity("tester", "tester"), new[] { "Member" });
Thread.CurrentPrincipal = defaultPrincipal;
if (httpContext != null)
httpContext.User = defaultPrincipal;
If the API method includes response caching, we can still use mocking to unit test it. The secure API controller HTTP GET method shown below uses bearer authentication and response caching with in-memory caching:
[HttpGet("api/[controller]/List")]
[ResponseCache(NoStore = true)]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<List<BookViewModel>> List()
{
try
{
if (!_memoryCache
.TryGetValue<List<BookViewModel>>(
"_BookListEntries",
out listEntries))
{
var books = await _bookService.GetBooks();
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(DateTime.Now.AddMinutes(3));
_memoryCache.Set<List<BookViewModel>>(
"_BookListEntries",
books,
cacheEntryOptions);
return books;
}
return listEntries;
}
catch (Exception ex)
{
throw new GeneralException(ex.Message);
}
}
To mock the response caching entry retrieval and setting, we use the following code section:
var memoryCache = Mock.Of<IMemoryCache>();
var cachEntry = Mock.Of<ICacheEntry>();
mockMemoryCache
.Setup(m => m.CreateEntry(It.IsAny<object>()))
.Returns(cachEntry);
If we had used the following method to directly mock the Set() extension method of the IMemoryCache class like this:
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(DateTime.Now.AddMinutes(3));
mockMemoryCache.Setup(m => m.Set<BookViewModel>(
It.IsAny<string>(),
book,
cacheEntryOptions))
.Returns(book);
then a run-time exception would be thrown informing us the Set() extension method cannot be mocked.
The entire unit test method is shown below:
[Test]
public void ListBooksAPITest()
{
// Arrange
var bookService = new BookService(
context,
mockUserContextService.Object,
mockLogger.Object,
mockBookRepository.Object,
mockMediaFactory.Object,
mockSpecificationFactory.Object,
mockTaskDurationMeasure.Object,
mockEventBus.Object);
var controller = new BookController(context,
mockControllerLogger.Object,
mockMemoryCache.Object,
…
bookService);
var book = new BookViewModel() {
ID = 3,
Author = "V.Prescott",
Title = "Boundary Rider",
YearPublished = 1974
};
context.Add(book);
context.SaveChanges();
mockBookRepository.Setup(r => r.GetAll()).Returns(() =>
{
return context.Books.ToListAsync();
});
var memoryCache = Mock.Of<IMemoryCache>();
var cachEntry = Mock.Of<ICacheEntry>();
mockMemoryCache
.Setup(m => m.CreateEntry(It.IsAny<object>()))
.Returns(cachEntry);
var defaultPrincipal = new GenericPrincipal(
new GenericIdentity("tester", "tester"), new[] { "Member" });
Thread.CurrentPrincipal = defaultPrincipal;
if (httpContext != null)
httpContext.User = defaultPrincipal;
// Act
var booklistResult = controller.List();
// Assert
var books = booklistResult.GetAwaiter().GetResult();
Assert.AreEqual(1, books.Count);
}
Unit Testing HTTP POST Web API Methods
One last example I will show is testing an API method that is an HTTP POST.
Below is a controller method that creates a book by calling a dependent SaveBook() method in a book service:
[HttpPost("api/[controller]/Create")]
public async Task<ActionResult> Create([FromBody] BookViewModel model)
{
try
{
BookViewModel book = new BookViewModel()
{
Title = model.Title,
Author = model.Author,
YearPublished =
System.Convert.ToInt32(
model.YearPublished.ToString()
),
Genre = model.Genre,
Edition = model.Edition,
ISBN = model.ISBN,
Location = model.Location,
MediaType = model.MediaType,
DateCreated = DateTime.Today
};
if (ModelState.IsValid)
{
await _bookService.SaveBook(book);
return Ok(book);
}
return BadRequest("Cannot create book record. Error in input.");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return BadRequest("Cannot create book record.");
}
}
The dependent SaveBook() service class method is shown below:
public async Task SaveBook(BookViewModel vm)
{
await this._bookRepository.AddAsync(vm);
await this._bookRepository.UnitOfWork.SaveChangesAsync();
}
Note, there are two class dependencies that we will need to mock, they are:
BookRepository.AddAsync(BookViewModel vm);
and
BookRepository.UnitOfWork.SaveChangesAsync();
The unit test to test the saving of changes in the data context is shown (object instantiations at the beginning have been omitted as they are identical to the unit tests earlier):
[Test]
public void SaveBooksAPITest()
{
…
var book = new BookViewModel() {
ID = 3,
Author = "V.Prescott",
Title = "Boundary Rider",
YearPublished = 1974
};
mockBookRepository.Setup(m => m.AddAsync(It.IsAny<BookViewModel>()))
.Callback(() =>
{
context.Add(book);
context.SaveChanges();
})
.Returns(() =>
{
return context.Books.Where(b => b.ID == book.ID)
.SingleOrDefaultAsync();
});
mockBookRepository.Setup(m => m.UnitOfWork)
.Returns(context);
mockUnitOfWork.Setup(m => m
.SaveChangesAsync(System.Threading.CancellationToken.None))
.Callback(() =>
{
context.SaveChanges();
});
// Act
var bookSaveResult = controller.Create(book);
// Assert
var booksResult = bookSaveResult.GetAwaiter().GetResult();
Assert.AreEqual(
bookSaveResult.Result.GetType().Name,
typeof(Microsoft.AspNetCore.Mvc.RedirectToActionResult).Name
);
int bookCount = context.Books.CountAsync().GetAwaiter().GetResult();
Assert.AreEqual(1, bookCount);
}
The first mock statement runs a callback delegate that adds a record into the in-memory context and returns the record:
mockBookRepository.Setup(m => m.AddAsync(It.IsAny<BookViewModel>()))
.Callback(() =>
{
context.Add(book);
context.SaveChanges();
})
.Returns(() =>
{
return context.Books.Where(b => b.ID == book.ID)
.SingleOrDefaultAsync();
});
The next mock statement returns the unit of work data context from the repository:
mockBookRepository.Setup(m => m.UnitOfWork)
.Returns(context);
Which is applicable for the following command within the SaveBook() service method:
this.bookRepository.UnitOfWork
Then the following mock statement:
mockUnitOfWork.Setup(m => m
.SaveChangesAsync(System.Threading.CancellationToken.None))
.Callback(() =>
{
context.SaveChanges();
});
Mocks the action for saving the data context asynchronously when the following command within the SaveBook() service method is called:
UnitOfWork.SaveChangesAsync();
The assertions test the expected number of records saved match, and the API response HTTP result matches the expected result:
Assert.AreEqual(
bookSaveResult.Result.GetType().Name,
typeof(Microsoft.AspNetCore.Mvc.OkObjectResult).Name
);
In the above we match the result to the Ok (200) status.
We can also add unit tests to cover the test case scenarios where there are failures of the above API HTTP POST method, with an expected status result of 400 (Bad Request).
The above showed us how to unit test API controller methods with the Mock library. In addition, we used the Mock unit testing API to mock any objects that are dependent within the call stack of our API controller methods. We also saw how to mock a unit test for a protected API method and how to mock an API that uses in-memory response caching.
That is all for today’s post.
I hope you have found this post useful and informative.
Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.