"
 
 
 
ASP.NET (snapshot 2017) Microsoft documentation and samples

Testing controller logic in ASP.NET Core

By Steve Smith

Controllers in ASP.NET MVC apps should be small and focused on user-interface concerns. Large controllers that deal with non-UI concerns are more difficult to test and maintain.

View or download sample from GitHub

Testing controllers

Controllers are a central part of any ASP.NET Core MVC application. As such, you should have confidence they behave as intended for your app. Automated tests can provide you with this confidence and can detect errors before they reach production. It’s important to avoid placing unnecessary responsibilities within your controllers and ensure your tests focus only on controller responsibilities.

Controller logic should be minimal and not be focused on business logic or infrastructure concerns (for example, data access). Test controller logic, not the framework. Test how the controller behaves based on valid or invalid inputs. Test controller responses based on the result of the business operation it performs.

Typical controller responsibilities:

Unit testing

Unit testing involves testing a part of an app in isolation from its infrastructure and dependencies. When unit testing controller logic, only the contents of a single action is tested, not the behavior of its dependencies or of the framework itself. As you unit test your controller actions, make sure you focus only on its behavior. A controller unit test avoids things like filters, routing, or model binding. By focusing on testing just one thing, unit tests are generally simple to write and quick to run. A well-written set of unit tests can be run frequently without much overhead. However, unit tests do not detect issues in the interaction between components, which is the purpose of (xref:)integration testing.

If you’re writing custom filters, routes, etc, you should unit test them, but not as part of your tests on a particular controller action. They should be tested in isolation.

[!TIP] Create and run unit tests with Visual Studio.

To demonstrate unit testing, review the following controller. It displays a list of brainstorming sessions and allows new brainstorming sessions to be created with a POST:

[!code-csharpMain]

   1:  using System;
   2:  using System.ComponentModel.DataAnnotations;
   3:  using System.Linq;
   4:  using System.Threading.Tasks;
   5:  using Microsoft.AspNetCore.Mvc;
   6:  using TestingControllersSample.Core.Interfaces;
   7:  using TestingControllersSample.Core.Model;
   8:  using TestingControllersSample.ViewModels;
   9:   
  10:  namespace TestingControllersSample.Controllers
  11:  {
  12:      public class HomeController : Controller
  13:      {
  14:          private readonly IBrainstormSessionRepository _sessionRepository;
  15:   
  16:          public HomeController(IBrainstormSessionRepository sessionRepository)
  17:          {
  18:              _sessionRepository = sessionRepository;
  19:          }
  20:   
  21:          public async Task<IActionResult> Index()
  22:          {
  23:              var sessionList = await _sessionRepository.ListAsync();
  24:   
  25:              var model = sessionList.Select(session => new StormSessionViewModel()
  26:              {
  27:                  Id = session.Id,
  28:                  DateCreated = session.DateCreated,
  29:                  Name = session.Name,
  30:                  IdeaCount = session.Ideas.Count
  31:              });
  32:   
  33:              return View(model);
  34:          }
  35:   
  36:          public class NewSessionModel
  37:          {
  38:              [Required]
  39:              public string SessionName { get; set; }
  40:          }
  41:   
  42:          [HttpPost]
  43:          public async Task<IActionResult> Index(NewSessionModel model)
  44:          {
  45:              if (!ModelState.IsValid)
  46:              {
  47:                  return BadRequest(ModelState);
  48:              }
  49:              else
  50:              {
  51:                  await _sessionRepository.AddAsync(new BrainstormSession()
  52:                  {
  53:                      DateCreated = DateTimeOffset.Now,
  54:                      Name = model.SessionName
  55:                  });
  56:              }
  57:   
  58:              return RedirectToAction(actionName: nameof(Index));
  59:          }
  60:      }
  61:  }

The controller is following the explicit dependencies principle, expecting dependency injection to provide it with an instance of IBrainstormSessionRepository. This makes it fairly easy to test using a mock object framework, like Moq. The HTTP GET Index method has no looping or branching and only calls one method. To test this Index method, we need to verify that a ViewResult is returned, with a ViewModel from the repository’s List method.

