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:
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; }
}
Initialise 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 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
:
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:
public IntegrationTestsBase()
{
var host = new WebHostBuilder()
.UseStartup<TStartup>()
.ConfigureServices(ConfigureServices);
this.server = new TestServer(host);
this.Client = this.server.CreateClient();
}
public HttpClient Client { get; }