Click here to Skip to main content
15,893,790 members
Articles / Web Development / ASP.NET / ASP.NET Core

ASP.NET Core & EF Core 2.0 Testing

Rate me:
Please Sign up or sign in to vote.
3.55/5 (3 votes)
5 Sep 2017CPOL2 min read 15.1K   5   6
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:

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:

C#
[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; }

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



Comments and Discussions

 
QuestionMore code but less explanation Pin
Mou_kol6-Sep-17 23:03
Mou_kol6-Sep-17 23:03 
AnswerRe: More code but less explanation Pin
User 10432646-Sep-17 23:18
User 10432646-Sep-17 23:18 
QuestionWhat is IntegrationTestsBase Pin
Mou_kol5-Sep-17 23:06
Mou_kol5-Sep-17 23:06 
AnswerRe: What is IntegrationTestsBase Pin
User 10432646-Sep-17 1:01
User 10432646-Sep-17 1:01 
QuestionFew question about unit testing Pin
Mou_kol5-Sep-17 23:03
Mou_kol5-Sep-17 23:03 
AnswerRe: Few question about unit testing Pin
User 10432646-Sep-17 1:10
User 10432646-Sep-17 1:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.