[!code-csharpMain]

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Threading.Tasks;
   5:  using Microsoft.AspNetCore.Mvc;
   6:  using Moq;
   7:  using TestingControllersSample.Controllers;
   8:  using TestingControllersSample.Core.Interfaces;
   9:  using TestingControllersSample.Core.Model;
  10:  using TestingControllersSample.ViewModels;
  11:  using Xunit;
  12:   
  13:  namespace TestingControllersSample.Tests.UnitTests
  14:  {
  15:      public class HomeControllerTests
  16:      {
  17:          [Fact]
  18:          public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
  19:          {
  20:              // Arrange
  21:              var mockRepo = new Mock<IBrainstormSessionRepository>();
  22:              mockRepo.Setup(repo => repo.ListAsync()).Returns(Task.FromResult(GetTestSessions()));
  23:              var controller = new HomeController(mockRepo.Object);
  24:   
  25:              // Act
  26:              var result = await controller.Index();
  27:   
  28:              // Assert
  29:              var viewResult = Assert.IsType<ViewResult>(result);
  30:              var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
  31:                  viewResult.ViewData.Model);
  32:              Assert.Equal(2, model.Count());
  33:          }
  34:   
  35:          [Fact]
  36:          public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
  37:          {
  38:              // Arrange
  39:              var mockRepo = new Mock<IBrainstormSessionRepository>();
  40:              mockRepo.Setup(repo => repo.ListAsync()).Returns(Task.FromResult(GetTestSessions()));
  41:              var controller = new HomeController(mockRepo.Object);
  42:              controller.ModelState.AddModelError("SessionName", "Required");
  43:              var newSession = new HomeController.NewSessionModel();
  44:   
  45:              // Act
  46:              var result = await controller.Index(newSession);
  47:   
  48:              // Assert
  49:              var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
  50:              Assert.IsType<SerializableError>(badRequestResult.Value);
  51:          }
  52:   
  53:          [Fact]
  54:          public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
  55:          {
  56:              // Arrange
  57:              var mockRepo = new Mock<IBrainstormSessionRepository>();
  58:              mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
  59:                  .Returns(Task.CompletedTask)
  60:                  .Verifiable();
  61:              var controller = new HomeController(mockRepo.Object);
  62:              var newSession = new HomeController.NewSessionModel()
  63:              {
  64:                  SessionName = "Test Name"
  65:              };
  66:   
  67:              // Act
  68:              var result = await controller.Index(newSession);
  69:   
  70:              // Assert
  71:              var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
  72:              Assert.Null(redirectToActionResult.ControllerName);
  73:              Assert.Equal("Index", redirectToActionResult.ActionName);
  74:              mockRepo.Verify();
  75:          }
  76:   
  77:          private List<BrainstormSession> GetTestSessions()
  78:          {
  79:              var sessions = new List<BrainstormSession>();
  80:              sessions.Add(new BrainstormSession()
  81:              {
  82:                  DateCreated = new DateTime(2016, 7, 2),
  83:                  Id = 1,
  84:                  Name = "Test One"
  85:              });
  86:              sessions.Add(new BrainstormSession()
  87:              {
  88:                  DateCreated = new DateTime(2016, 7, 1),
  89:                  Id = 2,
  90:                  Name = "Test Two"
  91:              });
  92:              return sessions;
  93:          }
  94:      }
  95:  }

The HomeController HTTP POST Index method (shown above) should verify:

Invalid model state can be tested by adding errors using AddModelError as shown in the first test below.

[!code-csharpMain]

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Threading.Tasks;
   5:  using Microsoft.AspNetCore.Mvc;
   6:  using Moq;
   7:  using TestingControllersSample.Controllers;
   8:  using TestingControllersSample.Core.Interfaces;
   9:  using TestingControllersSample.Core.Model;
  10:  using TestingControllersSample.ViewModels;
  11:  using Xunit;
  12:   
  13:  namespace TestingControllersSample.Tests.UnitTests
  14:  {
  15:      public class HomeControllerTests
  16:      {
  17:          [Fact]
  18:          public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
  19:          {
  20:              // Arrange
  21:              var mockRepo = new Mock<IBrainstormSessionRepository>();
  22:              mockRepo.Setup(repo => repo.ListAsync()).Returns(Task.FromResult(GetTestSessions()));
  23:              var controller = new HomeController(mockRepo.Object);
  24:   
  25:              // Act
  26:              var result = await controller.Index();
  27:   
  28:              // Assert
  29:              var viewResult = Assert.IsType<ViewResult>(result);
  30:              var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
  31:                  viewResult.ViewData.Model);
  32:              Assert.Equal(2, model.Count());
  33:          }
  34:   
  35:          [Fact]
  36:          public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
  37:          {
  38:              // Arrange
  39:              var mockRepo = new Mock<IBrainstormSessionRepository>();
  40:              mockRepo.Setup(repo => repo.ListAsync()).Returns(Task.FromResult(GetTestSessions()));
  41:              var controller = new HomeController(mockRepo.Object);
  42:              controller.ModelState.AddModelError("SessionName", "Required");
  43:              var newSession = new HomeController.NewSessionModel();
  44:   
  45:              // Act
  46:              var result = await controller.Index(newSession);
  47:   
  48:              // Assert
  49:              var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
  50:              Assert.IsType<SerializableError>(badRequestResult.Value);
  51:          }
  52:   
  53:          [Fact]
  54:          public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
  55:          {
  56:              // Arrange
  57:              var mockRepo = new Mock<IBrainstormSessionRepository>();
  58:              mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
  59:                  .Returns(Task.CompletedTask)
  60:                  .Verifiable();
  61:              var controller = new HomeController(mockRepo.Object);
  62:              var newSession = new HomeController.NewSessionModel()
  63:              {
  64:                  SessionName = "Test Name"
  65:              };
  66:   
  67:              // Act
  68:              var result = await controller.Index(newSession);
  69:   
  70:              // Assert
  71:              var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
  72:              Assert.Null(redirectToActionResult.ControllerName);
  73:              Assert.Equal("Index", redirectToActionResult.ActionName);
  74:              mockRepo.Verify();
  75:          }
  76:   
  77:          private List<BrainstormSession> GetTestSessions()
  78:          {
  79:              var sessions = new List<BrainstormSession>();
  80:              sessions.Add(new BrainstormSession()
  81:              {
  82:                  DateCreated = new DateTime(2016, 7, 2),
  83:                  Id = 1,
  84:                  Name = "Test One"
  85:              });
  86:              sessions.Add(new BrainstormSession()
  87:              {
  88:                  DateCreated = new DateTime(2016, 7, 1),
  89:                  Id = 2,
  90:                  Name = "Test Two"
  91:              });
  92:              return sessions;
  93:          }
  94:      }
  95:  }

