- Make Assertions, Not Assumptions
- Set Up a Unit Test Project
- An Introduction to Unit Testing
After reading the first part of this series you should have a better understanding of the purpose and usefulness of unit testing, as well as a familiarity with our example situation. So now we’re tasked with writing unit tests for the CastVote
function. After all, we want the electorate to be confident in the integrity of the electoral process, particularly our voting app! Let’s walk through the process of how to set up a unit test project.
Prepare a test class
The first step is to add a new project to the solution that references the main project. We should add a CastVoteTests
class that will house the unit tests for our function. We’ll also need to install the latest versions of the following NuGet packages:
- xunit
- xunit.runner.visualstudio
- FakeItEasy
If we’re going to test the function we’ll need to be able to call it, which means we’ll need an instance of the VotingService
class. But it has a dependency on an IVotingRepository
interface, which we’re not testing, which means we should provide a mocked implementation instead. Since each individual test will need these things, and we’d rather not repeat the setup over and over, we’ll just make them properties of the test class and then instantiate and configure them inside the constructor.
public class CastVoteTests { private readonly IVotingRepository _repo; private readonly VotingService _service; public CastVoteTests() { _repo = A.Fake<IVotingRepository>(options => options.Strict()); _service = new VotingService(_repo); ... } }
Mocking objects
This is a good time to talk about how to mock up objects using FakeItEasy. FakeItEasy provides a static class A
, which provides a method .Fake<T>()
, which creates a fake object of type T
. There’s also an overload for that function that accepts an optionsBuilder
parameter; a lambda to specify certain options for the fake object. The .Strict()
extension method means that any call to the fake that has not been explicitly configured will throw an exception, causing the test to fail. This is a good practice to follow in order to minimize unintentional behavior in your unit tests.
Since we’re setting the Strict option on our fake repository, we’ll have to explicitly configure every call that may happen in the course of running our function. This is done using FakeItEasy’s A.CallTo()
method. You specify the method of the faked object that you want to configure, including the arguments you expect it to be called with, and the desired action that should be taken when called. If the method is called with different arguments than what you configure, not only will the desired action not be triggered, but due to the Strict option the test will fail. There is a way, however, to specify a constraint that considers any value of an argument as valid, which we’ll see in action a little later.
If you want the test to actually execute the code you would follow it up with the .CallsBaseMethod()
extension method. But most of the time you’re going to want to intercept the call and do something specific. If the method has a void
return type then you’d follow it up with the .DoesNothing()
extension method. But for methods that actually return a value you’d follow it up with the .Returns<T>()
extension method, providing an instance of the expected type. So it might look something like this:
A.CallTo(() => _repo.GetCitizen(CitizenId)).Returns(Citizen);
Static Properties
In order to configure all the repository calls, we’ll create some properties to use as the return values.
private static readonly int CitizenId = 12345; private static readonly int BallotItemId = 123; private static readonly int ValidOption = 1; private static readonly int InvalidOption = 7; private static readonly int WriteInOption = 0; private static readonly string WriteInValue = "Chris Kuroda"; private static readonly Citizen Citizen = new Citizen { CitizenId = CitizenId }; private static readonly BallotItemOption TreyAnastasio = new BallotItemOption { BallotItemOptionId = 1, Name = "Trey Anastasio", }; private static readonly BallotItemOption PageMcConnell = new BallotItemOption { BallotItemOptionId = 2, Name = "Page McConnell", }; private static readonly BallotItemOption MikeGordon = new BallotItemOption { BallotItemOptionId = 3, Name = "Mike Gordon", }; private static readonly BallotItemOption JonFishman = new BallotItemOption { BallotItemOptionId = 4, Name = "Jon Fishman", }; private static readonly List<BallotItemOption> Options = new List<BallotItemOption> { TreyAnastasio, PageMcConnell, MikeGordon, JonFishman, }; private static readonly BallotItem BallotItemWithoutWriteIn = new BallotItem { BallotItemId = BallotItemId, Options = Options, IsWriteInOptionAvailable = false, }; private static readonly BallotItem BallotItemWithWriteIn = new BallotItem { BallotItemId = BallotItemId, Options = Options, IsWriteInOptionAvailable = true, }; private static readonly Vote Vote = new Vote { CitizenId = CitizenId, BallotItemId = BallotItemId, BallotItemOption = ValidOption, WriteIn = null, }; private static readonly VoteConfirmation VoteConfirmation = new VoteConfirmation { Vote = Vote, Success = true, };
Constructor
Now that we have these properties we can configure the repository calls inside the constructor. We’ll initially configure them to return the “happy path” values, and then we can override them one at a time in each test scenario as needed. So now our constructor looks like this:
public CastVoteTests() { _repo = A.Fake<IVotingRepository>(options => options.Strict()); _service = new VotingService(_repo); A.CallTo(() => _repo.GetCitizen(CitizenId)).Returns(Citizen); A.CallTo(() => _repo.GetBallotItem(BallotItemId)).Returns(BallotItemWithoutWriteIn); A.CallTo(() => _repo.IsCitizenEligibleToVoteOnBallotItem(CitizenId, BallotItemId)).Returns(true); A.CallTo(() => _repo.GetVote(CitizenId, BallotItemId)).Returns(null); A.CallTo(() => _repo.AddVote(CitizenId, BallotItemId, A<int>._, A<string>._)).Returns(VoteConfirmation); }
You may have noticed a couple of strange arguments in that last line, which is an example of something I mentioned earlier. FakeItEasy provides a way to specify a constraint that considers any value of the argument as valid. For example, if you want to allow for any integer to be passed, you could use A<int>.Ignored
or A<int>._
(the underscore is just a shortcut for the Ignored property).
You might be asking yourself, “Well why wouldn’t we just always use the Ignored property?” The answer is that we want to be as restrictive as possible with our tests. We don’t want to allow any argument to be permissible; only a specific argument. We want the tests to fail if anything happens that we didn’t explicitly expect to happen. However, there are times, just like with any guideline, where it’s okay or even necessary to make exceptions.
Identify testable scenarios
Before we write our first unit test, it’s good to list all scenarios that could produce different results that can and should be tested. Looking at the function I was able to identify 9 testable scenarios that I’ve grouped into the following two categories:
Scenarios That Throw
- Citizen not found
- Ballot item not found
- Citizen not eligible
- Citizen already voted
- Write-in not allowed
- Write-in not provided
- Invalid option
Scenarios That Work
- Valid option
- Write-in provided
In the next part of the series we’ll finally get to write the actual tests. Remember the AAA pattern from part one: Arrange, Act, and Assert? Try to think about what things we’ll need to arrange and assert for each of the testable scenarios above. Until next time!