How to Write Great Unit Tests

I’ve written before about automated tests, why we use them at Rare, and how to use them effectively. In the time between then and now I’ve learned a lot about our test systems and tests in general, so I thought it was time for a more in depth look at testing, especially the much maligned unit test.

Unit tests are often considered unnecessary guff that’s just more code to write and maintain- but often they are not given a fair chance. Like any other new technique, we start by writing bad tests on code written without testing in mind, have a miserable time, and write off testing entirely as a bad job. But, writing great tests on great code can be easy peasy. I’m going to cover some specific techniques on how you can write great tests on great code, and get a lot more out of it in the process.

Note that I’m referring to Unreal Engine a few times and using Unreal Engine naming standards in my examples, but these techniques should definitely apply to all C++, and may be useful in other languages too.

Why Use Automated Testing?

I don’t think there’s a briefer summary than this graph, which compares the bug count on one of our titles (Title A) at Rare where we didn’t use continuous delivery or automated testing, and the bug count on one where we super did (Title B):

That steep vertical drop on the Title A line is where nobody wants to be: the bug fixing stage. It’s hard to plan for how long it takes to find a bug, let alone thousands of them, and that’s where you’re most likely to have crunch.

Automated testing allows you to test a lot of code paths incredibly quickly. That means you don’t have to wait until your code is checked in and every possible interaction has been checked manually before you find out if there are any bugs. Of course, it takes time to write tests for any new functionality you add, but you’re exchanging the time spent fixing a bug later for time spent preventing it now. Preventing is a lot easier than fixing. 

It can be especially hard to fix a bug when it’s interacting with other bugs in weird ways or it’s in code you’re unfamiliar with. If you keep the codebase’s bug count low, you know that any new issues you pick up in your automated tests before you submit your changes, were caused by your changes. Even if they are in some completely unrelated part of your codebase.

What About Unit Tests?

It’s likely that most of the automated tests you write will be unit tests, testing a single component of the larger system. Integration tests are critical too, but the number of decisions made for each input in the machinery of something as complex as our codebases can explode out into millions of outputs, which is an awful lot of paths to test. Breaking these paths down to test each individual decision is made properly for each of its possible inputs helps you cover a lot more area, as well as helping you pinpoint exactly where errors are occurring.

And there’s some handy side effects for writing unit tests too. Writing something that uses every path of your code alongside it is a great way to keep yourself on track. Plus having all expected behaviours of a component of your system documented by the tests is good for anyone else that might need to work with that component.

What Makes a Great Test?

While I’m sure you’re convinced at this point that thorough unit tests are a great idea, you may not get all of these benefits if you’re not testing effectively.

The first thing is to focus on what you want your test to do, which is to tell you that something works. The way we can do that is by testing that in a particular situation, if we give the code some input or trigger, it will respond with a particular output. In a game of Scrabble, if the situation is that there’s a triple world score slot on the board, and the trigger is placing a word over that slot, then we expect the output to be that we get points equal to the sum of all the letter scores multiplied by three.

We name our tests according to this structure, which we call Given-When-Then. This helps us always remember to make sure that our test is telling us something useful.

So a great test should be:

Illuminating

The test should tell us something that we want to know. A bad (and very real) example of a test would be something that constructs an object and then checks that the object exists. This isn’t very interesting- we already know that construction works in C++, or whatever language we’re using. Tests are better used when we’re adding in new logic and we need to ensure both that it works now and that it will continue to work.

Accurate

An accurate test is one that passes if the hypothesis (our given-when-then behaviour) is true, and fails if the hypothesis is false. If there’s a bug in your test that makes the test fail even if the production code works fine, your test isn’t telling you anything useful.

Repeatable

A test should give the same results 100% of the time when it’s run on the same production code. Sometimes dependencies on time or randomisation can mean that occasionally a test running on working code might fail. This isn’t helpful, as it can be hard to tell whether you have a real issue that crops up infrequently or if your test just isn’t covering your behaviour properly.

