UPDATE (8 Jun 2021): This series has been updated to use ASP.NET 5.0. It previously used ASP.NET Core 3.0.

Let's continue our unit test extravaganza by writing a set of unit tests for our ASP.NET 5.0 MVC Controllers!

Support me: https://paypal.me/ugurakdemir
Instagram: https://www.instagram.com/ugur_akdemir/
I wonder how many unit tests these controllers have? Photo by Ugur Akdemir / Unsplash

We've already seen why and where we want to write unit tests, how to use Moq to create fluent mocked classes to make our tests more readable, and even how to unit test the business layer of our sample app.  Now, let's continue our test-writing spree and work up a bunch of unit tests for the MVC Controller classes in our sample app!

Here's the sample application for this post.

exceptionnotfound/XUnitMockTestsDemo
Contribute to exceptionnotfound/XUnitMockTestsDemo development by creating an account on GitHub.

Unit Testing the LeagueController

First, let's look at our LeagueController class.

public class LeagueController : Controller
{
    private readonly ILeagueService _leagueService;

    public LeagueController(ILeagueService leagueService)
    {
        _leagueService = leagueService;
    }

    public IActionResult Index()
    {
        var leagues = _leagueService.GetAll();
        return View(leagues);
    }
}

There's only one action here, Index(), so we only need to consider the test cases for that action. Given that there's no inputs, I only see two test scenarios:

  1. Leagues are NOT returned.
  2. Leagues ARE returned.

Therefore our tests should match these scenarios.

Unit Test Scenario #1: Leagues Not Returned

[Fact]
public void LeagueController_Index_NoLeagues()
{
    //Arrange
    var mockLeagueService = new MockLeagueService().MockGetAll(new List<League>());

    var controller = new LeagueController(mockLeagueService.Object);

    //Act
    var result = controller.Index();

    //Assert
    Assert.IsAssignableFrom<ViewResult>(result);
    mockLeagueService.VerifyGetAll(Times.Once());
}

Unit Test Scenario #2: Leagues Found

[Fact]
public void LeagueController_Index_LeaguesExist()
{
    //Arrange
    var mockLeagues = new List<League>()
    {
        new League()
        {
            ID = 1,
            FoundingDate = new DateTime(1933, 5, 3)
        }
    };

    var mockLeagueService = new MockLeagueService().MockGetAll(mockLeagues);

    var controller = new LeagueController(mockLeagueService.Object);

    //Act
    var result = controller.Index();

    //Assert
    Assert.IsAssignableFrom<ViewResult>(result);
    mockLeagueService.VerifyGetAll(Times.Once());
}

So far, our tests have not been noticeably different in practice from when we unit tested the business layer of this app. That changes (slightly) when we try to write tests for the TeamController class.

Unit Testing the TeamController

Here's the code for the TeamController class:

public class TeamController : Controller
{
    private readonly ITeamService _teamService;

    public TeamController(ITeamService teamService)
    {
        _teamService = teamService;
    }

    [HttpGet]
    public IActionResult Search()
    {
        var teamSearch = new TeamSearch();
        return View(teamSearch);
    }

    [HttpPost]
    public IActionResult Search(TeamSearch search)
    {
        if(!ModelState.IsValid)
        {
            return RedirectToAction("Search"); //POST-REDIRECT-GET pattern assumed, but not implemented.
        }

        var results = _teamService.Search(search);

        if(!results.Any())
        {
            return RedirectToAction("Search");
        }
        else
        {
            search.Results = results;
            return View(search);
        }
    }
}

Now we have two actions, and one of those actions relies on ModelState to make logical decisions.

From these actions, I see four test scenarios:

  1. In the GET version of Search() action, test that a ViewResult is returned. No additional tests are needed, since that is the only possible outcome of this action as it is currently written.
  2. In the POST version, if ModelState.IsValid is false, test that a RedirectToActionResult is returned.
  3. In the POST version, if ModelState.IsValid is true AND there are no results from the search, test that a RedirectToActionResult is returned.
  4. In the POST version, if ModelState.IsValid is true AND there ARE results from the search, test that a ViewResult is returned.