The first test confirms when ModelState is not valid, the same ViewResult is returned as for a GET request. Note that the test doesn’t attempt to pass in an invalid model. That wouldn’t work anyway since model binding isn’t running (though an (xref:)integration test would use exercise model binding). In this case, model binding is not being tested. These unit tests are only testing what the code in the action method does.

The second test verifies that when ModelState is valid, a new BrainstormSession is added (via the repository), and the method returns a RedirectToActionResult with the expected properties. Mocked calls that aren’t called are normally ignored, but calling Verifiable at the end of the setup call allows it to be verified in the test. This is done with the call to mockRepo.Verify, which will fail the test if the expected method was not called.

[!NOTE] The Moq library used in this sample makes it easy to mix verifiable, or “strict”, mocks with non-verifiable mocks (also called “loose” mocks or stubs). Learn more about customizing Mock behavior with Moq.

Another controller in the app displays information related to a particular brainstorming session. It includes some logic to deal with invalid id values:

[!code-csharpMain]

   1:  using System.Threading.Tasks;
   2:  using Microsoft.AspNetCore.Mvc;
   3:  using TestingControllersSample.Core.Interfaces;
   4:  using TestingControllersSample.ViewModels;
   5:   
   6:  namespace TestingControllersSample.Controllers
   7:  {
   8:      public class SessionController : Controller
   9:      {
  10:          private readonly IBrainstormSessionRepository _sessionRepository;
  11:   
  12:          public SessionController(IBrainstormSessionRepository sessionRepository)
  13:          {
  14:              _sessionRepository = sessionRepository;
  15:          }
  16:   
  17:          public async Task<IActionResult> Index(int? id)
  18:          {
  19:              if (!id.HasValue)
  20:              {
  21:                  return RedirectToAction(actionName: nameof(Index), controllerName: "Home");
  22:              }
  23:   
  24:              var session = await _sessionRepository.GetByIdAsync(id.Value);
  25:              if (session == null)
  26:              {
  27:                  return Content("Session not found.");
  28:              }
  29:   
  30:              var viewModel = new StormSessionViewModel()
  31:              {
  32:                  DateCreated = session.DateCreated,
  33:                  Name = session.Name,
  34:                  Id = session.Id
  35:              };
  36:   
  37:              return View(viewModel);
  38:          }
  39:      }
  40:  }

The controller action has three cases to test, one for each return statement:

[!code-csharpMain]

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Threading.Tasks;
   5:  using Microsoft.AspNetCore.Mvc;
   6:  using Moq;
   7:  using TestingControllersSample.Controllers;
   8:  using TestingControllersSample.Core.Interfaces;
   9:  using TestingControllersSample.Core.Model;
  10:  using TestingControllersSample.ViewModels;
  11:  using Xunit;
  12:   
  13:  namespace TestingControllersSample.Tests.UnitTests
  14:  {
  15:      public class SessionControllerTests
  16:      {
  17:          [Fact]
  18:          public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
  19:          {
  20:              // Arrange
  21:              var controller = new SessionController(sessionRepository: null);
  22:   
  23:              // Act
  24:              var result = await controller.Index(id: null);
  25:   
  26:              // Assert
  27:              var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
  28:              Assert.Equal("Home", redirectToActionResult.ControllerName);
  29:              Assert.Equal("Index", redirectToActionResult.ActionName);
  30:          }
  31:   
  32:          [Fact]
  33:          public async Task IndexReturnsContentWithSessionNotFoundWhenSessionNotFound()
  34:          {
  35:              // Arrange
  36:              int testSessionId = 1;
  37:              var mockRepo = new Mock<IBrainstormSessionRepository>();
  38:              mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
  39:                  .Returns(Task.FromResult((BrainstormSession)null));
  40:              var controller = new SessionController(mockRepo.Object);
  41:   
  42:              // Act
  43:              var result = await controller.Index(testSessionId);
  44:   
  45:              // Assert
  46:              var contentResult = Assert.IsType<ContentResult>(result);
  47:              Assert.Equal("Session not found.", contentResult.Content);
  48:          }
  49:   
  50:          [Fact]
  51:          public async Task IndexReturnsViewResultWithStormSessionViewModel()
  52:          {
  53:              // Arrange
  54:              int testSessionId = 1;
  55:              var mockRepo = new Mock<IBrainstormSessionRepository>();
  56:              mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
  57:                  .Returns(Task.FromResult(GetTestSessions().FirstOrDefault(s => s.Id == testSessionId)));
  58:              var controller = new SessionController(mockRepo.Object);
  59:   
  60:              // Act
  61:              var result = await controller.Index(testSessionId);
  62:   
  63:              // Assert
  64:              var viewResult = Assert.IsType<ViewResult>(result);
  65:              var model = Assert.IsType<StormSessionViewModel>(viewResult.ViewData.Model);
  66:              Assert.Equal("Test One", model.Name);
  67:              Assert.Equal(2, model.DateCreated.Day);
  68:              Assert.Equal(testSessionId, model.Id);
  69:          }
  70:   
  71:          private List<BrainstormSession> GetTestSessions()
  72:          {
  73:              var sessions = new List<BrainstormSession>();
  74:              sessions.Add(new BrainstormSession()
  75:              {
  76:                  DateCreated = new DateTime(2016, 7, 2),
  77:                  Id = 1,
  78:                  Name = "Test One"
  79:              });
  80:              sessions.Add(new BrainstormSession()
  81:              {
  82:                  DateCreated = new DateTime(2016, 7, 1),
  83:                  Id = 2,
  84:                  Name = "Test Two"
  85:              });
  86:              return sessions;
  87:          }
  88:      }
  89:  }

