At this point, we've done enough setup; it's time to get down to business and write some unit tests!
In the first part of this series, we saw an overview of how to use Moq and XUnit, and heard some basic advice about when and how to write unit tests.
In the second part, we saw how to use Moq to create "fluent mocked" classes that will clean up our unit test code.
In this part of the series, we're going to use ideas from the first part and the fluent mocked classes from the second to write some actual unit tests.
Don't forget to check out the sample project in this series! You can use it to follow along with the posts.
App Architecture
As a reminder, here's the architecture of our app:
Repositories talk to the database. Services call many Repositories to create our business models. Controllers call Services and return their data to Views.
For the purposes of this post, the "business layer" of the application is the Services. Those classes are what we will be testing.
Unit Testing the PlayerService
Let's start by reading the code for the PlayerService
class. Here it is:
public class PlayerService : IPlayerService
{
private readonly IPlayerRepository _playerRepo;
private readonly ITeamRepository _teamRepo;
private readonly ILeagueRepository _leagueRepo;
public PlayerService(IPlayerRepository playerRepo,
ITeamRepository teamRepo,
ILeagueRepository leagueRepo)
{
_playerRepo = playerRepo;
_teamRepo = teamRepo;
_leagueRepo = leagueRepo;
}
public Player GetByID(int id)
{
return _playerRepo.GetByID(id);
}
public List<Player> GetForLeague(int leagueID)
{
var isValidLeague = _leagueRepo.IsValid(leagueID);
if (!isValidLeague)
return new List<Player>();
List<Player> players = new List<Player>();
var teams = _teamRepo.GetForLeague(leagueID);
foreach(var team in teams)
{
players.AddRange(_playerRepo.GetForTeam(team.ID));
}
return players;
}
}
We've got two methods in the PlayerService
class: GetByID()
and GetForLeague()
. The question now is, do we even need to unit test them?
In my opinion, we absolutely need to unit test GetForLeague()
. That method does some internal logic (namely checking if the league ID submitted is valid) that needs to be tested. The real question is: do we need to unit test GetByID()
?
If you're going for full code coverage, you need to test all methods. However, there's a lot of weight to the idea that full code coverage is unnecessary, even a waste of time. I personally agree with that idea, so we will not be writing unit tests for GetByID()
, because all it does is pass through to the lower level repository. In a real-world application, we would write unit tests for the repository as well.
What Do We Test?
We know what method to unit test, but what kinds of unit tests should we write?
There are many ways of determining this.The one we're going to use looks at the "kinds" of inputs and outputs of the method in question, determines the number of possible combinations for each of them, and writes a test for each combination.
First, check the inputs of the method. In this method, we see an int leagueID
. There are only two possible "kinds" of values for this parameter: invalid, and valid. So, we need at least two unit tests.
In the "invalid" parameter case, the method returns immediately, without doing any further work. So, for the "invalid" case, we only need one unit test.
But in the "valid" case, there are more possibilities. For example: what if the League ID is valid, but no Teams are found in that League? What if Teams are found, but no Players are found for any Team? Finally, we need to consider the "true" valid case: League ID is valid, AND Teams are found, AND Players are found for those Teams. This is three unit test cases, plus the one for the "invalid" scenario.
In short, we need to write four unit tests. With all this in mind, let's discuss the unit test scenarios we need to account for.
Unit Test Scenarios
Here's a slightly more formal definition of our unit test scenarios:
- The submitted League ID is invalid. In this case, the code returns an empty
List<Player>
. - No teams are found during the call to
_teamRepo.GetForLeague()
. In that case, that method is called exactly once, and we return an empty list. - No players are found for any of the found teams. In this case, the method
_playerRepo.GetForTeam()
is called at least once but an emptyList<Player>
is returned. - Players are found for the given Teams, and so we return a non-empty
List<Player
.
Let's work through each of these scenarios and write the unit tests. Don't forget that we're going to use the mock Repository classes we created in the previous post.
Unit Test Scenario #1: Invalid League ID
The first scenario is arguably the simplest. Here's the unit test:
[Fact]
public void PlayerService_GetForLeague_InvalidLeague()
{
//Arrange - Setup the mock IsValid method
var mockLeagueRepo = new MockLeagueRepository().MockIsValid(false);
//Create the Service instance
var playerService = new PlayerService(new MockPlayerRepository().Object,
new MockTeamRepository().Object,
mockLeagueRepo.Object);
//Act - Call the method being tested
var allPlayers = playerService.GetForLeague(1);
//Assert
//First, assert that the player list returned is empty.
Assert.Empty(allPlayers);
//Also assert that IsValid was called exactly once.
mockLeagueRepo.VerifyIsValid(Times.Once());
}
Because of the work we did in the previous post in this series, this code is pretty clean.
Unit Test Scenario #2: No Teams Found
The next scenario, where no teams are found for the specified League ID, is slightly more complicated.
This time, we need to mock all the Repositories and a couple of methods in them. We also check to see if PlayerRepository.GetForTeam()
is never called, which it should be in this case because if no teams are found, we should not search for players for them. All this results in the unit test below:
[Fact]
public void PlayerService_GetForLeague_ValidLeagueNoTeams()
{
//Arrange
var mockLeagueRepo = new MockLeagueRepository().MockIsValid(true);
var mockPlayerRepo = new MockPlayerRepository();
var mockTeamRepo = new MockTeamRepository()
.MockGetForLeague(new List<Team>());
var playerService = new PlayerService(mockPlayerRepo.Object,
mockTeamRepo.Object,
mockLeagueRepo.Object);
//Act
var allPlayers = playerService.GetForLeague(1);
//Assert
Assert.Empty(allPlayers);
mockPlayerRepo.VerifyGetForTeam(Times.Never());
mockTeamRepo.VerifyGetForLeague(Times.Once());
mockLeagueRepo.VerifyIsValid(Times.Once());
}
Unit Test Scenario #3: No Players
This scenario is pretty close to the previous one. We now need a mock list of teams to be returned from PlayerRepository.GetForTeam()
, and we need to ensure that it is called at least once.
[Fact]
public void PlayerService_GetForLeague_ValidLeagueNoPlayers()
{
//Arrange
var mockLeagueRepo = new MockLeagueRepository().MockIsValid(true);
var mockPlayerRepo = new MockPlayerRepository()
.MockGetForTeam(new List<Player>());
var mockTeams = new List<Team>()
{
new Team()
{
ID = 1
}
};
var mockTeamRepo = new MockTeamRepository().MockGetForLeague(mockTeams);
var playerService = new PlayerService(mockPlayerRepo.Object,
mockTeamRepo.Object,
mockLeagueRepo.Object);
//Act
var allPlayers = playerService.GetForLeague(1);
//Assert
Assert.Empty(allPlayers);
mockPlayerRepo.VerifyGetForTeam(Times.AtLeastOnce());
mockTeamRepo.VerifyGetForLeague(Times.Once());
mockLeagueRepo.VerifyIsValid(Times.Once());
}
Unit Test Scenario #4: Players Found
This unit test looks similar to the others:
[Fact]
public void PlayerService_GetForLeague_ValidCompleteLeague()
{
//Arrange
var mockLeagueRepo = new MockLeagueRepository().MockIsValid(true);
var mockPlayers = new List<Player>()
{
new Player()
{
ID = 1
}
};
var mockPlayerRepo = new MockPlayerRepository().MockGetForTeam(mockPlayers);
var mockTeams = new List<Team>()
{
new Team()
{
ID = 1
}
};
var mockTeamRepo = new MockTeamRepository().MockGetForLeague(mockTeams);
var playerService = new PlayerService(mockPlayerRepo.Object,
mockTeamRepo.Object,
mockLeagueRepo.Object);
//Act
var allPlayers = playerService.GetForLeague(1);
//Assert
Assert.NotEmpty(allPlayers);
mockPlayerRepo.VerifyGetForTeam(Times.AtLeastOnce());
mockTeamRepo.VerifyGetForLeague(Times.Once());
mockLeagueRepo.VerifyIsValid(Times.Once());
}
With the four scenarios complete, we have completed our unit testing for the PlayerService
! Now let's do some more practice with the TeamService
.
Unit Testing the TeamService
Let's take a look at this service to decide what kinds of tests we need to write.
public class TeamService : ITeamService
{
private readonly ITeamRepository _teamRepo;
private readonly ILeagueRepository _leagueRepo;
public TeamService(ITeamRepository teamRepo,
ILeagueRepository leagueRepo)
{
_teamRepo = teamRepo;
_leagueRepo = leagueRepo;
}
public List<Team> Search(TeamSearch search)
{
//If we are searching for an invalid or unknown League...
var isValidLeague = _leagueRepo.IsValid(search.LeagueID);
if (!isValidLeague)
return new List<Team>(); //Return an empty list.
//Otherwise get all teams in the specified league...
var allTeams = _teamRepo.GetForLeague(search.LeagueID);
//... and filter them by the specified Founding Date and Direction.
if(search.Direction == Enums.SearchDateDirection.OlderThan)
return allTeams.Where(x => x.FoundingDate <= search.FoundingDate)
.ToList();
else return allTeams.Where(x => x.FoundingDate >= search.FoundingDate)
.ToList();
}
}
public class TeamSearch
{
[Required]
[Range(1, 1000)]
public int LeagueID { get; set; }
public DateTime FoundingDate { get; set; }
public SearchDateDirection Direction { get; set; }
public List<Team> Results { get; set; }
}
public enum SearchDateDirection
{
OlderThan,
NewerThan
}
The Search()
method is more complex than the last method we wrote unit tests for. In fact, as far as I can tell, there are five different possible scenarios we need to test.
- When
SearchDateDirection
isNewerThan
AND no teams are found. - When
SearchDateDirection
isNewerThan
AND teams are found. - When
SearchDateDirection
isOlderThan
AND no teams are found. - When
SearchDateDirection
isOlderThan
AND teams are found. - When the League ID is invalid.
Setup: GetMockTeams()
To make writing our unit tests for the TeamService
a bit cleaner, I created a method which returns a list of Teams with various Founding Dates.
private List<Team> GetMockTeams()
{
return new List<Team>()
{
new Team()
{
ID = 1,
FoundingDate = new DateTime(1970, 1, 1)
},
new Team()
{
ID = 2,
FoundingDate = new DateTime(1994, 12, 1)
},
new Team()
{
ID = 3,
FoundingDate = new DateTime(2012, 5, 12)
}
};
}
Unit Test Scenario #5: NewerThan and No Teams Found
Using the GetMockTeams()
method, we can write a unit test for our first scenario.
[Fact]
public void TeamService_Search_NewerThan_Invalid()
{
//Arrange
var mockTeams = GetMockTeams();
var mockTeamRepo = new MockTeamRepository().MockGetForLeague(mockTeams);
var mockLeagueRepo = new MockLeagueRepository().MockIsValid(true);
var teamService = new TeamService(mockTeamRepo.Object,
mockLeagueRepo.Object);
var searchParams = new TeamSearch()
{
LeagueID = 1,
FoundingDate = new DateTime(2017, 1, 1),
Direction = SearchDateDirection.NewerThan
};
//Act
var results = teamService.Search(searchParams);
//Assert
Assert.Empty(results);
mockLeagueRepo.VerifyIsValid(Times.Once());
mockTeamRepo.VerifyGetForLeague(Times.Once());
}
For simplicity's sake, at this point I'm going to assume that you, my dear reader, see the patterns of what we are doing. Hence, I'm just going to leave the code for the next four unit test scenarios, with no explanation; if you would like some reasoning as to why these tests are written this way, please let me know in the comments.
Unit Test Scenario #6: NewerThan and Teams Found
[Fact]
public void TeamService_Search_NewerThan_Valid()
{
//Arrange
var mockTeams = GetMockTeams();
var mockTeamRepo = new MockTeamRepository().MockGetForLeague(mockTeams);
var mockLeagueRepo = new MockLeagueRepository().MockIsValid(true);
var teamService = new TeamService(mockTeamRepo.Object,
mockLeagueRepo.Object);
var searchParams = new TeamSearch()
{
LeagueID = 1,
FoundingDate = new DateTime(1969, 1, 1),
Direction = SearchDateDirection.NewerThan
};
//Act
var results = teamService.Search(searchParams);
//Assert
Assert.NotEmpty(results);
mockLeagueRepo.VerifyIsValid(Times.Once());
mockTeamRepo.VerifyGetForLeague(Times.Once());
}
Unit Test Scenario #7: OlderThan and No Teams Found
[Fact]
public void TeamService_Search_OlderThan_Invalid()
{
//Arrange
var mockTeams = GetMockTeams();
var mockTeamRepo = new MockTeamRepository().MockGetForLeague(mockTeams);
var mockLeagueRepo = new MockLeagueRepository().MockIsValid(true);
var teamService = new TeamService(mockTeamRepo.Object,
mockLeagueRepo.Object);
var searchParams = new TeamSearch()
{
LeagueID = 1,
FoundingDate = new DateTime(1966, 1, 1),
Direction = SearchDateDirection.OlderThan
};
//Act
var results = teamService.Search(searchParams);
//Assert
Assert.Empty(results);
mockLeagueRepo.VerifyIsValid(Times.Once());
mockTeamRepo.VerifyGetForLeague(Times.Once());
}
Unit Test Scenario #8: OlderThan and Teams Found
[Fact]
public void TeamService_Search_OlderThan_Valid()
{
//Arrange
var mockTeams = GetMockTeams();
var mockTeamRepo = new MockTeamRepository().MockGetForLeague(mockTeams);
var mockLeagueRepo = new MockLeagueRepository().MockIsValid(true);
var teamService = new TeamService(mockTeamRepo.Object,
mockLeagueRepo.Object);
var searchParams = new TeamSearch()
{
LeagueID = 1,
FoundingDate = new DateTime(2013, 1, 1),
Direction = SearchDateDirection.OlderThan
};
//Act
var results = teamService.Search(searchParams);
//Assert
Assert.NotEmpty(results);
mockLeagueRepo.VerifyIsValid(Times.Once());
mockTeamRepo.VerifyGetForLeague(Times.Once());
}
Unit Test Scenario #9: Invalid League ID
[Fact]
public void TeamService_Search_InvalidLeague()
{
//Arrange
var mockTeams = GetMockTeams();
var mockTeamRepo = new MockTeamRepository().MockGetForLeague(mockTeams);
var mockLeagueRepo = new MockLeagueRepository().MockIsValid(false);
var teamService = new TeamService(mockTeamRepo.Object,
mockLeagueRepo.Object);
var searchParams = new TeamSearch()
{
LeagueID = 1,
FoundingDate = new DateTime(1997, 1, 1),
Direction = SearchDateDirection.NewerThan
};
//Act
var results = teamService.Search(searchParams);
//Assert
Assert.Empty(results);
mockLeagueRepo.VerifyIsValid(Times.Once());
mockTeamRepo.VerifyGetForLeague(Times.Never());
}
Remarks
There are ways to make the above tests even more concise, e.g. by writing a generation method for the TeamSearch
object with a single parameter for the year, but I've left that out of this post for clarity.
Also note that you will probably write far more test code than there is actual code being tested. This is normal, and acceptable, but the tests might very well be quicker to write.
Summary
When writing unit tests, the main things you need to consider are the inputs, the output, and how the behavior of the method changes based on each. You may need a unit test for each combination of inputs, outputs, and behavior, but that is up to your discretion and whatever your experience tells you.
Using Moq and XUnit, we can make these unit tests both more concise and more easily understood than they would be otherwise.
Did you spot a bug? Saw something I missed? Or do you just want to tell us how you use Moq and/or XUnit? Share in the comments below!
Don't forget to check out the sample project on GitHub!
In the next part of this series, we're going to write some more tests, but on the Controllers layer. This requires a bit of a different setup, though the methodology used to create the tests remains very similar.
Happy Testing!