Test fixtures

Last updated on 2026-06-30 | Edit this page

Overview

Questions

  • I am writing the same setup code in every test — is there a better way?
  • How do I share a complex starting state across many tests without tests interfering with each other?

Objectives

  • Identify repeated setup code across tests as a signal that a fixture is appropriate
  • Write a TEST_F() fixture class with a SetUp() method for Histogram
  • Explain that SetUp() runs fresh before every test and that tests do not share state
  • Refactor existing Histogram tests to use a fixture where appropriate
  • Explain why construction tests should remain outside the fixture

Introduction


We’ve written quite an extensive series of tests for Histogram, and many of them follow the same pattern e.g.:

CPP

TEST(HistogramFill, WeightedFillProducesCorrectCounts) {
    // 1. Construct histogram and comparison data
    Histogram h(10, 0.0f, 1.0f);
    std::vector<float> expected(10, 0.0f);

    // 2. Prepare state
    h.fill(0.1f, 0.5f);
    h.fill(0.6f, 1.5f);
    expected[0] = 0.5f;
    expected[5] = 1.5f;

    // 3. Run assertions
    EXPECT_THAT(h.bin_counts(), ::testing::Pointwise(::testing::FloatEq(), expected));
}

As our suite grew, we were unwittingly introducing a maintenance burden - if the Histogram constructor changes in the future, we would have to update it in every test, if we wanted to uses different binning/values, we’d also have to update those. There is a slightly subtler problem too: when a test fails, we have to read through the setup code to understand the starting state. Test Fixtures are a solution to this when a set of test cases need a common starting state.

Writing fixtures in GoogleTest


Fixtures have state, so are naturally programmed as classes in C++. GoogleTest provide a base class ::testing::Test from which our fixture needs to inherit. As this in our case this is purely associated with the Histogram tests, we can put it in tests/test_histogram.cpp before we add any test cases:

CPP

//! \file test_histogram.cpp
#include "histogram.hpp"

#include <gtest/gtest.h>
#include <gmock/gmock.h>

//! Setup a histogram and an expected result vector to test filling operations
class HistogramFillTest : public ::testing::Test {
protected:
    Histogram h{10, 0.0f, 1.0f};
    std::vector<float> expected(10, 0.0f);
};

// Tests follow

This is the close to the simplest possible fixture - just some basic structured data. Let’s immediately use this in one of our HistogramFill suite’s test cases:

CPP

TEST_F(HistogramFillTest, SingleFillLeavesOtherBinsZero) 
{
    h.fill(0.45f);
    expected[4] = 1.0f;
    EXPECT_THAT(h.bin_counts(), ::testing::ContainerEq(expected));
}

All we’ve done is use the TEST_F macro instead of TEST. Here, the first argument must be the type name of the Test Fixture class we want to use. Without going into the gory details, TEST_F essentially creates a new class HistogramFillTest_SingleFillLeavesOtherBinsZero that inherits from HistogramFillTest and sets up things so that running this test is roughly:

  1. Construct an instance X of this class (runs its constructor)
  2. Run X->SetUp() (see later)
  3. Run the test body (what we coded between the {...} )
  4. Run X->TearDown() (see later)
  5. Destructs instance X (runs its destructor)

This is why we declared our fixture’s data members as protected - we can access them directly in any subclass. In addition the subclassing and execution pattern mean that all fixture state is isolated to a specific test case - note the h and expected variables in each test case will be different. That’s exactly what we had before with TEST and setting things up in each test case, but we’ve been able put that code in one place using the fixture mechanism. If we ever wanted to change the default set up for Histogram fill, we only need do it in one place.

Challenge

Challenge

  1. Refactor your remaining HistogramFill test cases to use the fixture. Confirm they still work!
  2. Could/Should you use this fixture, or another, for the HistogramConstruction suite?
  1. This should just be a matter of find/replace in the HistogramFill suites
  2. We **shouldn’t* use the fixture, or create a new one, for the HistogramConstruction suite. A Histogram instance would already have been constructed by the fixture’s constructor, and we’d have no chance to actually put asserts around Histogram{10,0,1}.
