Avoiding Repeated Test Behaviour Across Multiple Tests with Test Fixtures

For anyone who has had the pleasure of writing/maintaining tests for code that require the use of external dependencies, you will (hopefully!) have incorporated the use of a mock of a function/object. In gMock, you need to define the behaviour of a mock via the EXPECT_CALL & ON_CALL macros in each test before it is used. When multiple tests rely on the same use of a mock, the common method I see far too frequently is to either, copy-paste the mock’s behaviour across each test case that relies it, or just dump all your assertions into a single monolithic test case to avoid copy-pasting. Both are terrible habits to fall into that lead to tests that are brittle and harder to read.

In this guide, I want to demonstrate ways in which you can recognise repeated test setups, and where utilising a Test Fixture class to resolve this can simplify production & maintenance of tests.

Bad Examples of Tests

Consider the following procedural function written in C. Let’s say that we’ve been tasked to write a series of tests on some legacy code that when called retrieves some data from an external module (retrieveExternalData), before passing it on to a function that exists in another external module (sendMessage). For now, ignore the fact that this code is absolute trash and should have been refactored; it’s hard to think of a good example!

Source Code

In between these 2 external function calls, some manipulation of data occurs that varies depending on the value of data_type passed into this function. For now our tests will focus on testing that the data passed into ‘sendMessage’ was performed to specification according to the value of data_type.

But because of the fact that there are external dependencies that we need to mock in order to take full control over how the tests will operate, for many of the tests the mocked behaviour may be identical. As a result, different tests may exhibit the same patterns. See for example the following:

Repeating Test Cases

We can clearly see that these tests are almost identical in every way except for the values that we’re actually trying to test for. If the number of data types that change the behaviour of this function was to increase, this would add to the number of test cases that were needed. To make matters even worse, what happens if something changes to the external dependency that affects our code? It would probably lead to a lot of broken test cases and time would be wasted in trying to fix them all. Such pitfalls makes the process of refactoring code far more tedious than it ought to be.

Now the most natural conclusion to make when seeing code repeated would be to place that behaviour in its own function. That would be great, except for the fact that gMock will not allow the behaviour of the mocks to be defined in a function that can be called from a TEST macro. So how can this problem be solved? Thankfully, googletest provides us with…

Test Fixtures

In googletest, a Test Fixture is a class that allows us to control how a group of tests behave. We can use this to define a common behaviour for mocks and initialise data at the beginning of each test without having to continually repeat ourselves.

To begin with, the following is a bare-bones implementation of a test group derived from the TestFixture class:

TestFixture Implementation

With this class, we can now place the default behaviours of the mocks and initialise any data within the SetUp method as follows:

TestFixture with Common Behaviour

Now for each test run, we no longer have to define the behaviour of the mocks, nor do we have to initialise commonly used data.

You may have also noticed that the mock behaviour is using the ON_CALL method instead of the EXPECT_CALL method. The reason that this is done is that I actually do not want to actually test whether a mocked function is called by default, I just want to define how that mocked function behaves if and when it is called. If I were to use EXPECT_CALL, then I am demanding that all tests for this group must always call these functions. This may not always be the case when tests focus on control flow that may result in sendMessage never being called, causing a test to fail unnecessarily. There’s an interesting read that goes into more detail on why you would use ON_CALL over EXPECT_CALL here: Knowing When to Expect.

With this common setup out of the way, we can refactor the tests so that they are easier to read and far more maintainable. This time, all tests will use the TEST_F macro instead:

Improved Test Cases

Already, these tests become far easier to read, and are far more maintainable. This simplifies the process of modifying tests when changes to the source code are planned, or allows for quick updates to the behaviour of external dependencies if they are changed.

On a side note, you have probably noticed that the tests still repeat themselves. The good news is that unlike gMock’s macros, googletest’s assertion macros can be placed into a function, with parameters allowing us to define what values we want to assert against. Bad news is that a very useful feature of googletest’s Test Explorer window in Visual Studio becomes slightly less useful. For any test that fails, the Test Explorer allows you to jump to the assertion that failed. If that assertion is within a function, it jumps to the line within that function, making it more difficult to see the overall context in which it failed. However, having less code overall is more beneficial in the long term than the slight annoyance introduced by having assertions within a function. In the interest of good coding practice, let’s refactor the tests to make them even more maintainable:

Improved & Refactored Test Cases

Deviating a Mock’s Default Behaviour

It should be clear by now how we can avoid repeating ourselves when defining a mock’s behaviour, but what do you do when testing for edge cases that may need a mock to do something differently? Referring back to the source code, we can observe that there is control flow within the function that will not modify any data when retrieveExternalData returns an error code other than 0:

Source Code: Different Control Flow

The great thing about gMock is that it allows us to create a new behaviour for a mock, and gMock will use the most recent definition that matches, allowing us to use the same Test Fixture. An example of a test case for testing this control flow could be as follows:

Overriding Default Mock Behaviour

In the above example, the mock’s behaviour in ON_CALL is defined after the one in the Test Fixture’s SetUp method, so it will take precedence over the one in SetUp.

It is also interesting that ON_CALL and EXPECT_CALL both follow this rule. This means that the most recent EXPECT_CALL can take precedence over an ON_CALL for the same mock, and vice-versa. This is useful as demonstrated in the above example, where it is important that we assert that sendMessage is never called when the function under test fails to retrieve any data from retrieveExternalData. We achieve this by specifying the cardinality of the mocked function’s call as 0 ( .Times(0) ).

Other Tests to Consider

With this knowledge, consider how the default behaviours would differ when testing the following edge cases, and whether ON_CALL or EXPECT_CALL would be appropriate:

  • Passing a data_type outside of the range accounted for within this function. The function retrieveExternalData would still be called in the same way, but what happens to sendMessage?
  • How could a test be written in the same Test Fixture that forces sendMessage to return an error?

When Not To Deviate Default Mock Behaviour

If you ever find yourself having to deviate a mock’s default behaviour in the same way more than once, it’s probably a good time to ask yourself whether the Test Fixture has done everything that it can reasonably do. To avoid falling into the same trap and repeating yourself, it is probably a good idea to create a new Test Fixture and defining the default behaviour for those mock(s) in its SetUp method. At the end of the day, the less code there is to maintain, the better it is for everyone. If it helps, always pretend that the person lumped with the misfortune of maintaining your code is a psychopath who knows where you live…