The app exposes functionality as a web API (a list of ideas associated with a brainstorming session and a method for adding new ideas to a session):

[!code-csharpMain]

   1:  using System;
   2:  using System.Linq;
   3:  using System.Threading.Tasks;
   4:  using Microsoft.AspNetCore.Mvc;
   5:  using TestingControllersSample.ClientModels;
   6:  using TestingControllersSample.Core.Interfaces;
   7:  using TestingControllersSample.Core.Model;
   8:   
   9:  namespace TestingControllersSample.Api
  10:  {
  11:      [Route("api/ideas")]
  12:      public class IdeasController : Controller
  13:      {
  14:          private readonly IBrainstormSessionRepository _sessionRepository;
  15:   
  16:          public IdeasController(IBrainstormSessionRepository sessionRepository)
  17:          {
  18:              _sessionRepository = sessionRepository;
  19:          }
  20:   
  21:          [HttpGet("forsession/{sessionId}")]
  22:          public async Task<IActionResult> ForSession(int sessionId)
  23:          {
  24:              var session = await _sessionRepository.GetByIdAsync(sessionId);
  25:              if (session == null)
  26:              {
  27:                  return NotFound(sessionId);
  28:              }
  29:   
  30:              var result = session.Ideas.Select(idea => new IdeaDTO()
  31:              {
  32:                  Id = idea.Id,
  33:                  Name = idea.Name,
  34:                  Description = idea.Description,
  35:                  DateCreated = idea.DateCreated
  36:              }).ToList();
  37:   
  38:              return Ok(result);
  39:          }
  40:   
  41:          [HttpPost("create")]
  42:          public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
  43:          {
  44:              if (!ModelState.IsValid)
  45:              {
  46:                  return BadRequest(ModelState);
  47:              }
  48:   
  49:              var session = await _sessionRepository.GetByIdAsync(model.SessionId);
  50:              if (session == null)
  51:              {
  52:                  return NotFound(model.SessionId);
  53:              }
  54:   
  55:              var idea = new Idea()
  56:              {
  57:                  DateCreated = DateTimeOffset.Now,
  58:                  Description = model.Description,
  59:                  Name = model.Name
  60:              };
  61:              session.AddIdea(idea);
  62:   
  63:              await _sessionRepository.UpdateAsync(session);
  64:   
  65:              return Ok(session);
  66:          }
  67:      }
  68:  }

The ForSession method returns a list of IdeaDTO types. Avoid returning your business domain entities directly via API calls, since frequently they include more data than the API client requires, and they unnecessarily couple your app’s internal domain model with the API you expose externally. Mapping between domain entities and the types you will return over the wire can be done manually (using a LINQ Select as shown here) or using a library like AutoMapper

The unit tests for the Create and ForSession API methods:

[!code-csharpMain]

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Threading.Tasks;
   5:  using Microsoft.AspNetCore.Mvc;
   6:  using Moq;
   7:  using TestingControllersSample.Api;
   8:  using TestingControllersSample.ClientModels;
   9:  using TestingControllersSample.Core.Interfaces;
  10:  using TestingControllersSample.Core.Model;
  11:  using Xunit;
  12:   
  13:  namespace TestingControllersSample.Tests.UnitTests
  14:  {
  15:      public class ApiIdeasControllerTests
  16:      {
  17:          [Fact]
  18:          public async Task Create_ReturnsBadRequest_GivenInvalidModel()
  19:          {
  20:              // Arrange & Act
  21:              var mockRepo = new Mock<IBrainstormSessionRepository>();
  22:              var controller = new IdeasController(mockRepo.Object);
  23:              controller.ModelState.AddModelError("error","some error");
  24:   
  25:              // Act
  26:              var result = await controller.Create(model: null);
  27:   
  28:              // Assert
  29:              Assert.IsType<BadRequestObjectResult>(result);
  30:          }
  31:   
  32:          [Fact]
  33:          public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
  34:          {
  35:              // Arrange
  36:              int testSessionId = 123;
  37:              var mockRepo = new Mock<IBrainstormSessionRepository>();
  38:              mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
  39:                  .Returns(Task.FromResult((BrainstormSession)null));
  40:              var controller = new IdeasController(mockRepo.Object);
  41:   
  42:              // Act
  43:              var result = await controller.Create(new NewIdeaModel());
  44:   
  45:              // Assert
  46:              Assert.IsType<NotFoundObjectResult>(result);
  47:          }
  48:   
  49:          [Fact]
  50:          public async Task Create_ReturnsNewlyCreatedIdeaForSession()
  51:          {
  52:              // Arrange
  53:              int testSessionId = 123;
  54:              string testName = "test name";
  55:              string testDescription = "test description";
  56:              var testSession = GetTestSession();
  57:              var mockRepo = new Mock<IBrainstormSessionRepository>();
  58:              mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
  59:                  .Returns(Task.FromResult(testSession));
  60:              var controller = new IdeasController(mockRepo.Object);
  61:   
  62:              var newIdea = new NewIdeaModel()
  63:              {
  64:                  Description = testDescription,
  65:                  Name = testName,
  66:                  SessionId = testSessionId
  67:              };
  68:              mockRepo.Setup(repo => repo.UpdateAsync(testSession))
  69:                  .Returns(Task.CompletedTask)
  70:                  .Verifiable();
  71:   
  72:              // Act
  73:              var result = await controller.Create(newIdea);
  74:   
  75:              // Assert
  76:              var okResult = Assert.IsType<OkObjectResult>(result);
  77:              var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
  78:              mockRepo.Verify();
  79:              Assert.Equal(2, returnSession.Ideas.Count());
  80:              Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
  81:              Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
  82:          }
  83:   
  84:          [Fact]
  85:          public async Task ForSession_ReturnsHttpNotFound_ForInvalidSession()
  86:          {
  87:              // Arrange
  88:              int testSessionId = 123;
  89:              var mockRepo = new Mock<IBrainstormSessionRepository>();
  90:              mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
  91:                  .Returns(Task.FromResult((BrainstormSession)null));
  92:              var controller = new IdeasController(mockRepo.Object);
  93:   
  94:              // Act
  95:              var result = await controller.ForSession(testSessionId);
  96:   
  97:              // Assert
  98:              var notFoundObjectResult = Assert.IsType<NotFoundObjectResult>(result);
  99:              Assert.Equal(testSessionId, notFoundObjectResult.Value);
 100:          }
 101:   
 102:          [Fact]
 103:          public async Task ForSession_ReturnsIdeasForSession()
 104:          {
 105:              // Arrange
 106:              int testSessionId = 123;
 107:              var mockRepo = new Mock<IBrainstormSessionRepository>();
 108:              mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId)).Returns(Task.FromResult(GetTestSession()));
 109:              var controller = new IdeasController(mockRepo.Object);
 110:   
 111:              // Act
 112:              var result = await controller.ForSession(testSessionId);
 113:   
 114:              // Assert
 115:              var okResult = Assert.IsType<OkObjectResult>(result);
 116:              var returnValue = Assert.IsType<List<IdeaDTO>>(okResult.Value);
 117:              var idea = returnValue.FirstOrDefault();
 118:              Assert.Equal("One", idea.Name);
 119:          }
 120:   
 121:          private BrainstormSession GetTestSession()
 122:          {
 123:              var session = new BrainstormSession()
 124:              {
 125:                  DateCreated = new DateTime(2016, 7, 2),
 126:                  Id = 1,
 127:                  Name = "Test One"
 128:              };
 129:   
 130:              var idea = new Idea() { Name = "One" };
 131:              session.AddIdea(idea);
 132:              return session;
 133:          }
 134:      }
 135:  }

As stated previously, to test the behavior of the method when ModelState is invalid, add a model error to the controller as part of the test. Don’t try to test model validation or model binding in your unit tests - just test your action method’s behavior when confronted with a particular ModelState value.

The second test depends on the repository returning null, so the mock repository is configured to return null. There’s no need to create a test database (in memory or otherwise) and construct a query that will return this result - it can be done in a single statement as shown.

The last test verifies that the repository’s Update method is called. As we did previously, the mock is called with Verifiable and then the mocked repository’s Verify method is called to confirm the verifiable method was executed. It’s not a unit test responsibility to ensure that the Update method saved the data; that can be done with an integration test.

Integration testing

Integration testing is done to ensure separate modules within your app work correctly together. Generally, anything you can test with a unit test, you can also test with an integration test, but the reverse isn’t true. However, integration tests tend to be much slower than unit tests. Thus, it’s best to test whatever you can with unit tests, and use integration tests for scenarios that involve multiple collaborators.

Although they may still be useful, mock objects are rarely used in integration tests. In unit testing, mock objects are an effective way to control how collaborators outside of the unit being tested should behave for the purposes of the test. In an integration test, real collaborators are used to confirm the whole subsystem works together correctly.

Application state

One important consideration when performing integration testing is how to set your app’s state. Tests need to run independent of one another, and so each test should start with the app in a known state. If your app doesn’t use a database or have any persistence, this may not be an issue. However, most real-world apps persist their state to some kind of data store, so any modifications made by one test could impact another test unless the data store is reset. Using the built-in TestServer, it’s very straightforward to host ASP.NET Core apps within our integration tests, but that doesn’t necessarily grant access to the data it will use. If you’re using an actual database, one approach is to have the app connect to a test database, which your tests can access and ensure is reset to a known state before each test executes.

In this sample application, I’m using Entity Framework Core’s InMemoryDatabase support, so I can’t just connect to it from my test project. Instead, I expose an InitializeDatabase method from the app’s Startup class, which I call when the app starts up if it’s in the Development environment. My integration tests automatically benefit from this as long as they set the environment to Development. I don’t have to worry about resetting the database, since the InMemoryDatabase is reset each time the app restarts.

