Problem
This post will show you how to perform unit and integration testing of ASP.NET Core and EF Core.
Solution
Note: The sample code contains a lot more tests, I would suggest to download and play with it. Here, I will list a few tests to demonstrate how testing works.
Testing MVC
Add MVC controller with action methods:
C#
public IActionResult Index() { var model = service.GetMovies(); var viewModel = ToViewModel(model); return View(viewModel); } public IActionResult Edit(int id) { var model = service.GetMovie(id); if (model == null) return NotFound(); var viewModel = ToViewModel(model); return View("CreateOrEdit", viewModel); } [HttpPost] public IActionResult Save(int id, MovieViewModel viewModel) { if (viewModel == null) return BadRequest(); if (!ModelState.IsValid) return View("CreateOrEdit", viewModel); var model = ToDomainModel(viewModel); if (viewModel.IsNew) service.AddMovie(model); else service.UpdateMovie(model); return RedirectToAction("Index"); }
Add test to verify ViewResult
is returned:
C#
[Fact(DisplayName = "Index_returns_ViewResult_and_model")] public void Index_returns_ViewResult_and_model() { // Arrange var mockService = new Mock<IMovieService>(); mockService.Setup(service => service.GetMovies()).Returns(new List<Movie>()); var sut = new HomeController(mockService.Object); // Act var result = sut.Index(); // Assert var viewResult = Assert.IsType<ViewResult>(result); var viewModel = Assert.IsType<List<MovieInfoViewModel>>(viewResult.Model); }
Add test to verify status code result (e.g. NotFound
) is returned:
C#
[Fact(DisplayName = "Edit_with_invalid_Id_returns_NotFound")] public void Edit_with_invalid_Id_returns_NotFound() { // Arrange var mockService = new Mock<IMovieService>(); mockService.Setup(service => service.GetMovie(It.IsAny<int>())).Returns((Movie)null); var sut = new HomeController(mockService.Object); // Act var result = sut.Edit(0); // Assert Assert.IsType<NotFoundResult>(result); }
Add test to verify RedirectToAction
is returned:
C#
[Fact(DisplayName = "Save_with_new_model_calls_AddMovie_and_returns_RedirectToAction")] public void Save_with_new_model_calls_AddMovie_and_returns_RedirectToAction() { // Arrange var mockService = new Mock<IMovieService>(); var sut = new HomeController(mockService.Object); // Act var result = sut.Save(1, new MovieViewModel() { IsNew = true }); // Assert mockService.Verify(service => service.AddMovie(It.IsAny<Movie>()), Times.Once); var redirectResult = Assert.IsType<RedirectToActionResult>(result); Assert.Equal(expected: "Index", actual: redirectResult.ActionName); }
Add a test to verify ModelState
errors don’t save and return back the view:
C#
[Fact(DisplayName = "Save_with_invalid_model_state_returns_ViewResult_and_model")] public void Save_with_invalid_model_state_returns_ViewResult_and_model() { // Arrange var mockService = new Mock<IMovieService>(); var sut = new HomeController(mockService.Object); sut.ModelState.AddModelError("Title", "Title is required"); // Act var result = sut.Save(1, new MovieViewModel()); // Assert var viewResult = Assert.IsType<ViewResult>(result); var viewModel = Assert.IsType<MovieViewModel>(viewResult.Model); }
Testing API
Add API controller with action methods:
C#
[HttpGet] public IActionResult Get() { var model = service.GetMovies(); var outputModel = ToOutputModel(model); return Ok(outputModel); } [HttpPost] public IActionResult Create([FromBody]MovieInputModel inputModel) { if (inputModel == null) return BadRequest(); if (!ModelState.IsValid) return Unprocessable(ModelState); var model = ToDomainModel(inputModel); service.AddMovie(model); var outputModel = ToOutputModel(model); return CreatedAtRoute("GetMovie", new { id = outputModel.Id }, outputModel); }
Add a test to verify OkObjectResult
is returned:
C#
[Fact(DisplayName = "Get_retruns_OkObjectResult_and_model")] public void Get_retruns_Ok_result_and_model() { // Arrange var mockService = new Mock<IMovieService>(); mockService.Setup(service => service.GetMovies()).Returns(new List<Movie>()); var sut = new MoviesController(mockService.Object); // Act var result = sut.Get(); // Assert var okObjectResult = Assert.IsType<OkObjectResult>(result); var outputModel = Assert.IsType<List<MovieOutputModel>>(okObjectResult.Value); }
Add a test to verify CreatedAtRouteResult
is returned:
[Fact(DisplayName = "Create_with_valid_model_calls_AddMovie_and_returns_CreatedAtRoute")] public void Create_with_valid_model_calls_AddMovie_and_returns_CreatedAtRoute() { // Arrange var mockService = new Mock<IMovieService>(); var sut = new MoviesController(mockService.Object); // Act var result = sut.Create(new MovieInputModel()); // Assert mockService.Verify(service => service.AddMovie(It.IsAny<Movie>()), Times.Once); var createAtRouteResult = Assert.IsType<CreatedAtRouteResult>(result); Assert.Equal(expected: "GetMovie", actual: createAtRouteResult.RouteName); }
Testing EF
Add a repository (implementation in sample code):
C#
public interface IMovieRepository { void Delete(int id); MovieEntity GetItem(int id); List<MovieEntity> GetList(); void Insert(MovieEntity entity); void Update(MovieEntity entity); }
The repository will work with a DbContext
:
C#
public class Database : DbContext { public Database( DbContextOptions<Database> options) : base(options) { } public DbSet<MovieEntity> Movies { get; set; } }
Initialise with test data:
C#
private void InitDbContext(Database context) { context.Movies.Add(new MovieEntity { ... }); context.Movies.Add(new MovieEntity { ... }); context.Movies.Add(new MovieEntity { ... }); context.SaveChanges(); }
Now you could test various methods of repository, e.g. test GetList()
method:
C#
[Fact(DisplayName = "GetList_returns_correct_count")] public void GetList_returns_correct_count() { // Arrange var builder = new DbContextOptionsBuilder<Database>(); builder.UseInMemoryDatabase(databaseName: "GetList_returns_correct_count"); var context = new Database(builder.Options); InitDbContext(context); var repo = new MovieRepository(context); // Act var result = repo.GetList(); // Assert Assert.Equal(expected: 3, actual: result.Count); }
Integration Testing
Create a base class for integration test classes:
C#
public class IntegrationTestsBase<TStartup> : IDisposable where TStartup : class { private readonly TestServer server; public IntegrationTestsBase() { var host = new WebHostBuilder() .UseStartup<TStartup>() .ConfigureServices(ConfigureServices); this.server = new TestServer(host); this.Client = this.server.CreateClient(); } public HttpClient Client { get; } public void Dispose() { this.Client.Dispose(); this.server.Dispose(); } protected virtual void ConfigureServices(IServiceCollection services) { } }
Create a controller to test MVC/API:
C#
public class MoviesControllerIntegration : IntegrationTestsBase<Startup> { [Fact(DisplayName = "Get_retruns_Ok")] public async Task Get_retruns_Ok_status_code() { // Arrange // Act var response = await this.Client.GetAsync("api/movies"); // Assert Assert.Equal(expected: HttpStatusCode.OK, actual: response.StatusCode); var outputModel = response.ContentAsType<List<MovieOutputModel>>(); Assert.Equal(expected: 2, actual: outputModel.Count); }
Discussion
The single biggest selling point of MVC architecture in general and ASP.NET Core in particular is that it makes testing much simpler. ASP.NET team has done a great job in making a framework that is pluggable, thus enabling testing of controllers, repositories and even the entire application a breeze.
Unit Testing
Unit Testing ASP.NET Core and API controllers is not very different than testing any other class in your application. The sample code contains a lot more tests to show examples of type of tests you could perform, e.g.:
- Verify correct
IActionResult
is returned, e.g.ViewResult
,RedirectAtRouteResult
- Verify correct view name is returned
- Verify correct model is returned
- Verify correct HTTP status code is returned e.g.
NotFoundResult
,BadRequestResult
- Verify model state behaviour e.g. not saving record and returning the view.
- Verify controller dependencies are being called.
Testing Entity Framework
You could test EF using in-memory database, you’ll need package Microsoft.EntityFrameworkCore.InMemory
that gives you UseInMemoryDatabase
extension method on DbContextOptionsBuilder
. With these pieces in place, you could now create an in-memory DbContext
:
C#
var builder = new DbContextOptionsBuilder<Database>(); builder.UseInMemoryDatabase( databaseName: "GetList_returns_correct_count"); var context = new Database(builder.Options); InitDbContext(context); var repo = new MovieRepository(context);
Integration Testing
Remember that ASP.NET Core application is just a console application that sets up web server to listen to HTTP requests. We can setup a test web server using TestServer
class and use HttpClient
to send requests to it:
C#
public IntegrationTestsBase() { var host = new WebHostBuilder() .UseStartup<TStartup>() .ConfigureServices(ConfigureServices); this.server = new TestServer(host); this.Client = this.server.CreateClient(); } public HttpClient Client { get; }