An Introduction to Unit Testing


This entry is part [part not set] of 3 in the series 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!

Series Navigation

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.