8.0 KiB
title | author | description | ms.author | ms.date | uid |
---|---|---|---|---|---|
Test controller logic in ASP.NET Core | ardalis | Learn how to test controller logic in ASP.NET Core with Moq and xUnit. | riande | 10/14/2016 | mvc/controllers/testing |
Test controller logic in ASP.NET Core
By Steve Smith
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:
- Verify
ModelState.IsValid
. - Return an error response if
ModelState
is invalid. - Retrieve a business entity from persistence.
- Perform an action on the business entity.
- Save the business entity to persistence.
- Return an appropriate
IActionResult
.
View or download sample code (how to download)
Unit tests of controller logic
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 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 don't detect issues in the interaction between components, which is the purpose of integration tests.
If you're writing custom filters and routes, you should unit test them in isolation, not as part of your tests on a particular controller action.
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:
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.
The HomeController
HTTP POST Index
method (shown above) should verify:
-
The action method returns a Bad Request
ViewResult
with the appropriate data whenModelState.IsValid
isfalse
. -
The
Add
method on the repository is called and aRedirectToActionResult
is returned with the correct arguments whenModelState.IsValid
is true.
Invalid model state can be tested by adding errors using AddModelError
as shown in the first test below.
The first test confirms when ModelState
isn't 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 integration test would use exercise model binding). In this case, model binding isn't 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 wasn't 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:
The controller action has three cases to test, one for each return
statement:
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):
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:
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.