Mocking the Singleton

Posted on Wed 13 November 2024 in miscellaneous

Introduction

The singleton is one of the four classical creational patterns from the GoF book. There are innumerable blog posts on how to implement this pattern in a myriad of languages. There are a similarly plentiful supply of posts attempting to steer would-be implementers away form this pattern. First of all, let's take a look at the canonical singleton pattern in C++:

class Singleton
{
public:
  static Singleton& get_instance()
  {
    static Singleton instance;
    return instance;
  }

  Singleton& operator=(const Singleton&) = default;

  int do_magic() { /* Some calculation or another */ }

protected:
  Singleton() = default;
  ~Singleton() = default;
};

The basic objective here is to only ever permit a single instance of the Singleton class to exist. The constructor is private, thereby forcing the user to retrieve a Singleton instance from the Singleton::get_instance static method. We've exposed the assignment operator since we have no qualms about holding references to the singleton -- we just want to forbid construction of more than a single instance of the class.

There are several design patterns for a singleton in C++; above is a version referred to as the Meyer singleton (named for Scott Meyer, who introduced it in his Effective C++ book). On first entry to this method, the statically allocated instance is initialised, and a reference to it returned. Subsequent calls to this method will skip construction (since the static variable already exists), and simply return a reference to this solitary instance of Singleton. This mechanism is also inherently thread-safe, since static variables with block scope are guaranteed to be created only once (there's some hidden magic that goes on behind the scenes using locks). Users of the singleton may then interact with this instance as follows:

int result = Singleton::get_instance().do_magic();

Clever, huh? There are several reasons why one may want to implement a class such that only a single object may be instantiated:

  1. Reality The singleton can be physically-motivated if there are some limited number of hardware resources (think peripheral microcontrollers or communication ports). If there's only a single such hardware resource, use of the singleton reflects this constraint.
  2. Safety The singleton can offer some limited forms of safety when interacting with single hardware resources. In particular, start-up and shut-down routines can be guaranteed to only ever be called once, meaning we don't encounter any undefined behaviour like if we accidentally invoke the start-up procedure twice in a row. Use of the thread-safe singleton also means we never encounter any conflicts or resource contention between threads which may be attempting to concurrently interface with a hardware resource.
  3. Compliance For software to be MISRA-compliant, no use of heap memory is permitted. Because the singleton just wraps a statically-allocated instance of a class, it guarantees that we can't create instances of the class on the heap.

Issues With The Singleton

So why would anyone argue with what seems to be, at worst, a bit of a benign design pattern? Sure its implementation may be a little niche, but who cares? Principal among the objections are based in software good-practice principles. The singleton is a form of global variable; think about it -- it gets instantiated once at some point in the program, and then wraps some state for the remainder of the execution. The actual object never needs to be passed as an argument since it can be retrieved through a static method. In saying this, the singleton doesn't just provide syntactic sugar for global state. It provides mechanisms for thread-safe access to the global state and lazy evaluation since the object is only instantiated when the get_instance method is invoked for the first time.

Perhaps a little more tedious, but as I've come to find out, rather more practical, is the fact that these objects are really difficult to test and mock. Hopefully the user can appreciate that unit testing of a singleton is hideously complicated because it can be accessed from anywhere within the program, making it difficult to explicitly test the myriad sequence of ways in which the singleton might be manipulated, particularly if it wraps some state that's being mutated.

What I'd like to concentrate on in this post is the task of mocking a singleton. Let's take a framework like GTest/ GMock as an example. If we want to mock a class, we derive from it and designate the methods we want to set expectations on;

class MockFoo : public Foo {
  MOCK_METHOD(void, bar, (int), (override));
  MOCK_METHOD(int, baz, (int), (override));
};

{
  MockFoo foo;
  EXPECT_CALL(foo, bar(_)).Times(1);
  EXPECT_CALL(foo, baz(2)).WillOnce(Return(5));
  // Use foo in the testing of some other class
}

So we instantiate the mock, set some expectations on it (bar will be called once with an arbitrary argument and baz will also be called once, but with and argument of 2 and it will return the value 5) and then use it to test some other class that depends on Foo.

The problem here is that GMock works by dependency injection. We first have to be able to derive from the class we're trying to mock; the methods we want to set expectations on are required to be virtual so that we can override them with mocked counterparts. We then go on to instantiate the mocked object, set our expectations and pass the mock around as an argument to whatever functions uses it. But this isn't how the singleton works -- we access it through the get_instance method.

Runtime Singleton

Our first attempt to improve on the singleton is to do away with the pattern altogether. Rather than have the compile-time guarantee that we can only ever instantiate the object once, we can defer this guarantee to runtime by inheriting from a class which is "once-constructible":

class OnceConstructible
{
  public:
    OnceConstructible()
    {
      std::lock_guard<std::mutex> lock(init_mutex_);

      if (initialised_)
      {
        throw std::runtime_error("Object is once-constructible.");
      }
      initialised_ = true;
    }

private:
  inline static bool initialised_ = false;
  mutable std::mutex init_mutex_{};
};

This class is made thread-safe through use of the lock guard, which prevents race conditions on the initialised_ member variable. We can consequently make any class we like be a "runtime singleton" as follows:

class RuntimeSingleton : public OnceConstructible
{
  public:
    RuntimeSingleton() : OnceConstructible() {}
}

And that's it. If anyone tries to instantiate more than one RuntimeSingleton, we get a std::runtime_error thrown. Ultimately this pattern guarantees a class deriving from OnceConstructible can only create a single instance, and consequently is semantically equivalent to the singleton design pattern. However, we don't have any compile-time guarantees like we get with the the singleton. On the other hand, RuntimeSingleton can be mocked like any other class we might encounter; we create the object and pass it around by dependency injection.

Linker Magic

Our next solution involves the notion of a "weak symbol". A symbol in an object file is, by default, "strong". At link time, the linker attempts to resolve external symbols, i.e. those symbols that reference some other module where the symbol is defined. If it encounters a strong symbol, then the linker resolves the symbol. We can however, annotate a symbol with the "weak" attribute. If the linker encounters two symbols, one being annotated strong and another weak, it will resolve the strong symbol. This is problably best illustrated by example:

// strong.cpp
std::string foo() { return "strong"; }

// weak.cpp
std::string __attribute__((weak)) foo() { return "weak"; }

// foo.hpp
#ifndef __FOO_HPP
#define __FOO_HPP

std::string foo();

#endif /* #ifndef __FOO_HPP */ 

So we have three files. One is a header providing the declaration of the function foo which returns a string. Then we have two separate definitions of foo in strong.cpp and weak.cpp, providing strong and weak symbols, respectively. We can try to use this:

#include "foo.hpp"

int main() {
  std::cout << "Symbol was: " << foo() << '\n';
}

While will simply report on which symbol was resolved. Let's try a few use cases:

$> g++ main.cpp strong.cpp -o strong.x
$> ./strong.x
   Symbol was: strong

$> g++ main.cpp weak.cpp -o weak.x
$> ./weak.x
   Symbol was: weak

$> g++ main.cpp main.cpp strong.cpp weak.cpp -o dunno.x
$> ./dunno.x
   Symbol was: strong

That's cool -- we can effectively write two implementations for a function and avoid getting a multiple definition error at link time; we're basically just setting a precedence for the linker if it encounters multiple definitions. This type of trick is useful if you need to provide some stubbed out function that a user may wish to override in the future with their own implementation. For instance, C++ has the concept of "replaceable functions", such as the new and delete operators which can be replaced if a user-provided definition is supplied.

Back to our original intention of mocking a singleton; how does this help us? We first need to modify our original singleton; all those methods we want to set expectations on need to be virtual and we need to move the definition of get_instance() into a separate implementation file, designating it as a weak symbol:

// singleton.hpp
class Singleton
{
public:
  static Singleton& get_instance();
  virtual int do_magic() { /* Some calculation or another */ }
};

// singleton.cpp
#include "singleton.hpp"

__attribute__((weak)) Singleton& Singleton::get_instance() {
    static Singleton instance;
    return instance;
}

Our mocked singleton looks much like any other mock:

// mock_singleton.hpp
#include <gmock/gmock.h>
#include "singleton.hpp"

class MockSingleton : public Singleton {
public:
  MOCK_METHOD(int, do_magic, (), (override));
};

But now, we add a strongly-linked get_instance() in a separate implementation file:

// mock_singleton.cpp
#include "mock_singleton.hpp"

Singleton& Singleton::get_instance() {
  static MockSingleton instance;
  return instance;
}

Now, when we just create an application using singleton.cpp, we get the expected concrete singleton behaviour. Nothing to see here. If we link against the object file resulting from mock_singleton.cpp though, calling Singleton::get_instance() will return a MockSingleton, which we can set expectations on!

#include "singleton.hpp"

int main() {
  auto mock = dynamic_cast<MockSingleton&>(Singleton::get_instance());
  EXPECT_CALL(mock, do_magic).WillRepeatedly(::testing::Return(2));
  std::cout << Singleton::get_instance().do_magic() << '\n';
}

Now, when we build our main application, we simply include singleton.cpp in our build and Singleton::get_instance() returns a reference to our concrete Singleton as usual.

The reader may be wondering at this point why we need to play with the linker at all -- surely if we want to use a mock, we simply link against mock_singleton.o. If we want to use a concrete Singleton, we simple link against singleton.o. While this is all well and good for examples that we can build directly on the command line, but if we try this in a large project with a build tool, this conditional linking is tedious and frankly it's far easier to just annotate a symbol as being weak or strong.

Unfortunately, since annotating symbols is not a part of the C/C++ standard, the means by which we designate a weak or strong symbol is not portable amongst compilers. In our examples, we've leverage the __attribute__((weak)) annotation that's available with gcc, QQ: clang. However, we see that the use of symbol annotation means we need only perform moderate refactoring of any existing software, while offering all the same "benefits" of the singleton design pattern as outlined in the GoF book. Admittedly it leverages a fairly niche feature of the compiler (if it exists at all), but appropriate documentation makes it comprehensible.