You may be wondering why scenarios 4 and 5 are listed separately, given that they are expected to return the same type under similar conditions. The primary reason is that in Scenario 5, the method _teamService.Search() is expected to be called, whereas in Scenario 4, it will not be. Because unit testing is also regression testing, we want to know when the action changes its dependencies, and unit testing those scenarios separately is a good way to do that.

With these scenarios, let's write the tests!

Unit Test Scenario #3: Search() GET

[Fact]
public void TeamController_Search_Get_Valid()
{
    //Arrange
    var teamResults = new List<Team>()
    {
        new Team() { ID = 1 }
    };

    var mockTeamService = new MockTeamService().MockSearch(teamResults);

    var controller = new TeamController(mockTeamService.Object);

    //Act
    var result = controller.Search();

    //Assert
    Assert.IsAssignableFrom<ViewResult>(result);
}

Note the use of the Assert.IsAssignableFrom<>() method. This is the preferred way to check for the type of IActionResult that is normally returned from ASP.NET 5.0 MVC Controller classes.

Unit Test Scenario #4: Search POST, ModelState Not Valid

[Fact]
public void TeamController_Search_Post_ModelStateInvalid()
{
    //Arrange
    var teamResults = new List<Team>()
    {
        new Team() { ID = 1 }
    };

    var mockTeamService = new MockTeamService().MockSearch(teamResults);

    var controller = new TeamController(mockTeamService.Object);
    controller.ModelState.AddModelError("Test", "Test");

    //Act
    var result = controller.Search(new TeamSearch());

    //Assert
    Assert.IsAssignableFrom<RedirectToActionResult>(result);
    mockTeamService.VerifySearch(Times.Never());

    var redirectToAction = (RedirectToActionResult)result; //See note below.
    Assert.Equal("Search", redirectToAction.ActionName);
}

I want to call special attention to the last two lines in this unit test.  It is possible to check if the redirected action is the action that we expect, and this is how to do it. You may want to do this when a single action can redirect to multiple different places, depending on the inputs and logic of the method.

Unit Test Scenario #5: Search POST, ModelState Valid, No Results

[Fact]
public void TeamController_Search_Post_NoResults()
{
    //Arrange
    var mockTeamService = new MockTeamService().MockSearch(new List<Team>());

    var controller = new TeamController(mockTeamService.Object);

    //Act
    var result = controller.Search(new TeamSearch());

    //Assert
    Assert.IsAssignableFrom<RedirectToActionResult>(result);
    mockTeamService.VerifySearch(Times.Once());
}

Unit Test Scenario #6: Search POST, ModelState Valid, With Results

[Fact]
public void TeamController_Search_Post_Valid()
{
    //Arrange
    var teamResults = new List<Team>()
    {
        new Team() { ID = 1 }
    };
    var mockTeamService = new MockTeamService().MockSearch(teamResults);

    var controller = new TeamController(mockTeamService.Object);

    //Act
    var result = controller.Search(new TeamSearch());

    //Assert
    Assert.IsAssignableFrom<ViewResult>(result);
    mockTeamService.VerifySearch(Times.Once());
}

Unit Testing the PlayerController

Finally, let's consider the PlayerController class:

public class PlayerController : Controller
{
    private readonly ILeagueService _leagueService;
    private readonly IPlayerService _playerService;

    public PlayerController(ILeagueService leagueService,
                            IPlayerService playerService)
    {
        _leagueService = leagueService;
        _playerService = playerService;
    }

    public IActionResult Index(int id)
    {
        try
        {
            var player = _playerService.GetByID(id);
            return View(player);
        }
        catch (Exception)
        {
            return RedirectToAction("Error", "Home");
        }
    }

    public IActionResult League(int id)
    {
        if (!_leagueService.IsValid(id))
            return RedirectToAction("Error", "Home");

        var players = _playerService.GetForLeague(id);
        return View(players);
    }
}

