Problem
How to perform unit and integration testing of ASP.NET Core and EF Core.
Solution
The sample code contain a lot more tests, I would suggest you 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,
- 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,
- [Fact(DisplayName = "Index_returns_ViewResult_and_model")]
- public void Index_returns_ViewResult_and_model()
- {
-
- var mockService = new Mock<IMovieService>();
- mockService.Setup(service =>
- service.GetMovies()).Returns(new List<Movie>());
-
- var sut = new HomeController(mockService.Object);
-
-
- var result = sut.Index();
-
-
- 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,
- [Fact(DisplayName = "Edit_with_invalid_Id_returns_NotFound")]
- public void Edit_with_invalid_Id_returns_NotFound()
- {
-
- var mockService = new Mock<IMovieService>();
- mockService.Setup(service =>
- service.GetMovie(It.IsAny<int>())).Returns((Movie)null);
-
- var sut = new HomeController(mockService.Object);
-
-
- var result = sut.Edit(0);
-
-
- Assert.IsType<NotFoundResult>(result);
- }
Add test to verify RedirectToAction is returned,
- [Fact(DisplayName =
- "Save_with_new_model_calls_AddMovie_and_returns_RedirectToAction")]
- public void Save_with_new_model_calls_AddMovie_and_returns_RedirectToAction()
- {
-
- var mockService = new Mock<IMovieService>();
- var sut = new HomeController(mockService.Object);
-
-
- var result = sut.Save(1, new MovieViewModel() { IsNew = true });
-
-
- 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,
- [Fact(DisplayName =
- "Save_with_invalid_model_state_returns_ViewResult_and_model")]
- public void Save_with_invalid_model_state_returns_ViewResult_and_model()
- {
-
- var mockService = new Mock<IMovieService>();
- var sut = new HomeController(mockService.Object);
- sut.ModelState.AddModelError("Title", "Title is required");
-
-
- var result = sut.Save(1, new MovieViewModel());
-
-
- var viewResult = Assert.IsType<ViewResult>(result);
- var viewModel = Assert.IsType<MovieViewModel>(viewResult.Model);
- }
Testing API
Add API controller with action methods,
- [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,
- [Fact(DisplayName = "Get_retruns_OkObjectResult_and_model")]
- public void Get_retruns_Ok_result_and_model()
- {
-
- var mockService = new Mock<IMovieService>();
- mockService.Setup(service =>
- service.GetMovies()).Returns(new List<Movie>());
-
- var sut = new MoviesController(mockService.Object);
-
-
- var result = sut.Get();
-
-
- 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()
- {
-
- var mockService = new Mock<IMovieService>();
- var sut = new MoviesController(mockService.Object);
-
-
- var result = sut.Create(new MovieInputModel());
-
-
- 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),
- 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,
- public class Database : DbContext
- {
- public Database(
- DbContextOptions<Database> options) : base(options) { }
-
- public DbSet<MovieEntity> Movies { get; set; }
- }
Initialize with test data,
- 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,
- [Fact(DisplayName = "GetList_returns_correct_count")]
- public void GetList_returns_correct_count()
- {
-
- 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);
-
-
- var result = repo.GetList();
-
-
- Assert.Equal(expected: 3, actual: result.Count);
- }
Integration Testing
Create a base class for integration test classes,
- 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,
- public class MoviesControllerIntegration : IntegrationTestsBase<Startup>
- {
- [Fact(DisplayName = "Get_retruns_Ok")]
- public async Task Get_retruns_Ok_status_code()
- {
-
-
-
- var response = await this.Client.GetAsync("api/movies");
-
-
- 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 that much different than testing any other class in your application. The sample code contain 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,
- 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 set up a test web server using TestServer class and use HttpClient to send requests to it,
- public IntegrationTestsBase()
- {
- var host = new WebHostBuilder()
- .UseStartup<TStartup>()
- .ConfigureServices(ConfigureServices);
-
- this.server = new TestServer(host);
- this.Client = this.server.CreateClient();
- }
-
- public HttpClient Client { get; }
Source Code
GitHub