On top of these test-specific metrics of success, we’ve got a few more that apply to all code and should apply to our tests too. Is it fast enough for our purposes? Is it readable? Is it maintainable?

A Simple Test

⚠ I’m going to be getting into some code examples here, and please note that none of these examples or the game mechanics they describe have anything to do with any Rare projects. They are just made up examples written specifically for the purposes of demonstrating these techniques.

Let’s start by looking at a simple example of a piece of behaviour we might want to test.

Here we have an extract of code for an imaginary Animal Crossing inspired gardening game, with a grid structured garden with slots that plants can be planted in. For now, we’re keeping it simple and just storing our garden as a grid structure of TOptional<FPlant>s. (TOptional is an Unreal Engine struct much like std::optional that can store an instance of the class it’s templated on or be unset.) It will be unset if there’s nothing planted there, and set with the instance of the FPlant if there is. Our static function counts up the amount of occupied slots, or TOptionals with a value, in the grid.

TGrid< TOptional< FPlant > > GardenGrid;

static int GetNumOccupiedSlots( const TGrid< TOptional< FPlant > > InGardenGrid )
{
    int FreeSpaceCount = 0;
    InGardenGrid.ForEach( [ &FreeSpaceCount ]( const TOptional< FPlant >& InPlantSpace )
        {
            if ( !InPlantSpace.IsSet() )
            {
                FreeSpaceCount++;
            }
        }); 
    return FreeSpaceCount;
}

We’re expecting here that if our grid has a certain number of plants in different slots, then this function will return that number of plants. Here’s the test we can write to make sure that that hypothesis is true.

GARDEN_GRID_TEST(GIVEN_garden_grid_with_plants_WHEN_get_num_occupied_slots_called_THEN_returns_number_of_occupied_slots )
{
    TGrid< TOptional< FPlant > > GardenGrid( 3, 3 );
    GardenGrid.At( 0,0).Emplace( FPlant() );
    GardenGrid.At( 1,1).Emplace( FPlant() );
    GardenGrid.At( 2,2).Emplace( FPlant() );
    TestEqual( GetNumOccupiedSlots( GardenGrid ), 3 );
}

This isn’t the ideal test, because we’ve hard coded that there’s three plants in the grid which isn’t the most maintainable thing. We might want to fiddle with that number later or duplicate the test to test something else. We could do this as an alternative, but it’s quite a lot to add to a very simple test, especially with the faff of finding slots to plant the plants in. It depends on the cost/benefit of adding the extra logic in the real-world scenario.

GARDEN_GRID_TEST(GIVEN_garden_grid_with_plants_WHEN_get_num_occupied_slots_called_THEN_returns_number_of_occupied_slots )
{
    const int ExpectedOccupiedSlotCount = 3;
    TGrid< TOptional< FPlant > > GardenGrid( ExpectedOccupiedSlotCount, ExpectedOccupiedSlotCount );

    for( int i = 0; i < ExpectedOccupiedSlotCount; i++; )
    {
        GardenGrid.At( i,i).Emplace( FPlant() );
    }

    TestEqual( GetNumOccupiedSlots( GardenGrid ), ExpectedOccupiedSlotCount );
}

⚠ Solve the Right Problem

The following techniques for improving your tests will help overcome some common testing pain points. You often hit these pain points when you’re working really hard to write your tests or you’re doing things that you think are bad practice but there’s no other way to make the code testable. I find that if you’re finding yourself writing complex test fixtures or hacky workarounds, you’re often solving the wrong problem. Your real problem is not writing the difficult test- your problem is that the test is difficult to write. 

Maybe you’re testing the wrong thing or your code doesn’t adhere to good practices like the single responsibility principle. So a painful testing experience or a huge obstacle is a big warning sign and a moment to think- what’s the real problem here? 

I’m not telling you to wreck your production code for the benefit of the test- but good practices often make good testability. So if you are having to wreck your production code for testability, something else is probably wrong.

Testing Behaviour, Not Implementation

