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:
- 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.
- 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.
- 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.