It’s well-known that it’s difficult to write test doubles/fakes/mocks/stubs for the STL. But there are plenty of reasons to do so: for example, writing unit tests where you’re using std::chrono::system_clock::now(), or std::mt19937 (or other random number generator) or std::ofstream (might be slow), etc.

Ideally, we want to touch the legacy code as lightly as possible, so wrapping up stl types in our own types and replacing every instance with our types is out. Similarly, silly macro tricks are out, as are linker tricks which are mostly not portable.

But “Test Base Class Injection” can handle it. If you haven’t read about that yet, please do so now: TBCI

Problem Statement: write tests for legacy code

Suppose you have this simple D-dimensional Monte Carlo class:


#include <random>
#include <array>

namespace Monte
{
template <int D>
class Carlo
{
    std::mt19937 rng;
    std::uniform_real_distribution<> U01;
    std::array<double, D> lo, hi;

public:
    Carlo(const std::array<double, D>& lo, const std::array<double, D>& hi, uint32_t seed = 12345)
         : rng(seed), U01(0.0, 1.0), lo(lo), hi(hi)
    {
        for (int d=0; d<D; ++d) {
            if (hi[d] <= lo[d]) {
                std::string msg = "Invalid bounding box: hi[" + std::to_string(d) + "] <= lo[" + std::to_string(d) + "]";
                throw std::invalid_argument(msg);
            }
        }
    }

    template<typename F>
    double run(int n, F payoff)
    {
        std::array<double, D> point{};
        double sum = 0.0;
        for (int i=0; i<n; ++i)
        {
            for (int d=0; d<D; ++d)
                point[d] = lo[d] + (hi[d] - lo[d])*U01(rng);
            sum += payoff(point);
        }
        double volumeOfBoundingBox = 1.0;
        for (int d=0; d<D; ++d)
            volumeOfBoundingBox *= (hi[d] - lo[d]);
        return volumeOfBoundingBox*(sum/n);
    }
};
}

What we’d like is to write unit tests, but they have to be deterministic, so mocking std::mt19937 is our goal. But there are some constraints:

  1. Touch the code as lightly as possible
  2. No macros
  3. No wrappers
  4. No virtual methods
  5. No funky build or linker tricks
  6. No adding to the std namespace (which is undefined behavior)

Clearly, we’re going to use TBCI, but how?

If you’d like to think about it for a bit, go ahead; when you’re ready, click Next.

Back to TBCI landing page
Back to MiddleRaster’s pages