UPDATE (8 Jun 2021): The sample code in this post and this series has been updated to use .NET 5.0. It formerly used ASP.NET Core 3.0.
Welcome to a brand new series! This time around, we are tackling how to do unit testing in an ASP.NET 5.0 application using XUnit and Moq. And if that last sentence sounded like gibberish, don't worry; it will all make sense after you read this series.
I previously wrote that testing was hard because nobody could tell me exactly what to test, and why.
"I want to do automated testing! I do! And yet every time I try to get started, I run into these roadblocks. What are tests? What do they test? Why do we test these things? How do we select what things to test? I'm trying to be the self-driven learner and try something new and I'm getting thwarted at every turn. Why does this have to be so difficult?"
This series represents what I've learned more than two years after writing that original post. I've learned a lot about testing, and specifically automated unit testing, and I'm hoping that some of what I learned will be useful to my readers.
Let's get going!
What Are Unit Tests?
Unit Tests are sets of code that test individual modules of a system. Said tests are designed to determine whether the modules being tested are fit for use in production systems.
That is to say: unit tests test small pieces of your system, independently of behavior defined in other pieces.
In a way, unit tests are sanity checks. If a test is written thoroughly, it should help developers catch unintended changes caused when modifying the tested code.
Unit tests are also regression tests. That is, unit tests are designed to detect changes made to a tested system ("regressions"), where such changes cause the system to behave differently than expected. Unit tests are the first indicator that something unexpected has gone wrong in our code.
Why Bother With Writing Tests?
Because you are going to change your code. And when you do, you might cause an outcome you did not anticipate.
The primary thing unit tests give us developers is a kind of insight into when we made a change that caused some kind of unexpected outcome. When a test fails, we can check to see if any of the following are the issue:
- The test itself is wrong, and needs to be changed.
- The test relies on improperly separated dependencies.
- The code itself is now wrong and needs to be fixed.
And those are just a few of the possible causes. But without unit tests, we wouldn't get any notification that something went wrong, we'd just continue on down a bad road.
Should We Test Everything?
In a word: no. Primarily because nothing in software development is ever as simple as "do this every time."
Not everything needs to be unit tested. Pieces of the code that might never change, or are inconsequential to the main functionality of the system, or are indeed too jumbled to properly unit test might be worth not doing (though there are ways to mitigate all of these situations). Whether or not you need to test any given piece of code is a function of complicated it is, how critical it is, and how easy it is to test.
Also, exactly when and why something needs to be tested is rapidly approaching "spaces vs. tabs" levels of holy-war nonsense. There is no one-size-fits-all testing scheme. Every system is different, every need is different, and you have to evaluate the breadth of testing you need against the design and complexity of the system you are building, not to mention the time it takes to do such tests.
OK, So What Should We Test?
Anything which is:
- Important to the functionality of the app as a whole.
- Known to, or likely to, break.
- Likely to change in the future.
In other words: you should test code which is important, fragile, or likely to change.
How Do We Test?
I'm so glad you asked that. Exactly how DO we write unit tests?
There are many unit testing libraries available in the .NET world, including the basic MSTest, NUnit, XUnit, and others. These are the most popular, and they behave in similar fashions. However, in my opinion XUnit offers the best ease of use and reusability, so that framework is what we will use in this series.
XUnit Basics
XUnit is a unit testing framework for .NET that provides an easy way to mark, run, and debug unit tests.
In XUnit, a basic test is a method in a public class, with no parameters or return value, decorated with a Fact attribute, like so:
public class TestCases
{
[Fact]
public void ClassName_MethodName_ExpectedResult() { }
}
At this moment, the above test does precisely nothing. In order to fill out what the test should do, we can follow the method "Arrange, Act, Assert."
NOTE: You can set up XUnit to run tests with inputs and outputs using the [Theory]
attribute; check out Andrew Lock's blog post for an excellent example. We will go over this more in Part 5 of this series.
Arrange, Act, Assert
The phrase "arrange, act, assert" is a good mnemonic for remembering what a test consists of.
- Arrange means to set up the needed test environment and dependencies. This might mean creating a set of known-good test data, or creating a mock of a dependency that we are not currently testing. In short, "arrange" means "create what the test needs to run."
- Act means to actually perform the code being tested, given the setup in the Arrange step.
- Assert means to check results and outputs and confirm that they are what we expect them to be.
As an example, here's an annotated test from the sample project on GitHub that this series will use:
[Fact]
public void PlayerService_GetForLeague_InvalidLeague()
{
//Arrange
var mockLeagueRepo = new MockLeagueRepository().MockIsValid(false);
var playerService = new PlayerService(new MockPlayerRepository().Object,
new MockTeamRepository().Object,
mockLeagueRepo.Object);
//Act
var allPlayers = playerService.GetForLeague(1);
//Assert
Assert.Empty(allPlayers);
mockLeagueRepo.VerifyIsValid(Times.Once());
}
Don't worry about the details of this code snippet; for now, all you need to remember is the pattern "arrange, act, assert." We will go over what exactly this snippet does in Part 3 of this series.
The Rest of the Series
The rest of this series deals with how we might implement a set of tests for a given project. The sample project for this series is hosted on GitHub, as always.
In Part 2, we will demonstrate how to build a "fluent mocked" set of classes that allow us to easily create and setup mock classes for testing (as well as defining what exactly a "mock" even is).
In Part 3, we will demonstrate how to test the "business layer" of the app in question; that is, how to unit test a group of Service classes that have many dependencies.
In Part 4, we will extend on Part 3 to show how to unit test ASP.NET MVC Controller classes, including how to set up the ModelState in tests.
Finally, in Part 5, we will use XUnit's [Theory]
and [InlineData]
attributes to test a few extension methods with many different inputs.
Summary
Unit testing is the most basic kind of testing for a given software system. It can (and, in my opinion, should be) considered a form of regression testing; that is, it is designed to detect changes to the implementation of a given system.
XUnit is one of many unit testing packages, and it provides a simple way to do tests using the [Fact]
attribute.
Don't forget to check out the sample project on GitHub!
In the next part of this series, we're going to use the Moq mocking library to create "fluent mocked" classes, which will make writing our tests in the later parts of the series cleaner.
Happy Testing!