ASP.NET Core & EF Core 2.0 Testing






3.55/5 (3 votes)
How to perform unit and integration testing of ASP.NET Core and EF Core. Continue reading...
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()
{
// 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:
[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:
[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:
[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:
[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()
{
// 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):
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()
{
// 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:
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()
{
// 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
:
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; }