Caution

It is possible to share state between all test cases in a test suite/fixture and at the program levell too. These are advanced topics for which care is needed.

More complex fixtures


Test fixtures give us two pairs of places we can do more complex setup and teardown of the state. We can either:

  1. setup the state either in the fixture class constructor and do teardown in the fixture’s destructor
  2. Override the virtual SetUp() member function to setup the state, and override the virtual TearDown() member function to teardown the state (Note the capitalization!)

These give us freedom to create complex but repeatable and isolated states. Let’s use the second method to create a new

CPP

//! \file test_histogram.cpp
#include "histogram.hpp"

#include <gtest/gtest.h>
#include <gmock/gmock.h>

class FilledHistogramTest : public ::testing::Test {
protected:
    void SetUp() override {
        // 10 bins over [0, 10): each bin covers 1 unit
        // Fill the Ith bin I times
        for (int bin = 0; bin < 10; ++bin)
            for (int i = 0; i < bin+1; ++i)
                h.fill(bin + 0.5f);
        
        // Add one underflow entry
        h.fill(-1.0f);
    }

    Histogram h{10, 0.0f, 10.0f};
};

We don’t need a TearDown in this fixture as we’re not doing anything that would require it like, for example, memory or temporary file management. We can then write a test case for this fixture:

CPP

TEST_F(LinearHistogramTest, TotalEntryCount) 
{
    // 50 in-range + 1 underflow
    EXPECT_EQ(h.n_entries(), 51);
}
Challenge

Challenge

  1. Write TEST_F cases for n_underflow() and n_overflow().
  2. Write one that checks bin_counts() using Pointwise.
  3. Write one that checks the mean is as expected.

Before writing, work out by hand what the expected values should be — the fixture setup tells you everything you need.

CPP

// 1. Under/Overflow are trivial!
TEST_F(LinearHistogramTest, UnderflowCount)
{
    EXPECT_EQ(h.n_underflow(), 1);
}

TEST_F(LinearHistogramTest, OverflowCount)
{
    EXPECT_EQ(h.n_overflow(), 0);
}

// 2. Like before, we use `FloatEq`
TEST_F(LinearHistogramTest, BinCounts)
{
    std::vector<float> expected{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f};
    EXPECT_THAT(h.bin_counts(), ::testing::Pointwise(::testing::FloatEq(), expected));
}

// 3. `EXPECT_NEAR` given large number of operations
TEST_F(LinearHistogramTest, MeanIsNearCentreOfRange)
{
    // Nth bin filled N+1 times
    // Nth bin center at 1.0*N + 0.5 -> Sum_{bins}(fill*bin_center) = 357.5
    // 55 fills in total (Underflow fill of -1.0f is excluded from mean)
    EXPECT_NEAR(h.mean(), 357.5f/55.0f, 0.01);
}

The key thing here is that whilst the tests are simple, they are only so because of the fixture. Imagine repeating that setup across five tests!

We now have a well-organised test suite: construction tests that stand alone, behavioural tests grouped into suites, and a fixture that gives tests needing a realistic starting state a clean, shared setup. The next question is: how do we know whether this suite is thorough? We have been writing tests based on our reading of Histogram’s specification — but the specification may not have told us about every branch in the implementation. In the next episode we will look at the implementation itself for the first time, and use coverage tools to find the gaps.

Callout

This is where we stop on introducing further GoogleTest capabilities. There is much more it can do, so do take the time to read through its documentation and see what else it can do.

Key Points
  • A fixture eliminates repeated setup code and makes the intended starting state of each test using that fixture explicit.
  • SetUp() runs before every individual test — each test starts from a clean, identical state regardless of what other tests do
  • Fixtures do not change what is being tested, only how the starting state is prepared
  • Construction tests belong outside the fixture — the fixture assumes construction succeeds and tests behaviour from that point