The Startup class:

[!code-csharpMain]

   1:  using System;
   2:  using System.Linq;
   3:  using System.Threading.Tasks;
   4:  using Microsoft.AspNetCore.Builder;
   5:  using Microsoft.AspNetCore.Hosting;
   6:  using Microsoft.EntityFrameworkCore;
   7:  using Microsoft.Extensions.DependencyInjection;
   8:  using Microsoft.Extensions.Logging;
   9:  using TestingControllersSample.Core.Interfaces;
  10:  using TestingControllersSample.Core.Model;
  11:  using TestingControllersSample.Infrastructure;
  12:   
  13:  namespace TestingControllersSample
  14:  {
  15:      public class Startup
  16:      {
  17:          public void ConfigureServices(IServiceCollection services)
  18:          {
  19:              services.AddDbContext<AppDbContext>(
  20:                  optionsBuilder => optionsBuilder.UseInMemoryDatabase("InMemoryDb"));
  21:   
  22:              services.AddMvc();
  23:   
  24:              services.AddScoped<IBrainstormSessionRepository,
  25:                  EFStormSessionRepository>();
  26:          }
  27:   
  28:          public void Configure(IApplicationBuilder app,
  29:              IHostingEnvironment env,
  30:              ILoggerFactory loggerFactory)
  31:          {
  32:              if (env.IsDevelopment())
  33:              {
  34:                  var repository = app.ApplicationServices.GetService<IBrainstormSessionRepository>();
  35:                  InitializeDatabaseAsync(repository).Wait();
  36:              }
  37:   
  38:              app.UseStaticFiles();
  39:   
  40:              app.UseMvcWithDefaultRoute();
  41:          }
  42:   
  43:          public async Task InitializeDatabaseAsync(IBrainstormSessionRepository repo)
  44:          {
  45:              var sessionList = await repo.ListAsync();
  46:              if (!sessionList.Any())
  47:              {
  48:                  await repo.AddAsync(GetTestSession());
  49:              }
  50:          }
  51:   
  52:          public static BrainstormSession GetTestSession()
  53:          {
  54:              var session = new BrainstormSession()
  55:              {
  56:                  Name = "Test Session 1",
  57:                  DateCreated = new DateTime(2016, 8, 1)
  58:              };
  59:              var idea = new Idea()
  60:              {
  61:                  DateCreated = new DateTime(2016, 8, 1),
  62:                  Description = "Totally awesome idea",
  63:                  Name = "Awesome idea"
  64:              };
  65:              session.AddIdea(idea);
  66:              return session;
  67:          }
  68:      }
  69:  }

You’ll see the GetTestSession method used frequently in the integration tests below.

Accessing views

Each integration test class configures the TestServer that will run the ASP.NET Core app. By default, TestServer hosts the web app in the folder where it’s running - in this case, the test project folder. Thus, when you attempt to test controller actions that return ViewResult, you may see this error:

The view 'Index' was not found. The following locations were searched:
(list of locations)

To correct this issue, you need to configure the server’s content root, so it can locate the views for the project being tested. This is done by a call to UseContentRoot in the TestFixture class, shown below:

