Learning Unit Testing in ASP.NET Core
This is the follow-up to my last post about IoC and dependency injection. Today, we'll be focusing on unit testing. Since DI and unit testing go hand in hand we had to cover those topics first to lay the foundation for what we'll learn today. If you're not already familiar with IoC/DI then I'd encourage you to go back and check out that post first before you dig into this one.
What is unit testing?
Unit testing simply verifies that individual units of code (mostly functions) work as expected. They allow you to isolate these specific methods/functions and test their behaviors under different conditions and data.
Why bother unit testing?
One big reason is that unit tests make it much easier to change our code while making sure we don't change its behavior (refactoring). This means that properly unit tested code can be heavily refactored/cleaned up so if we happen to break existing functionality we'll know instantly by way of failing tests. This is awesome because it means bugs can't sneak further into our app development. The earlier a bug is identified the less it costs to fix.
Get notified on new posts
Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.
Unit tests create a better design
Unit tests help influence our software design by exposing natural decoupling points in our code. If something is awkward or impossible to test it's likely there are tightly coupled dependencies that are causing us grief. When you're designing classes with unit testing in mind you're forced to break your app down into separate, testable components.
Once you get a feel for this you'll never go back to your old process.
In some cases, you almost don't have to think about the design at all. The tests will guide you to ensure the right behavior lands in the right classes/methods and those classes are properly decoupled from each other using interfaces and dependency injection. This has an added side effect of creating focused components that follow the single responsibility principle.
What about test-driven development?
I typically write code in small increments in parallel with my tests and practice what I call test-driven thinking. Test-driven development (TDD) is a different process where tests are written before a line of code is cut - rad! The tests act as a spec whereby you write a test for a non-existent object/function and it initially fails because no code exists yet. Then you write just enough code to get it to pass.
So while TDD is often a motivation for unit testing, unit testing does not require TDD. I want to make that distinction as this can be a point of confusion for folks who are familiar with these terms but may not fully understand how they work together.
Adding a unit test project
A good approach to unit testing in .net is to add your tests in their own class library project completely separate from your application code. So, to get started I added a new .NET Core Class Library project to the existing solution we used in the first post.
Next up, we need to add a unit testing framework - we'll use xUnit as it currently supports .net core and works with the built-in visual studio 2015 test runner. NUnit is another very popular framework - there are differences between the two but neither will change your life for the better in any dramatic way so don't split hairs about which one to go with.
An additional library is required for mocking up some behavior and data within our unit tests. This can be referred to as mocking, stubbing or faking depending on who you talk to but let's not get caught up with semantics. We'll see in a minute how to use moq to mimic behavior of an interface in our test - this is a powerful construct in unit testing.
Finally, we need to add a reference in our unit test project to our application so we have access to the classes we want to test.
For completeness, your project.json should look like this:
{
"version": "1.0.0-*",
"dependencies": {
"moq.netcore": "4.4.0-beta8",
//http://stackoverflow.com/questions/37288385/moq-netcore-failing-for-net-core-rc2
"System.Diagnostics.TraceSource": "4.0.0-rc2-24027",
"NETStandard.Library": "1.6.0",
"xunit": "2.2.0",
"dotnet-test-xunit": "2.2.0-preview2-build1029",
"ASPNetCoreDIAndUnitTesting": "1.0.0-*"
},
"testRunner": "xunit",
"frameworks": {
"netcoreapp1.0": {
"imports": [ "netcoreapp1.0", "portable-net45+win8", "dnxcore50" ],
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.1.0",
"type": "platform"
}
}
}
}
}
The important parts are "dependencies:{...}"
which brings in the xUnit framework and it's test runner and "testRunner": "xunit"
which instructs the dotnet cli we're using xUnit for our tests.
Writing unit tests
Ok, we covered off some good theory on what unit tests are and why we should use them. We got our project provisioned with the xUnit framework so our app code is testable. We're ready to a write a test...
Our first test will target the TeamStatCalculator class in our app. Specfically, the super-complex GetTotalGoalsForSeason()
method.
public class TeamStatCalculator
{
private readonly ITeamStatRepository _teamStatRepository;
public TeamStatCalculator(ITeamStatRepository teamStatRepository)
{
_teamStatRepository = teamStatRepository;
}
public int GetTotalGoalsForSeason(int seasonId)
{
// get all the team stats for the given season
var teamStatsBySeason = _teamStatRepository
.FindAll(ts => ts.SeasonId == seasonId);
// sum and return the total goals
return teamStatsBySeason.Sum(ts => ts.GoalsFor);
}
}
Breaking it down, we can see this method simply queries _teamStatRepository
to get the list of team stats for a given season and then relies on linq's Sum()
to return the total goals for all the elements...too easy.
One notable thing is that we can see _teamStatRepository
is a dependency that gets injected through our constructor. In reality, this repository could be talking to a database, a file or rest api to get its data but the point is it doesn't matter because we've hidden its implementation details behind the ITeamStatRepository
interface. We only deal with a contract of its behavior not the concrete details of the class. You'll see why this matters in a few seconds. Side note, if your test code writes to a file, opens a database connection or does something over the network, it's more appropriately categorized as an integration test - but that's a different topic.
Let's see the associated test for this method...
[Fact]
public void GetTotalGoalsForSeason_returns_expected_goal_count()
{
// ARRANGE
var mockTeamStatRepo = new Mock();
// setup a mock stat repo to return some fake data in our target method
mockTeamStatRepo
.Setup(mtsr => mtsr.FindAll(It.IsAny<Func<TeamStatSummary, bool>>()))
.Returns(new List<TeamStatSummary>
{
new TeamStatSummary {SeasonId = 1,Team = "team 1",GoalsFor=1},
new TeamStatSummary {SeasonId=1,Team = "team 2",GoalsFor=2},
new TeamStatSummary {SeasonId = 1,Team = "team 3",GoalsFor=3}
});
// create our TeamStatCalculator by injecting our mock repository
var teamStatCalculator = new TeamStatCalculator(mockTeamStatRepo.Object);
// ACT - call our method under test
var result = teamStatCalculator.GetTotalGoalsForSeason(1);
// ASSERT - we got the result we expected - our fake data has 6 goals
we should get this back from the method
Assert.True(result==6);
}
Unit test structure
Our unit test follows the "AAA" approach where we:
- Arrange by doing any necessary setup.
- Act by executing the test - ie. calling the actual method/function and grab its result.
- Assert by verifying the returned result matches expected results.
We can see the bulk of our test code happens in the arrangement part. Here we're creating a mock of our ITeamStatRepository
and defining some custom behavior and data for its FindAll()
method. We then inject our mock repo object into TeamStatCalculator
when we create it.
Once our setup is complete, the act portion simply calls our target method GetTotalGoalsForSeason(1)
to get a result. That's only one little line in our test but if you run the test while debugging you can see the real power and magic in all of this. If you actually step through GetTotalGoalsForSeason()
you will see our mock repository object returning the data we made up in our test within our app code! This is IoC/DI in its full glory! We've completely inverted control of creating our repository and can make it do anything we please in our test - let that sink in!
Finally, in our assertion - we know we have 6 goals in our test data and thus our method should return that if it's working properly.
Running the test in VS test explorer I get a passing green check!
Alternatively, we can run tests from the command line using the new CLI tools. In our project directory simply run the command dotnet test
to invoke the test runner from the command line.
Unit testing controllers
Controllers are a big part of an ASP.NET Core application so providing some coverage for them is important to ensure they behave as expected. We typically want our controllers to be thin and free of code directly performing any sort of business logic or data access (this stuff belongs in its own class(es)). So, we're really just verifying how our controller actions behave based on given inputs and the responses they generate based on the operation(s) they perform.
Let's write a test for the Index()
method on the HomeController.
public IActionResult Index([FromServices] IGameRepository gameRepository)
{
var model = new IndexViewModel
{
Players = _playerRepository.GetAll().ToList(), // constructor injected
Games = gameRepository.GetTodaysGames().ToList() // parameter injected
};
return View(model);
}
The associated test for this action...
[Fact]
public void Index_returns_viewresult_with_list_of_players_and_games()
{
// ARRANGE
var mockPlayerRepo = new Mock();
mockPlayerRepo.Setup(mpr => mpr.GetAll()).Returns(new List
{
new Player {Name = "Sidney Crosby"},
new Player {Name="Patrick Kane"}
});
var mockGameRepo = new Mock();
mockGameRepo.Setup(mpr => mpr.GetTodaysGames()).Returns(new List
{
new Game {
HomeTeam = "Montreal Canadiens",
AwayTeam = "Toronto Maple Leafs",
Date = DateTime.Today},
new Game {
HomeTeam = "Calgary Flames",
AwayTeam = "Vancouver Canucks",
Date = DateTime.Today},
new Game {
HomeTeam = "Los Angeles Kings",
AwayTeam = "Anaheim Ducks",
Date = DateTime.Today},
});
// player repository is injected through constructor
var controller = new HomeController(mockPlayerRepo.Object);
// ACT
// game repository is injected through action parameter
var result = controller.Index(mockGameRepo.Object);
// ASSERT our action result and model
var viewResult = Assert.IsType(result);
var model = Assert.IsAssignableFrom(viewResult.ViewData.Model);
Assert.Equal(2, model.Players.Count);
Assert.Equal(3, model.Games.Count);
}
Nothing radically different in this test than what we just did in our custom TeamStatCalculator one. We're using "aaa", we mock up some dependencies for PlayerRepository
and GameRepository
, inject them into our controller then call its Index()
action method. The biggest difference is in the assertion where we're looking at the ViewResult and model to make sure they're returning what we expect. Other than that, it's safe to say that writing and structuring tests for custom logic AND controllers is practically the same...sweet!
Wrapping up
Well friend, if you're still with me - you rock!!
Right now between the last post and what we've just covered I hope you have a clearer picture of how IoC/DI and unit tests all work together in ASP.NET Core to help produce higher-quality software that is easier and faster to test, fix and change.
The best way to truly understand and feel the benefits of these concepts is to put them into action yourself. So, I challenge you to do so in your next project and experience first hand how they can improve your life as a developer.
Thanks for reading and please drop any questions or feedback in the comments below!
Get notified on new posts
Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.
Get notified on new posts
Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.