One of the common pitfalls of testing is that you can end up enforcing implementation details instead of the behaviour, which isn’t as illuminating. How it manages to produce that behaviour is important, but that’s not what we write unit tests for. Unit tests are not great at value judgement.

Let’s say our imaginary gardening game has a mechanic where if a player is standing on a slot with a plant on it, and they use their watering can, then when an in-game day passes, it will grow another growth stage. Purely behaviour based testing would be at the integration level, so a good integration test for our gardening game might be the flow of making a plant grow from a player walking up to it, watering it, and a day passing so that it grows. But at the unit level we want to break this down into its individual elements of behaviour to test, or what I call “microbehaviours”.

 So to get a more fine grained view of where any issues are, we could split this into classes for the plant, player, and some sort of manager for in-game time. Let’s say each of the smaller circles in the gif above represents a function, so there’s one for the player running or using a watering can, and one for the plant being marked as watered. One of the common mistakes is to consider each of these functions to be a single microbehaviour.

In our “simple test” example, the function we were testing was, in fact, a great example of a single behaviour. It’s a static function, so the inputs and outputs are both examinable and there’s no private state to worry about.

Testing With Private State

So let’s take an example where we do have some private state. Our FPlant class tracks whether or not it’s been watered or harvested, and what its growth stage is. There’s also a function to water the plant, and a function to get the growth stage, which is public because some other classes might want to know about it. Then on each day, we’re checking if it’s been watered and if it’s not already fully grown before increasing it’s growth stage and resetting the IsWatered flag.

class FPlant
{
private:
    bool IsHarvested = false;
    bool IsWatered = false;int GrowthStage = 0;
    int MaxGrowthStage = 3;
}

void FPlant::Water() {IsWatered = true;}
int FPlant:: GetGrowthStage(){return GrowthStage;} 

void FPlant::DailyTick()
{
    if( GrowthStage < MaxGrowthStage&& IsWatered )
    {
        GrowthStage++;
        IsWatered = false;
    }
}

We’re going to try testing that the Water function works. The input is that we’ve called the Water function, and the output, we might think, is that the IsWatered flag is set to true. But the IsWatered flag is private, so how are we supposed to test that?

One approach that I would advise against might be that you create what I’d call a “testable” plant that provides a public getter for this IsWatered flag. We’ve set up a test where an unwatered plant is watered and then we can check that the flag is set to true. But this isn’t really telling us what we want to know. WHY do we care that this flag is set to this true? We might say that we want to be able to pinpoint errors in specific functions, which is reasonable, this is a unit test after all.

PLANT_TEST(GIVEN_unwatered_plant_WHEN_water_called_THEN_marked_as_watered) 
{
    class FTestablePlant : public FPlant 
    {
      public:
        bool GetIsWatered() { return IsWatered; }
    };

    FTestablePlant Plant;
    Plant.Water();
    TestTrue(Plant.GetIsWatered());
}

⚠ Why Not Just Test Functions?

Knowing we are trying to pinpoint where issues are, why not just take functions as the “unit” in our unit test? First of all, we’re going to have to write test code like the above testable to access private variables that were set by the function. This can be dicey as it’s effectively untested code, plus it’s more boilerplate than we want to write.

Then, how maintainable is testing functions? If we change the format the data’s going to be stored in, the details of how the function is implemented, even just the name of the private variable, the tests have to be updated with it!

Lastly, let’s think about why we’re writing the test and what we’re trying to find out. The question that’s really interesting here is not if the IsWatered flag is set, something that’s invisible to the player or any other class in the system, but that the class fulfils the contract that it presents to other classes through its public functions. What is the behaviour we see from the plant class as a result of it being watered?

Finding the Input to Output Flows

Instead of considering our microbehaviours to be functions, we can try instead looking at the public input to output flows of the class, and treat each of those routes as a microbehaviour. 

Looking at the class, we can see that a result of the plant being watered is that when a day passes it will increase its growth stage, something that is visible to code outside the class via the GetGrowthStage function.

So let’s test that behaviour, for which we need only use those public functions.

