AspNetCore.Docs/aspnetcore/mvc/controllers/testing.md

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.

[!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-csharp]

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-csharp]

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

  • The action method returns a Bad Request ViewResult with the appropriate data when ModelState.IsValid is false.

  • The Add method on the repository is called and a RedirectToActionResult is returned with the correct arguments when ModelState.IsValid is true.

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

[!code-csharp]

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:

[!code-csharp]

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

[!code-csharp]

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-csharp]

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-csharp]

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.

Additional resources