aspnetcore/mvc/controllers/testing.md
By Steve Smith
:::moniker range=">= aspnetcore-3.0"
Unit tests involve 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 are tested, not the behavior of its dependencies or of the framework itself.
Set up unit tests of controller actions to focus on the controller's behavior. A controller unit test avoids scenarios such as filters, routing, and model binding. Tests that cover the interactions among components that collectively respond to a request are handled by integration tests. For more information on integration tests, see xref:test/integration-tests.
If you're writing custom filters and routes, unit test them in isolation, not as part of tests on a particular controller action.
To demonstrate controller unit tests, review the following controller in the sample app.
View or download sample code (how to download)
The Home controller displays a list of brainstorming sessions and allows the creation of new brainstorming sessions with a POST request:
The preceding controller:
IBrainstormSessionRepository.IBrainstormSessionRepository service using a mock object framework, such as Moq. A mocked object is a fabricated object with a predetermined set of property and method behaviors used for testing. For more information, see Introduction to integration tests.The HTTP GET Index method has no looping or branching and only calls one method. The unit test for this action:
IBrainstormSessionRepository service using the GetTestSessions method. GetTestSessions creates two mock brainstorm sessions with dates and session names.Index method.StormSessionViewModel.ViewDataDictionary.Model.The Home controller's HTTP POST Index method tests verifies that:
false, the action method returns a 400 Bad Request xref:Microsoft.AspNetCore.Mvc.ViewResult with the appropriate data.ModelState.IsValid is true:
Add method on the repository is called.An invalid model state is tested by adding errors using xref:Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary.AddModelError* as shown in the first test below:
When ModelState isn't valid, the same ViewResult is returned as for a GET request. The test doesn't attempt to pass in an invalid model. Passing an invalid model isn't a valid approach, since model binding isn't running (although an integration test does use model binding). In this case, model binding isn't tested. These unit tests are only testing the code in the action method.
The second test verifies that when the ModelState is valid:
BrainstormSession is added (via the repository).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 mock validation in the test. This is performed with the call to mockRepo.Verify, which fails the test if the expected method wasn't called.
[!NOTE] The Moq library used in this sample makes it possible to mix verifiable, or "strict", mocks with non-verifiable mocks (also called "loose" mocks or stubs). Learn more about customizing Mock behavior with Moq.
SessionController in the sample app displays information related to a particular brainstorming session. The controller includes logic to deal with invalid id values (there are two return scenarios in the following example to cover these scenarios). The final return statement returns a new StormSessionViewModel to the view (Controllers/SessionController.cs):
The unit tests include one test for each return scenario in the Session controller Index action:
Moving to the Ideas controller, the app exposes functionality as a web API on the api/ideas route:
IdeaDTO) associated with a brainstorming session is returned by the ForSession method.Create method adds new ideas to a session.Avoid returning business domain entities directly via API calls. Domain entities:
Mapping between domain entities and the types returned to the client can be performed:
Select, as the sample app uses. For more information, see LINQ (Language Integrated Query).Next, the sample app demonstrates unit tests for the Create and ForSession API methods of the Ideas controller.
The sample app contains two ForSession tests. The first test determines if ForSession returns a xref:Microsoft.AspNetCore.Mvc.NotFoundObjectResult (HTTP Not Found) for an invalid session:
The second ForSession test determines if ForSession returns a list of session ideas (<List<IdeaDTO>>) for a valid session. The checks also examine the first idea to confirm its Name property is correct:
To test the behavior of the Create method when the ModelState is invalid, the sample app adds a model error to the controller as part of the test. Don't try to test model validation or model binding in unit tests—just test the action method's behavior when confronted with an invalid ModelState:
The second test of Create 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 returns this result. The test can be accomplished in a single statement, as the sample code illustrates:
The third Create test, Create_ReturnsNewlyCreatedIdeaForSession, verifies that the repository's UpdateAsync method is called. The mock is called with Verifiable, and the mocked repository's Verify method is called to confirm the verifiable method is executed. It's not the unit test's responsibility to ensure that the UpdateAsync method saved the data—that can be performed with an integration test.
ActionResult<T> (xref:Microsoft.AspNetCore.Mvc.ActionResult%601) can return a type deriving from ActionResult or return a specific type.
The sample app includes a method that returns a List<IdeaDTO> for a given session id. If the session id doesn't exist, the controller returns xref:Microsoft.AspNetCore.Mvc.ControllerBase.NotFound*:
Two tests of the ForSessionActionResult controller are included in the ApiIdeasControllerTests.
The first test confirms that the controller returns an ActionResult but not a nonexistent list of ideas for a nonexistent session id:
ActionResult type is ActionResult<List<IdeaDTO>>.For a valid session id, the second test confirms that the method returns:
ActionResult with a List<IdeaDTO> type.List<IdeaDTO> type.GetTestSession).The sample app also includes a method to create a new Idea for a given session. The controller returns:
Three tests of CreateActionResult are included in the ApiIdeasControllerTests.
The first text confirms that a xref:Microsoft.AspNetCore.Mvc.ControllerBase.BadRequest* is returned for an invalid model.
The second test checks that a xref:Microsoft.AspNetCore.Mvc.ControllerBase.NotFound* is returned if the session doesn't exist.
For a valid session id, the final test confirms that:
ActionResult with a BrainstormSession type.CreatedAtActionResult is analogous to a 201 Created response with a Location header.BrainstormSession type.UpdateAsync(testSession), was invoked. The Verifiable method call is checked by executing mockRepo.Verify() in the assertions.Idea objects are returned for the session.Idea added by the mock call to UpdateAsync) matches the newIdea added to the session in the test.:::moniker-end
:::moniker range="< aspnetcore-3.0"
Controllers play a central role in any ASP.NET Core MVC app. As such, you should have confidence that controllers behave as intended. Automated tests can detect errors before the app is deployed to a production environment.
View or download sample code (how to download)
Unit tests involve 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 are tested, not the behavior of its dependencies or of the framework itself.
Set up unit tests of controller actions to focus on the controller's behavior. A controller unit test avoids scenarios such as filters, routing, and model binding. Tests that cover the interactions among components that collectively respond to a request are handled by integration tests. For more information on integration tests, see xref:test/integration-tests.
If you're writing custom filters and routes, unit test them in isolation, not as part of tests on a particular controller action.
To demonstrate controller unit tests, review the following controller in the sample app. The Home controller displays a list of brainstorming sessions and allows the creation of new brainstorming sessions with a POST request:
The preceding controller:
IBrainstormSessionRepository.IBrainstormSessionRepository service using a mock object framework, such as Moq. A mocked object is a fabricated object with a predetermined set of property and method behaviors used for testing. For more information, see Introduction to integration tests.The HTTP GET Index method has no looping or branching and only calls one method. The unit test for this action:
IBrainstormSessionRepository service using the GetTestSessions method. GetTestSessions creates two mock brainstorm sessions with dates and session names.Index method.StormSessionViewModel.ViewDataDictionary.Model.The Home controller's HTTP POST Index method tests verifies that:
false, the action method returns a 400 Bad Request xref:Microsoft.AspNetCore.Mvc.ViewResult with the appropriate data.ModelState.IsValid is true:
Add method on the repository is called.An invalid model state is tested by adding errors using xref:Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary.AddModelError* as shown in the first test below:
When ModelState isn't valid, the same ViewResult is returned as for a GET request. The test doesn't attempt to pass in an invalid model. Passing an invalid model isn't a valid approach, since model binding isn't running (although an integration test does use model binding). In this case, model binding isn't tested. These unit tests are only testing the code in the action method.
The second test verifies that when the ModelState is valid:
BrainstormSession is added (via the repository).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 mock validation in the test. This is performed with the call to mockRepo.Verify, which fails the test if the expected method wasn't called.
[!NOTE] The Moq library used in this sample makes it possible to mix verifiable, or "strict", mocks with non-verifiable mocks (also called "loose" mocks or stubs). Learn more about customizing Mock behavior with Moq.
SessionController in the sample app displays information related to a particular brainstorming session. The controller includes logic to deal with invalid id values (there are two return scenarios in the following example to cover these scenarios). The final return statement returns a new StormSessionViewModel to the view (Controllers/SessionController.cs):
The unit tests include one test for each return scenario in the Session controller Index action:
Moving to the Ideas controller, the app exposes functionality as a web API on the api/ideas route:
IdeaDTO) associated with a brainstorming session is returned by the ForSession method.Create method adds new ideas to a session.Avoid returning business domain entities directly via API calls. Domain entities:
Mapping between domain entities and the types returned to the client can be performed:
Select, as the sample app uses. For more information, see LINQ (Language Integrated Query).Next, the sample app demonstrates unit tests for the Create and ForSession API methods of the Ideas controller.
The sample app contains two ForSession tests. The first test determines if ForSession returns a xref:Microsoft.AspNetCore.Mvc.NotFoundObjectResult (HTTP Not Found) for an invalid session:
The second ForSession test determines if ForSession returns a list of session ideas (<List<IdeaDTO>>) for a valid session. The checks also examine the first idea to confirm its Name property is correct:
To test the behavior of the Create method when the ModelState is invalid, the sample app adds a model error to the controller as part of the test. Don't try to test model validation or model binding in unit tests—just test the action method's behavior when confronted with an invalid ModelState:
The second test of Create 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 returns this result. The test can be accomplished in a single statement, as the sample code illustrates:
The third Create test, Create_ReturnsNewlyCreatedIdeaForSession, verifies that the repository's UpdateAsync method is called. The mock is called with Verifiable, and the mocked repository's Verify method is called to confirm the verifiable method is executed. It's not the unit test's responsibility to ensure that the UpdateAsync method saved the data—that can be performed with an integration test.
In ASP.NET Core 2.1 or later, ActionResult<T> (xref:Microsoft.AspNetCore.Mvc.ActionResult%601) enables you to return a type deriving from ActionResult or return a specific type.
The sample app includes a method that returns a List<IdeaDTO> for a given session id. If the session id doesn't exist, the controller returns xref:Microsoft.AspNetCore.Mvc.ControllerBase.NotFound*:
Two tests of the ForSessionActionResult controller are included in the ApiIdeasControllerTests.
The first test confirms that the controller returns an ActionResult but not a nonexistent list of ideas for a nonexistent session id:
ActionResult type is ActionResult<List<IdeaDTO>>.For a valid session id, the second test confirms that the method returns:
ActionResult with a List<IdeaDTO> type.List<IdeaDTO> type.GetTestSession).The sample app also includes a method to create a new Idea for a given session. The controller returns:
Three tests of CreateActionResult are included in the ApiIdeasControllerTests.
The first text confirms that a xref:Microsoft.AspNetCore.Mvc.ControllerBase.BadRequest* is returned for an invalid model.
The second test checks that a xref:Microsoft.AspNetCore.Mvc.ControllerBase.NotFound* is returned if the session doesn't exist.
For a valid session id, the final test confirms that:
ActionResult with a BrainstormSession type.CreatedAtActionResult is analogous to a 201 Created response with a Location header.BrainstormSession type.UpdateAsync(testSession), was invoked. The Verifiable method call is checked by executing mockRepo.Verify() in the assertions.Idea objects are returned for the session.Idea added by the mock call to UpdateAsync) matches the newIdea added to the session in the test.:::moniker-end