- Make Assertions, Not Assumptions
- Set Up a Unit Test Project
- An Introduction to Unit Testing
Never written a unit test? Don’t see what all the fuss is about? Or perhaps you’ve written a couple, but you’re not quite sure if they’re actually testing anything? Well, over the course of the next few posts let’s see if we can remedy that. In this example, we’re going to be using the xUnit.net testing framework as well as the FakeItEasy mocking library. But before jumping into the code let me give you an introduction to unit testing from a high-level perspective.
Basics of unit testing
Unit testing, at its core, is a practical way to ensure that a unit of code is behaving in exactly the way it was intended. It’s another way of saying, “When I call this function with these arguments I expect this result.” This provides confidence in the integrity of the code before it’s ever deployed to production. But it does more than just that. Let’s say that several weeks, months, or even years later a dev makes a code change and inadvertently introduces a new bug to the existing code. Assuming that the deployment pipeline requires all unit tests to pass, you can reliably catch future bugs before they’re ever released. Pretty cool, huh?
This is all made possible through unit testing frameworks. There are tons of them out there, but they all provide ways of making assertions. If any of the assertions fail, then the unit test fails.
All of this seems simple enough, but quickly becomes complicated when the function takes on dependencies or relies on state. That’s where mocking libraries come into play. Mocking is a way of creating “fake” objects that can be customized to behave and respond in specific ways.
One of the most common patterns in unit testing is the AAA pattern, or Arrange, Act, and Assert. It’s a great way to partition the code for each test.
- Arrange: Setup everything needed for running the test (dependencies, mocks, data, etc.)
- Act: Invoke the code that’s being tested
- Assert: Specify the criteria for the test to pass
Example situation
Let’s say we’re working on a voting application, and we have a VotingService
, which provides a VotingRepository
via constructor-based dependency injection.
public class VotingService { private readonly IVotingRepository _repo; public VotingService(IVotingRepository repo) { _repo = repo; } ... }
And in this service we have a CastVote
function that allows a citizen to cast a vote on a particular ballot item. A ballot item has a list of options that can be voted for (i.e. candidates), as well as a flag that determines whether or not write-in votes are permitted. Before allowing a vote to be cast, we need to verify the following criteria:
- the citizen exists
- the ballot item exists
- the citizen is eligible to cast a vote on the ballot item
- the citizen hasn’t already cast a vote on the ballot item
- the chosen option is valid, including write-ins
Here is the full CastVote
function with all the checks:
public VoteConfirmation CastVote( int citizenId, int ballotItemId, int ballotItemOption, string writeIn = null) { // Verify that the citizen exists var citizen = _repo.GetCitizen(citizenId); if (citizen == null) throw new RecordNotFoundException("Could not find that citizen"); // Verify that the ballot item exists var ballotItem = _repo.GetBallotItem(ballotItemId); if (ballotItem == null) throw new RecordNotFoundException("Could not find that ballot item"); // Verify that the citizen is eligible to cast a vote on the ballot item var isEligible = _repo.IsCitizenEligibleToVoteOnBallotItem(citizenId, ballotItemId); if (!isEligible) throw new IneligibleVoteException("That citizen is not eligible to vote on that ballot item"); // Verify that the citizen hasn't already cast a vote on the ballot item var vote = _repo.GetVote(citizenId, ballotItemId); if (vote != null) throw new AlreadyVotedException("That citizen has already voted on that ballot item"); // Verify that the chosen option is valid if (ballotItemOption == 0) { if (!ballotItem.IsWriteInOptionAvailable) throw new InvalidVoteException("The write-in option is not available on that ballot item"); if (string.IsNullOrWhiteSpace(writeIn)) throw new InvalidVoteException("The write-in value cannot be null or empty"); } else if (ballotItem.Options.All(option => option.BallotItemOptionId != ballotItemOption)) { throw new InvalidVoteException("That option is not valid"); } // Cast the vote var voteConfirmation = _repo.AddVote(citizenId, ballotItemId, ballotItemOption, writeIn); return voteConfirmation; }
It’s very important that this function behaves the way we expect. Otherwise the system might allow someone to vote more than once. Or vote for a candidate from a different race, etc. Writing a full suite of unit tests that cover every run-time scenario of this function is of utmost importance for the integrity of our voting application.
By this time I hope you have a better understanding of the purpose and usefulness of unit testing, as well as a familiarity with our example situation. In the next part of this series we’ll set up a unit test project with a class file to hold the tests for this function. Before you do though, take another look at our function and try to identify as many different testing scenarios as possible. See you soon!