Overview [ Documentation]
Unit testing controller logic involves testing a single action (not the dependencies of that action). It does not test filters, routing, model binding, or model validation (these aspects are tested in integration testing).
These notes also apply:
See also: Moq
See also: JustMockLite
See also: MyTested.AspNetCore.Mvc
Example
Consider this controller with an Index
action method:
public class HomeController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;
public HomeController(IBrainstormSessionRepository sessionRepository)
{
_sessionRepository = sessionRepository;
}
public async Task<IActionResult> Index()
{
var sessionList = await _sessionRepository.ListAsync();
var model = sessionList.Select(session => new StormSessionViewModel()
{
Id = session.Id,
DateCreated = session.DateCreated,
Name = session.Name,
IdeaCount = session.Ideas.Count
});
return View(model);
}
public class NewSessionModel
{
[Required]
public string SessionName { get; set; }
}
[HttpPost]
public async Task<IActionResult> Index(NewSessionModel model)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
else
{
await _sessionRepository.AddAsync(new BrainstormSession()
{
DateCreated = DateTimeOffset.Now,
Name = model.SessionName
});
}
return RedirectToAction(actionName: nameof(Index));
}
}
Create test sessions:
private List<BrainstormSession> GetTestSessions()
{
var sessions = new List<BrainstormSession>();
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 2),
Id = 1,
Name = "Test One"
});
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 1),
Id = 2,
Name = "Test Two"
});
return sessions;
}
The unit test for this action:
- Confirms a
ViewResult
is returned - Confirms the
ViewDataDictionary.Model
is aStormSessionViewModel
- Confirms there are two brainstorming sessions stored in
ViewDataDictionary.Model
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync()).ReturnsAsync(GetTestSessions());
var controller = new HomeController(mockRepo.Object);
// Act
var result = await controller.Index();
// Assert
var viewResult = Assert.IsType<ViewResult>(result); // #1
var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(viewResult.ViewData.Model); // #2
Assert.Equal(2, model.Count()); // #3
}
The unit test for the HTTP Post Index method:
- Confirms that when
ModelState.IsValid
isfalse
, the action method returns an HTTP 400ViewResult
- Confirms that when
ModelState.IsValid
istrue
:- The
Add
method on the repository is called - A
RedirectToActionResult
is returned
- The
[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(GetTestSessions());
var controller = new HomeController(mockRepo.Object);
// adds errors to test the invalid model state
controller.ModelState.AddModelError("SessionName", "Required");
var newSession = new HomeController.NewSessionModel();
// Act
var result = await controller.Index(newSession);
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result); // #1
Assert.IsType<SerializableError>(badRequestResult.Value); // #1
}
[Fact]
public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
.Returns(Task.CompletedTask)
.Verifiable();
var controller = new HomeController(mockRepo.Object);
var newSession = new HomeController.NewSessionModel() { SessionName = "Test Name" };
// Act
var result = await controller.Index(newSession);
// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result); // #2.2
Assert.Null(redirectToActionResult.ControllerName); // #2.2
Assert.Equal("Index", redirectToActionResult.ActionName); // #2.2
mockRepo.Verify(); // Fails the test if the expected method wasn't called (#2.1)
}