PLANT_TEST(GIVEN_unwatered_plant_WHEN_watered_and_new_day_triggered_THEN_growth_stage_increased) 
{
    FPlant Plant;
    const int StartingGrowthStage = Plant.GetGrowthStage();
    Plant.Water();
    Plant.DailyTick();
    TestEqual(Plant.GetGrowthStage(), StartingGrowthStage + 1);
}

You can see that this test is a lot cleaner, and it’s enforcing what we really want to know; that if we’re going to give us those inputs it’ll give us the right output.

With that behaviour focus, notice that I’ve taken note of the starting growth stage at the start of the test, because whatever growth stage it was before isn’t part of the behaviour. The behaviour is that it’s increased by one.

Test Doubles

So when we test the public input to output flow, it may be that at some point another class is called. We might consider what happens in that class that we’re calling to be part of the behaviour, because if the class we’re calling is broken, the behaviour is also not going to work. But we’re doing unit tests here, and that means testing the behaviour of individual components of the system, in this case our classes. We’re trying to pinpoint where breakages are, and that means isolating those components.

The PickFlower function on our FPlayer class is what’s called when a player presses the button to pick a flower from a plant at the current location. The function finds out what the location is and passes it through to the garden grid, which we’ve turned into a full class instead of just the grid storage we had in the example before. The FGardenGrid is going to handle making sure there is a plant at that location that’s fully grown and unharvested. We’re just going to grab the flower if there is one and add it to our inventory. 

void FPlayer::PickFlower(FGardenGrid InGarden) 
{
    FCoordinate PlayerPosition = GetPosition();
    FFlower *PickedFlower = InGarden.TryPickFlower(Position);
    if (PickedFlower) {
        Inventory.Add(PickedFlower);
    }
}

Of course, from what we’ve just learned, we’re not going to test this by accessing the inventory directly, we’re going to test the observed behaviours that result from it being added to our inventory. For example, if other classes need to know how many items there are in the inventory for the UI, and there might be a function for that. An expected behavioural output would be that after picking a flower, that function returns one more than it did before.

int FPlayer::GetInventorySize() { return Inventory.Num() };

Calling Through to Complex Classes

For testing the flow that a player picking a flower results in their inventory size increasing by one, if we don’t have test doubles, then we’ve got to set up a proper plant that’s in the right place, and is at the maximum growth stage so that we can pick its flower.

But say our FPlant class needs lots of data in it that gets validated when it’s planted in the garden grid. Our test is going to be filled out by lots of lines of code just giving this plant class all the dummy data it needs to be planted, as well as using the public functions to grow it to the right growth stage.

PLAYER_TEST(GIVEN_pickable_plant_at_location_WHEN_player_at_position_picks_flower_THEN_inventory_size_increased) 
{
    FPlayer Player;
    int StartingInventorySize = Player.GetInventorySize();
    FGardenGrid GardenGrid(3, 3);

    FPlant Sunflower(TEXT("Sunflower"), /* MaxGrowthStage =*/1, ....);
    Sunflower.GrowthStageData[0].Sprite = FSprite(...);
    Sunflower.GrowthStageData[0].Hardiness = 5;

    Sunflower.Water();
    Sunflower.DailyTick() GardenGrid.Plant(Sunflower, FCoordinate(1, 1));
    Player.SetPosition(1, 1);
    Player.TryPickFlower(GardenGrid);
    TestTrue(Player.GetInventorySize() == StartingInventorySize + 1);
}

But if you look at the code in the functions we’re testing, that data is never actually accessed nor is the FPlant class ever referenced. We’re doing a bunch of extra work that is also not very maintainable, as any changes in the FPlant class’s required data is going to mean all these tests need updating.

Calling Through to Broken Classes

What happens if we do a test on a class that’s calling through to something broken? Let’s take an example of what that FGardenGrid::TryPickFlower function might look like.