[!code-csharpMain]

   1:  using System;
   2:  using System.IO;
   3:  using System.Net.Http;
   4:  using System.Reflection;
   5:  using Microsoft.AspNetCore.Hosting;
   6:  using Microsoft.AspNetCore.Mvc.ApplicationParts;
   7:  using Microsoft.AspNetCore.Mvc.Controllers;
   8:  using Microsoft.AspNetCore.Mvc.ViewComponents;
   9:  using Microsoft.AspNetCore.TestHost;
  10:  using Microsoft.Extensions.DependencyInjection;
  11:   
  12:  namespace TestingControllersSample.Tests.IntegrationTests
  13:  {
  14:      /// <summary>
  15:      /// A test fixture which hosts the target project (project we wish to test) in an in-memory server.
  16:      /// </summary>
  17:      /// <typeparam name="TStartup">Target project's startup type</typeparam>
  18:      public class TestFixture<TStartup> : IDisposable
  19:      {
  20:          private readonly TestServer _server;
  21:   
  22:          public TestFixture()
  23:              : this(Path.Combine("src"))
  24:          {
  25:          }
  26:   
  27:          protected TestFixture(string relativeTargetProjectParentDir)
  28:          {
  29:              var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
  30:              var contentRoot = GetProjectPath(relativeTargetProjectParentDir, startupAssembly);
  31:   
  32:              var builder = new WebHostBuilder()
  33:                  .UseContentRoot(contentRoot)
  34:                  .ConfigureServices(InitializeServices)
  35:                  .UseEnvironment("Development")
  36:                  .UseStartup(typeof(TStartup));
  37:   
  38:              _server = new TestServer(builder);
  39:   
  40:              Client = _server.CreateClient();
  41:              Client.BaseAddress = new Uri("http://localhost");
  42:          }
  43:   
  44:          public HttpClient Client { get; }
  45:   
  46:          public void Dispose()
  47:          {
  48:              Client.Dispose();
  49:              _server.Dispose();
  50:          }
  51:   
  52:          protected virtual void InitializeServices(IServiceCollection services)
  53:          {
  54:              var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
  55:   
  56:              // Inject a custom application part manager. 
  57:              // Overrides AddMvcCore() because it uses TryAdd().
  58:              var manager = new ApplicationPartManager();
  59:              manager.ApplicationParts.Add(new AssemblyPart(startupAssembly));
  60:              manager.FeatureProviders.Add(new ControllerFeatureProvider());
  61:              manager.FeatureProviders.Add(new ViewComponentFeatureProvider());
  62:   
  63:              services.AddSingleton(manager);
  64:          }
  65:   
  66:          /// <summary>
  67:          /// Gets the full path to the target project that we wish to test
  68:          /// </summary>
  69:          /// <param name="projectRelativePath">
  70:          /// The parent directory of the target project.
  71:          /// e.g. src, samples, test, or test/Websites
  72:          /// </param>
  73:          /// <param name="startupAssembly">The target project's assembly.</param>
  74:          /// <returns>The full path to the target project.</returns>
  75:          private static string GetProjectPath(string projectRelativePath, Assembly startupAssembly)
  76:          {
  77:              // Get name of the target project which we want to test
  78:              var projectName = startupAssembly.GetName().Name;
  79:   
  80:              // Get currently executing test project path
  81:              var applicationBasePath = System.AppContext.BaseDirectory;
  82:   
  83:              // Find the path to the target project
  84:              var directoryInfo = new DirectoryInfo(applicationBasePath);
  85:              do
  86:              {
  87:                  directoryInfo = directoryInfo.Parent;
  88:   
  89:                  var projectDirectoryInfo = new DirectoryInfo(Path.Combine(directoryInfo.FullName, projectRelativePath));
  90:                  if (projectDirectoryInfo.Exists)
  91:                  {
  92:                      var projectFileInfo = new FileInfo(Path.Combine(projectDirectoryInfo.FullName, projectName, $"{projectName}.csproj"));
  93:                      if (projectFileInfo.Exists)
  94:                      {
  95:                          return Path.Combine(projectDirectoryInfo.FullName, projectName);
  96:                      }
  97:                  }
  98:              }
  99:              while (directoryInfo.Parent != null);
 100:   
 101:              throw new Exception($"Project root could not be located using the application root {applicationBasePath}.");
 102:          }
 103:      }
 104:  }

The TestFixture class is responsible for configuring and creating the TestServer, setting up an HttpClient to communicate with the TestServer. Each of the integration tests uses the Client property to connect to the test server and make a request.

[!code-csharpMain]

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Net;
   4:  using System.Net.Http;
   5:  using System.Threading.Tasks;
   6:  using Xunit;
   7:   
   8:  namespace TestingControllersSample.Tests.IntegrationTests
   9:  {
  10:      public class HomeControllerTests : IClassFixture<TestFixture<TestingControllersSample.Startup>>
  11:      {
  12:          private readonly HttpClient _client;
  13:   
  14:          public HomeControllerTests(TestFixture<TestingControllersSample.Startup> fixture)
  15:          {
  16:              _client = fixture.Client;
  17:          }
  18:   
  19:          [Fact]
  20:          public async Task ReturnsInitialListOfBrainstormSessions()
  21:          {
  22:              // Arrange - get a session known to exist
  23:              var testSession = Startup.GetTestSession();
  24:   
  25:              // Act
  26:              var response = await _client.GetAsync("/");
  27:   
  28:              // Assert
  29:              response.EnsureSuccessStatusCode();
  30:              var responseString = await response.Content.ReadAsStringAsync();
  31:              Assert.Contains(testSession.Name, responseString);
  32:          }
  33:   
  34:          [Fact]
  35:          public async Task PostAddsNewBrainstormSession()
  36:          {
  37:              // Arrange
  38:              string testSessionName = Guid.NewGuid().ToString();
  39:              var data = new Dictionary<string, string>();
  40:              data.Add("SessionName", testSessionName);
  41:              var content = new FormUrlEncodedContent(data);
  42:   
  43:              // Act
  44:              var response = await _client.PostAsync("/", content);
  45:   
  46:              // Assert
  47:              Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
  48:              Assert.Equal("/", response.Headers.Location.ToString());
  49:          }
  50:      }
  51:  }

In the first test above, the responseString holds the actual rendered HTML from the View, which can be inspected to confirm it contains expected results.

The second test constructs a form POST with a unique session name and POSTs it to the app, then verifies that the expected redirect is returned.

API methods

If your app exposes web APIs, it’s a good idea to have automated tests confirm they execute as expected. The built-in TestServer makes it easy to test web APIs. If your API methods are using model binding, you should always check ModelState.IsValid, and integration tests are the right place to confirm that your model validation is working properly.

The following set of tests target the Create method in the (xref:)IdeasController class shown above:

[!code-csharpMain]

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Net;
   5:  using System.Net.Http;
   6:  using System.Threading.Tasks;
   7:  using Newtonsoft.Json;
   8:  using TestingControllersSample.ClientModels;
   9:  using TestingControllersSample.Core.Model;
  10:  using Xunit;
  11:   
  12:  namespace TestingControllersSample.Tests.IntegrationTests
  13:  {
  14:      public class ApiIdeasControllerTests : IClassFixture<TestFixture<TestingControllersSample.Startup>>
  15:      {
  16:          internal class NewIdeaDto
  17:          {
  18:              public NewIdeaDto(string name, string description, int sessionId)
  19:              {
  20:                  Name = name;
  21:                  Description = description;
  22:                  SessionId = sessionId;
  23:              }
  24:   
  25:              public string Name { get; set; }
  26:              public string Description { get; set; }
  27:              public int SessionId { get; set; }
  28:          }
  29:   
  30:          private readonly HttpClient _client;
  31:   
  32:          public ApiIdeasControllerTests(TestFixture<TestingControllersSample.Startup> fixture)
  33:          {
  34:              _client = fixture.Client;
  35:          }
  36:   
  37:          [Fact]
  38:          public async Task CreatePostReturnsBadRequestForMissingNameValue()
  39:          {
  40:              // Arrange
  41:              var newIdea = new NewIdeaDto("", "Description", 1);
  42:   
  43:              // Act
  44:              var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);
  45:   
  46:              // Assert
  47:              Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
  48:          }
  49:   
  50:          [Fact]
  51:          public async Task CreatePostReturnsBadRequestForMissingDescriptionValue()
  52:          {
  53:              // Arrange
  54:              var newIdea = new NewIdeaDto("Name", "", 1);
  55:   
  56:              // Act
  57:              var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);
  58:   
  59:              // Assert
  60:              Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
  61:          }
  62:   
  63:          [Fact]
  64:          public async Task CreatePostReturnsBadRequestForSessionIdValueTooSmall()
  65:          {
  66:              // Arrange
  67:              var newIdea = new NewIdeaDto("Name", "Description", 0);
  68:   
  69:              // Act
  70:              var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);
  71:   
  72:              // Assert
  73:              Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
  74:          }
  75:   
  76:          [Fact]
  77:          public async Task CreatePostReturnsBadRequestForSessionIdValueTooLarge()
  78:          {
  79:              // Arrange
  80:              var newIdea = new NewIdeaDto("Name", "Description", 1000001);
  81:   
  82:              // Act
  83:              var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);
  84:   
  85:              // Assert
  86:              Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
  87:          }
  88:   
  89:          [Fact]
  90:          public async Task CreatePostReturnsNotFoundForInvalidSession()
  91:          {
  92:              // Arrange
  93:              var newIdea = new NewIdeaDto("Name", "Description", 123);
  94:   
  95:              // Act
  96:              var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);
  97:   
  98:              // Assert
  99:              Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
 100:          }
 101:   
 102:          [Fact]
 103:          public async Task CreatePostReturnsCreatedIdeaWithCorrectInputs()
 104:          {
 105:              // Arrange
 106:              var testIdeaName = Guid.NewGuid().ToString();
 107:              var newIdea = new NewIdeaDto(testIdeaName, "Description", 1);
 108:   
 109:              // Act
 110:              var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);
 111:   
 112:              // Assert
 113:              response.EnsureSuccessStatusCode();
 114:              var returnedSession = await response.Content.ReadAsJsonAsync<BrainstormSession>();
 115:              Assert.Equal(2, returnedSession.Ideas.Count);
 116:              Assert.Contains(testIdeaName, returnedSession.Ideas.Select(i => i.Name).ToList());
 117:          }
 118:   
 119:          [Fact]
 120:          public async Task ForSessionReturnsNotFoundForBadSessionId()
 121:          {
 122:              // Arrange & Act
 123:              var response = await _client.GetAsync("/api/ideas/forsession/500");
 124:   
 125:              // Assert
 126:              Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
 127:          }
 128:   
 129:          [Fact]
 130:          public async Task ForSessionReturnsIdeasForValidSessionId()
 131:          {
 132:              // Arrange
 133:              var testSession = Startup.GetTestSession();
 134:   
 135:              // Act
 136:              var response = await _client.GetAsync("/api/ideas/forsession/1");
 137:   
 138:              // Assert
 139:              response.EnsureSuccessStatusCode();
 140:              var ideaList = JsonConvert.DeserializeObject<List<IdeaDTO>>(
 141:                  await response.Content.ReadAsStringAsync());
 142:              var firstIdea = ideaList.First();
 143:              Assert.Equal(testSession.Ideas.First().Name, firstIdea.Name);
 144:          }
 145:      }
 146:  }

Unlike integration tests of actions that returns HTML views, web API methods that return results can usually be deserialized as strongly typed objects, as the last test above shows. In this case, the test deserializes the result to a BrainstormSession instance, and confirms that the idea was correctly added to its collection of ideas.

You’ll find additional examples of integration tests in this article’s sample project.





Comments ( )
<00>  <01>  <02>  <03>  <04>  <05>  <06>  <07>  <08>  <09>  <10>  <11>  <12>  <13>  <14>  <15>  <16>  <17>  <18>  <19>  <20>  <21>  <22>  <23
Link to this page: //www.vb-net.com/AspNet-DocAndSamples-2017/aspnetcore/mvc/controllers/testing.htm
<SITEMAP>  <MVC>  <ASP>  <NET>  <DATA>  <KIOSK>  <FLEX>  <SQL>  <NOTES>  <LINUX>  <MONO>  <FREEWARE>  <DOCS>  <ENG>  <CHAT ME>  <ABOUT ME>  < THANKS ME>