There are two actions in this controller, each with two outcomes that can be tested, for a total of four scenarios.  Let's list them:

  1. In the Index() action, an invalid Player ID returns a RedirectToActionResult.
  2. In the Index() action, a valid Player ID returns a ViewResult.
  3. In the League() action, an invalid League ID returns a RedirectToActionResult.
  4. In the League() action, a valid League ID returns a ``ViewResult```.

Now, we can write the unit tests for these scenarios.

Unit Test Scenario #7: Index() Action, Invalid Player ID

[Fact]
public void PlayerController_Index_Invalid()
{
    //Arrange
    var mockPlayerService = new MockPlayerService().MockGetByIDInvalid();

    var controller = new PlayerController(new MockLeagueService().Object,
                                          mockPlayerService.Object);

    //Act
    var result = controller.Index(1); //ID doesn't matter

    //Assert
    Assert.IsAssignableFrom<RedirectToActionResult>(result);
    mockPlayerService.VerifyGetByID(Times.Once());
}

Unit Test Scenario #8: Index() Action, Valid Player ID

[Fact]
public void PlayerController_Index_Valid()
{
    //Arrange
    var mockPlayer = new Player()
    {
        ID = 1
    };

    var mockPlayerService = new MockPlayerService().MockGetByID(mockPlayer);

    var controller = new PlayerController(new MockLeagueService().Object,
                                          mockPlayerService.Object);

    //Act
    var result = controller.Index(1); //ID doesn't matter

    //Assert
    Assert.IsAssignableFrom<ViewResult>(result);
    mockPlayerService.VerifyGetByID(Times.Once());
}

Unit Test Scenario #9: League() Action, Invalid League ID

[Fact]
public void PlayerController_League_Invalid()
{
    //Arrange
    var mockLeagueService = new MockLeagueService().MockIsValid(false);
    var mockPlayerService = new MockPlayerService().MockGetForLeague(new List<Player>());

    var controller = new PlayerController(mockLeagueService.Object,
                                          mockPlayerService.Object);

    //Act
    var result = controller.League(1); //ID doesn't matter

    //Assert
    Assert.IsAssignableFrom<RedirectToActionResult>(result);
    mockPlayerService.VerifyGetForLeague(Times.Never());
    mockLeagueService.VerifyIsValid(Times.Once());
}

Note that in this scenario we want to confirm that _playerService.GetForLeague() was never called.

Unit Test Scenario #10: League() Action, Valid League ID

[Fact]
public void PlayerController_League_Valid()
{
    //Arrange
    var mockPlayers = new List<Player>()
    {
        new Player()
        {
            ID = 1
        }
    };

    var mockLeagueService = new MockLeagueService().MockIsValid(true);
    var mockPlayerService = new MockPlayerService().MockGetForLeague(mockPlayers);

    var controller = new PlayerController(mockLeagueService.Object,
                                          mockPlayerService.Object);

    //Act
    var result = controller.League(1); //ID doesn't matter

    //Assert
    Assert.IsAssignableFrom<ViewResult>(result);
    mockPlayerService.VerifyGetForLeague(Times.Once());
    mockLeagueService.VerifyIsValid(Times.Once());
}

Remarks

The primary differences you see when setting up unit tests for ASP.NET MVC Controller are:

  • You must manually set attributes of the Controller class to be what you expect them to be (e.g. Unit Test Scenario #4, where we set ModelState.IsValid to be false). This also means if, for example, you need values in other properties of the controller object, such as Request, to be set for your function to be tested, you must set them before the test is run.
  • It is preferable to use Assert.IsAssignableFrom<>() to check if the type of the returned IActionResult is what you expect it to be.

Summary

Writing unit tests for ASP.NET 5.0 MVC Controller is not too different from unit testing other classes, with the main exceptions of setting up the controller class and using Assert.IsAssignableFrom<>() to check the results of actions.

See a way I can improve the above unit tests?  Did you do something similar, and want to let us know about it?  Share in the comments!

Don't forget to check out the sample project over on GitHub!

In the next and final post in this series, we will test a C# extension method using XUnit's [Theory] and [InlineData] attributes, showing how you can run many tests with the same expected outcome in just a few lines of code.

Using XUnit [Theory] and [InlineData] to Test C# Extension Methods
Let’s unit test a C# extension method using XUnit’s [Theory] and [InlineData] attributes, so we can write lots of tests in little time!

Happy Testing!