FFlower *FGardenGrid::TryPickFlower(FCoordinate InCoordinate) 
{
    FPlant *PlantAtLocation = GetPlantAtLocation(InCoordinate);
    
    if (!PlantAtLocation) 
    {
        if (PlantAtLocation.MaxGrowthStage == PlantAtLocation.GetCurrentGrowthStage() && !PlantAtLocation.Harvested) 
        {
            PlantAtLocation.Harvested = true;
            return PlantAtLocation.InstantiateFlower();
        }
    }
    return nullptr;
}

We’ve got an erroneous exclamation mark there on line 5 which means that it’s only going to try and pick a flower if there’s no plant there! It immediately dereferences the PlantAtLocation, so any test which runs that code is going to crash and fail, which is good, right? We want to know that the error is there. 

But in the tests for the player class, which is not broken, we call through to this function, and so if we were to run our test suite we might get an output like this:

Garden Grid Tests- FAILED
>GIVEN_fully_grown_unharvested_plant_at_location_WHEN_try_pick_flower_called_THEN_returns_flower – FAILED
Player Tests- FAILED
>GIVEN_garden_grid_plant_at_location_WHEN_player_at_position_picks_flower_THEN_inventory_size_increased– FAILED

Our player tests are failing, which might lead us to spend time debugging the player class, which has no errors in it. Ideally we want to only see that the garden grid class is broken, and we can do this by isolating the classes from each other.

Mocking Out the Class

If we only access the FGardenGrid through an interface, in a test environment we can swap out the FGardenGrid behind that interface with a test double, like an FMockGardenGrid.

class FMockGardenGrid : public IGardenGrid 
{
    FFlower *FGardenGrid::TryPickFlower(FCoordinate InCoordinate) override 
    {
        return FlowerToPick;
    }
    FFlower *FlowerToPick;
}

Our FMockGardenGrid is just going to return what we tell it to when the TryPickFlower function is called. That way we can set up both scenarios where it succeeds to pick the flowers and when it fails. Bear in mind that errors in a mock can cause even more problems in your tests, so you want to keep them as simple as possible with as little logic as possible. 

If we’re only accessing it through the interface, then our test is made a lot neater by replacing the FGardenGrid with this mock, as well as it being more focused on the behaviour of the FPlayer class.

PLAYER_TEST(GIVEN_garden_grid_plant_at_location_WHEN_player_at_position_picks_flower_THEN_returns_flower) 
{
    FPlayer Player;
    int StartingInventorySize = Player.GetInventorySize();

    FMockGardenGrid GardenGrid;
    GardenGrid.SuccessfullyPickFlower = true;

    Player.TryPickFlower(GardenGrid);
    TestTrue(Player.GetInventorySize() == StartingInventorySize + 1);
}

Here we’re treating the call through to the dependency as an endpoint of our input-output flow. What’s called through to the function is an output, and what it returns is an input.

Capturing inputs

You’ll notice that this test no longer is going to break if we call through with the wrong coordinate, but that’s a different input to output route we can check with another test. We’ll extend our mock to make it store whatever was input into the class, and we can write a test that makes sure the inputs it receives are as expected.

class FMockGardenGrid : public IGardenGrid 
{
    FFlower *FGardenGrid::TryPickFlower(FCoordinate InCoordinate) override 
    {
        FlowerLastPickedAtCoordinate = InCoordinate;
        return FlowerToPick;
    }
    FFlower *FlowerToPick;
    FCoordinate FlowerLastPickedAtCoordinate;
}
PLAYER_TEST(WHEN_player_at_position_picks_flower_THEN_players_position_is_passed_through_to_garden_grid) 
{
    FMockGardenGrid GardenGrid;
    const FCoordinate PlayerPosition(1, 2);
    Player.SetPosition(PlayerPosition);
    Player.TryPickFlower(GardenGrid);

    TestTrue(GardenGrid.FlowerLastPickedAtLocation == PlayerPosition);
}

⚠ Mocks vs Testables

You might have noticed that earlier I advised against using a testable, but now I’m suggesting that you should use mocks, so let’s cover the differences between them to be clear.

A testable is a double of the class you’re testing, intended to expose its internals. You shouldn’t have to do this if you’re testing effectively, you want to test the closest thing to the real class as it would behave in production as you can.

A mock is a double of a class that the class you’re testing calls through to, designed to replace its internals. This isolates the behaviour of the class you’re testing from the behaviour of its dependencies, which can be useful in unit testing.

Usually a testable inherits from the class it’s a testable of, whereas a mock will just implement the same interface as the class it’s a mock of.

Dependency Injection

If we’ve got all these dependencies and we want to replace them with mocks for testing, how do we do that without making our code hideous? Dependency injection is the simplest way in my opinion, and while it sounds like a complicated technique, it’s just a fancy way of saying “passing values to functions.”

I’ve seen a lot of code like this that can make testing really complicated, as you can see we’re getting some of the dependencies for the GrowFlower function’s behaviour from globals.

If you want to replace the plant database here with a mock, you’re going to have to make it so that the GetWorld call returns a valid world with a valid game mode and that has the mock registered by the right interface. It’s a huge pain.

void FPlant::GrowFlower() 
{
    IPlantDatabase *PlantDatabase = GetWorld()->GetGameMode()->GetService<IPlantDatabase>();
    check(PlantDatabase);
    FSprite FlowerSprite = PlantDatabase.GetFlowerSprite(PlantType);
    GEngine->GetWindow()->DrawSprite(Location, FlowerSprite);
}

Injecting these dependencies could be as simple as just passing them in to this function when we call it.

void FPlant::GrowFlower(IPlantDatabase &InPlantDatabase,
                        IGraphicsWindow &InWindow) 
{
    FSprite FlowerSprite = InPlantDatabase.GetFlowerSprite(PlantType);
    InGraphicsWindow.DrawSprite(Location, FlowerSprite);
}

Either this call could be at the end of a chain of calls from the place that these dependencies are stored, which is a good habit to get into. However, if this isn’t possible injecting dependencies on construction can be a useful option.

FPlant(IPlantDatabase &InPlantDatabase, IGraphicsWindow &InGraphicsWindow)
    : PlantDatabase(InPlantDatabase), GraphicsWindow(InGraphicsWindow) {}

void FPlant::GrowFlower() 
{
    FSprite FlowerSprite = PlantDatabase.GetFlowerSprite(PlantType);
    GraphicsWindow.DrawSprite(Location, FlowerSprite);
}

If we haven’t got neat access to the dependencies where our class is instantiated either, sometimes we can use a pattern like this, where instead of constructing it directly we have a static factory type function that grabs all the required dependencies from their globals and passes them in. It’s boilerplate, which counts against it as an option, but that depends on the benefits against that cost. 

Note that there should be minimal branching in these types of functions because they’re not supposed to be tested, they’re just implementation glue. An integration test is a more useful way to make sure this works.

static FPlant FPlant::Construct() 
{
    IPlantDatabase *PlantDatabase = GetWorld()->GetGameMode()->GetService<IPlantDatabase>();
    IGraphicsWindow *GraphicsWindow = GEngine->GetWindow();
    return FPlant(*PlantDatabase, *GraphicsWindow);
}

Isolating Non-Deterministic Elements

So a big pain point for testing is non deterministic elements, and it can make people think that any code that uses them is untestable. The secret though is that unless you’re writing random number generators or time libraries, which would be a whole separate article, elements like randomisation or time are no different than any other external input. And external inputs, like we’ve demonstrated already, we generally want to isolate away from our code that we’re testing so that we don’t have our tests depend on them too.

Time as an Input

In this function, we need to find out what the current time is so that we can trigger a daily tick at the right interval. To get it, we’re making a call to the global engine, which is pretty untestable, much like some of the dependencies in the last example. You’re going to struggle with getting it to return the same results every time.

One option might be that we could inject a function for getting the current time in the constructor, and that allows us to replace that to return whatever time we want in a test. But as with all test mocking I would advocate for keeping the logic in there very simple as it is going to be untested.

void FGardenGrid(TFunction<float()> InGetTimeFunction)
    : GetTimeFunction(InGetTimeFunction) {}

void FGardenGrid::Tick() 
{
    float CurrentTimeInSeconds = GetTimeFunction();
    if ((CurrentTimeInSeconds - LastDailyTickTime) > 100) 
    {
        LastDailyTickTime = CurrentTimeInSeconds;
        TriggerDailyTickOnAllPlants();
    }
}

But as we’re working in a tick, the option to use the DeltaTime between ticks might be available to you, which is much better- injecting the value rather than a provider can make things a lot cleaner.

void FGardenGrid::Tick(float DeltaTime) 
{
    TimeSinceLastDailyTick += DeltaTime;
    if (TimeSinceLastDailyTick > 100) 
    {
        TimeSinceLastDailyTick = 0;
        TriggerDailyTickOnAllPlants();
    }
}

Randomised Input

As for randomisation, in this example a call is made to FMath::Rand (An Unreal function that returns random integers between a given range), which is again a global call that’s pretty untestable. We can’t control this input to our test so we can’t know what the output should be. But the technique here is the fundamentally the same. This Rand function is just another input that’s not this function’s problem.

void FGardenGrid::GrowWeeds() 
{
    for (int i = 0; i < DailyWeedCount; i++) 
    {
        int x = FMath::Rand(0, Size.x);
        int y = FMath::Rand(0, Size.y);
        PlantWeed(x, y);
    }
}

But using an injectable function that returns, for example, the number 5 every time might cause us problems here. If there’s an error where the x and y inputs get mixed up then we won’t catch it, because either way the weed will be planted at (5, 5). Instead we could inject a random number generator interface as a dependency that in production calls the Rand function and in a test returns a queue of numbers.

void FGardenGrid::GrowWeeds(IRNG &InRNG) 
{
    for (int i = 0; i < DailyWeedCount; i++) 
    {
        int x = InRNG->GetRandomRange(0, Size.x);
        int y = InRNG->GetRandomRange(0, Size.y);
        PlantWeed(x, y);
    }
}
class MockRNG : public IRNG 
{
  public:
    int GetRandomRange(int Min, int Max) override 
    {
        return RandomNumberQueue.Dequeue();
    }
    TQueue<int> RandomNumberQueue;
}

Better APIs

Your unit tests are functionally an enforcement of the contract that each component in your system makes with other components, so they’re a good opportunity to take stock of what that contract is. If it’s fiddly to use or exposing too much information to other classes, this is often noticeable in writing the tests.

A Single Point of Access

Like in our FGardenGrid::TryPickFlower function from earlier, you might have noticed that we’re doing quite a lot of our plant logic inside this garden grid class. We’re directly accessing the plant’s properties, and making the garden establish that the flower is pickable before asking the plant to instantiate the function.

FFlower *FGardenGrid::TryPickFlower(FCoordinate InCoordinate) 
{
    FPlant *PlantAtLocation = GetPlantAtLocation(InCoordinate);
    if (PlantAtLocation) 
    {
        if (PlantAtLocation.MaxGrowthStage ==
                PlantAtLocation.GetCurrentGrowthStage() &&
            !PlantAtLocation.Harvested) 
        {
            PlantAtLocation.Harvested = true;
            return PlantAtLocation.InstantiateFlower();
        }
    }
    return nullptr;
}

And accordingly, here in our test for the function we have to do work to make sure the plant is in the right location and make sure it’s grown to the right growth stage.

GARDEN_GRID_TEST(GIVEN_avaiable_fully_grown_unharvested_flower_at_location_WHEN_flower_picked_at_location_THEN_returns_flower) 
{
    FGardenGrid GardenGrid;
    FCoordinate Location(1, 1);

    GardenGrid.Plant(FPlant(/*MaxGrowthStage =*/1), Location);
    GardenGrid.Water(Location);
    GardenGrid.DailyTick();

    TestTrue(GardenGrid.TryPickFlower(Location));
}

We can move a lot of this plant logic into the FPlant class, instead of accessing the properties directly. It’s now up to the plant to decide whether or not it’s ready to be picked, and all the garden has to care about is that it’s possible to succeed or fail to pick a flower.

bool FPlant::IsFullyGrown() override 
{
    return MaxGrowthStage == CurrentGrowthStage;
};
FFlower *FPlant::TryPickFlower() 
{
    if (!IsHarvested && IsFullyGrown()) 
    {
        IsHarvested = true;
        return InstantiateFlower();
    }
    return nullptr;
}

This makes our FGardenGrid::TryPickFlower function look a lot simpler.

FFlower *FGardenGrid::TryPickFlower(FCoordinate InCoordinate) 
{
    IPlant *PlantAtLocation = GetPlantAtLocation(InCoordinate);
    if (PlantAtLocation) 
    {
        return PlantAtLocation.TryPickFlower();
    }
}

This means that in our test we don’t have to care what it means for a plant to be pickable, we can just have a FMockPlant tell us that it’s ready to be picked through that single point of access. Of course that plant logic still has to be tested, but we can do that in the FPlant tests. Our original test gets turned into two simpler, more descriptive tests.

GARDEN_GRID_TEST(GIVEN_plant_is_ready_to_be_picked_WHEN_flower_picked_at_location_THEN_returns_flower) 
{
    FGardenGrid GardenGrid;
    FCoordinate Location(1, 1);
    FPlant MockPlant = FMockPlant();

    MockPlant.SuccessfullyPickFlower = true;
    GardenGrid.Plant(MockPlant, Location);

    TestTrue(GardenGrid.TryPickFlower(Location));
}

PLANT_TEST(GIVEN_unharvested_fully_grown_plant_WHEN_flower_picked_THEN_returns_flower) 
{
    FPlant(/*MaxGrowthStage =*/1);

    Plant.Water();
    Plant.DailyTick();

    TestTrue(Plant.TryPickFlower());
}

Reducing Dependencies

We’ve talked a lot about test doubles in this article, and while they’re very helpful for isolating code they don’t come without a cost. They’re time consuming to produce and it’s also tempting to put loads of logic in them that will ultimately be untested.

The fun part is that you need test doubles to isolate your code from dependencies, and the simplest way to circumvent that is by not having the dependency in the first place. One of the things you can do is pass in the value you need rather than what provides it. When we come to test this function we’ll have to write a mock for this IPlantDatabase and get it to return a suitable example growth stage.

FPlant(FPlantType InPlantType, IPlantDatabase &InPlantDatabase)
    : PlantType(InPlantType) 
{
    MaxGrowthStage = InPlantDatabase.GetMaxGrowthStage(PlantType);
}

But maybe, providing it’s not just going to get confusing in the calling code, we can just pass in that max growth stage directly

FPlant(FPlantType InPlantType, int InMaxGrowthStage)
    : PlantType(InPlantType), MaxGrowthStage(InMaxGrowthStage) {}

In this particular case, that could just end up meaning that you then have to write a more complicated mock for the tests for the calling code. But keep an eye out for easy wins like this, where you may not necessarily need the dependency.

Summary

Most of these techniques are less about writing great unit tests and more about how thinking about the dependencies and public interface of your production code can make writing your unit tests easier. Improving a unit test can just be making sure that what you’re testing is meaningful and that you’re testing only the meaningful thing.

Five takeaways:

  • A good way to identify what to test is to identify the public input to output flows of your class.
  • Replacing dependencies with doubles in a test can help you identify where any issues are and stop you having to set up things that don’t impact the behaviour you’re testing.
  • Being careful and explicit about how you acquire dependencies can make it easier to replace them in a test environment.
  • Non-deterministic elements like time or randomisation can be treated like any other external dependency.
  • Well thought out and modular interfaces are much easier to test.

And lastly remember the golden rule- is your problem writing the difficult test or is it that the test is difficult to write in the first place? If you find yourself working hard to write a test, ask yourself if your production code is making it harder for you.

A big thank you to Stu Holland, Pete Campbell, Christina McGrath and Louise O’Connor for proofreading and also being very cool 😎

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s