Transforming singletons into something testable

Reading time ~10 minutes

Legacy code and singleton classes seem to go hand in hand which will cause problems when trying to write testable code. This post will look at how a singleton can be transformed into something that is better.

Disclaimer

First of all before we start with anything - I don’t like singletons! This article is not about trying to make singletons something we shall use by dressing it in a nicer costume. The only reason I write this is because singletons exists - especially in legacy code.

One can argue that there are a use for a singleton, and sure there might be. But this blog post is not about that. This blog post is about making your code a little bit simpler to test.

Painful to remove singletons

If you have a large legacy code base that you would like to transform into something more testable singletons can stand in your way. Why you ask. Well frankly because it’s not possible to mock them. In some cases this isn’t a problem for your unit-tests - but in 99% of the cases it is.

Trying to remove singletons is probably the best solution to start with. But if your code base is large this might be really problematic since your architecture probably is built around singleton usage. But just let me be super clear here. Removing singletons is always the best option. What I describe in the rest of this post is a problem solver when removing singletons is not an option.

Classic singleton

As always lets look at code.

In this example I’ve created a SettingsSingleton class. This class can return the values for a specific settings. The settings are read from a file, settings.data, when instantiated. This is a really over simplified implementation. One can of course argue that there no need to a singleton in this case. But think of this as something simple to illustrate.

#pragma once

#include <map>

/**
 * @brief The SettingsSingleton class
 *
 * This is a really simplyfied class just to
 * illustrate a singleton.
 *
 * Yes I know this implementation is not thread safe,
 * but this code is written to illustrate an example.
 */
class SettingsSingleton
{
public:
    enum class Parameter {
        PinCode,
        AlarmTemperature,
        PortNumber,
        InstallationId
    };

    static SettingsSingleton *instance();

    bool hasValue(const Parameter &parameter) const;
    int value(const Parameter &parameter) const;


private:
    SettingsSingleton();

    static SettingsSingleton *m_instance;

    std::map<Parameter, int> m_values;
};
#include "settingssingleton.h"

#include <fstream>
#include <iostream>

SettingsSingleton* SettingsSingleton::m_instance = nullptr;

constexpr const auto settingsFile = "settings.data";

SettingsSingleton *SettingsSingleton::instance()
{
    if (m_instance == nullptr) {
        m_instance = new SettingsSingleton();
    }

    return m_instance;
}

/**
 * @brief SettingsSingleton::hasValue
 * @param parameter Parameter to ask for
 * @return True if the parameter has a valid value, otherwise false
 */
bool SettingsSingleton::hasValue(const SettingsSingleton::Parameter &parameter) const
{
    return m_values.find(parameter) != m_values.end();
}

/**
 * @brief SettingsSingleton::value
 * @param parameter Parameter to ask for
 * @return value if parameter, -1 if invalid.
 *
 * Make sure to check that the parameter has a valid value
 * by calling SettingsSingleton::hasValue() before.
 */
int SettingsSingleton::value(const SettingsSingleton::Parameter &parameter) const
{
    if (hasValue(parameter)) {
        return m_values.at(parameter);
    }
    return -1;
}

SettingsSingleton::SettingsSingleton()
{
    std::ifstream inFile;

    int parameter = 0;
    int value = 0;

    inFile.open(settingsFile, std::ifstream::in);

    while(inFile >> parameter >> value)
    {
        auto p = static_cast<Parameter>(parameter);
        m_values[p] = value;
    }
}

Now we have a really simple SettingsSingleton implementation that anyone is free to use. For example our super simple PinCode class which can determine if an entered pin code is correct or not.

#pragma once

/**
 * @brief The PinCode class
 *
 * This class is just a very simple example
 * of a class that use the SettingsSingleton.
 *
 * Yes, the singleton is not really needed here,
 * but this is just a example to illustrate the problem.
 */
class PinCode
{
public:
    PinCode() = default;

    bool validPinCode(const int enteredPinCode) const;
};
#include "pincode.h"
#include "settingssingleton.h"

/**
 * @brief PinCode::validPinCode
 * @param enteredPinCode
 * @return True if the enteredPinCode is correct.
 */
bool PinCode::validPinCode(const int enteredPinCode) const
{
    auto parameter = SettingsSingleton::Parameter::PinCode;

    if (SettingsSingleton::instance()->hasValue(parameter)) {
        return SettingsSingleton::instance()->value(parameter) == enteredPinCode;
    }

    return false;
}

Using the PinCode class is done by just doing something like this:

if (pinCode.validPinCode(1234)) {
    std::cout << "is valid" << std::endl;
} else {
    std::cout << "is not valid" << std::endl;
}

So now we have our classic singleton nightmare. The class PinCode depends on SettingsSingleton and trying to write a test-case for PinCode is a problem.

Yes - once again one can argue that there is no need for a singleton. Sure - but it’s a singleton for demonstrational purposes.

Transforming this into something more testable

Now let’s look at how we can transform this into something that we can make testable, without removing the singleton. Remember - removing the singleton is the best option. But in this blog post we try to solve an issue were the legacy code makes it more or less impossible to remove the singleton.

First of all - why transform it?

The goal with the rest of the post is to be able to write a test-case for the PinCode class where I’m able to mock the settings parameters.

Step 1: Create an interface for your singleton

In our example the SettingsSingleton has two functions, hasValue and value.

bool hasValue(const Parameter &parameter) const;
int value(const Parameter &parameter) const;

This is what our new interface class shall contain. We also need to move our enum into the interface class.

#pragma once

class ISettings {
public:
    virtual ~ISettings() = default;

    enum class Parameter {
        PinCode,
        AlarmTemperature,
        PortNumber,
        InstallationId
    };

    virtual bool hasValue(const Parameter &parameter) const = 0;
    virtual int value(const Parameter &parameter) const = 0;
};

Step 2: Add the possibility to initialize the singleton

The next thing we need to do might be a bit unorthodox when working with singletons. But the hole idea with this post is to step away from the singleton-pattern a bit.

The idea here is to not let the singleton it self initialize it self but let “us” initialize it at program start (or when it’s a good idea to do it). So instead of creating the singleton the first time we use it we need to “create” it before the first time we use ut. This is were I might step on some people toes. Now we are creating a situation were we might, by mistake, use a singleton before it’s initialized. This is something I’m aware of and something I feel can be okay in comparison. Because by doing this we make our code base simpler to test. So I thing this is a scarify I’m willing to do.

Our new singleton looks like this

#pragma once

#include "isettings.h"

#include <cassert>
#include <memory>

/**
 * @brief The SettingsSingleton class
 *
 * This is a really simplyfied class just to
 * illustrate a singleton.
 *
 * Yes I know this implementation is not thread safe,
 * but this code is written to illustrate an example.
 */
class SettingsSingleton
{
public:
    static void initialize(std::unique_ptr<ISettings> implementation);
    static SettingsSingleton *instance();

    bool hasValue(const ISettings::Parameter &parameter) const;
    int value(const ISettings::Parameter &parameter) const;


private:
    SettingsSingleton() = default;

    static SettingsSingleton *m_instance;

    std::unique_ptr<ISettings> m_implementation;
};
#include "settingssingleton.h"

#include <iostream>

SettingsSingleton* SettingsSingleton::m_instance = nullptr;


void SettingsSingleton::initialize(std::unique_ptr<ISettings> implementation)
{
    assert(m_instance == nullptr);
    m_instance = new SettingsSingleton();
    m_instance->m_implementation = std::move(implementation);
}

SettingsSingleton *SettingsSingleton::instance()
{
    assert(m_instance != nullptr);
    return m_instance;
}

/**
 * @brief SettingsSingleton::hasValue
 * @param parameter Parameter to ask for
 * @return True if the parameter has a valid value, otherwise false
 */
bool SettingsSingleton::hasValue(const ISettings::Parameter &parameter) const
{
    if (m_implementation) {
        return m_implementation->hasValue(parameter);
    }
    return false;
}

/**
 * @brief SettingsSingleton::value
 * @param parameter Parameter to ask for
 * @return value if parameter, -1 if invalid.
 *
 * Make sure to check that the parameter has a valid value
 * by calling SettingsSingleton::hasValue() before.
 */
int SettingsSingleton::value(const ISettings::Parameter &parameter) const
{
    if (m_implementation) {
        return m_implementation->value(parameter);
    }
    return -1;
}

There are two big changes.

  1. We now have a initialize-function.
  2. We now need an implementation for the singleton to work.

initialize-function - This is the magic

void SettingsSingleton::initialize(std::unique_ptr<ISettings> implementation)
{
    assert(m_instance == nullptr);
    m_instance = new SettingsSingleton();
    m_instance->m_implementation = std::move(implementation);
}

The above code is the magic that makes our project more testable. What we do here is allowing the user to initialize our singleton with any type implementation as long as it implements the ISettings interface. The reason I used a std::unique_ptr here is to make it clear that the SettingsSingleton takes ownership of the implementation.

To initialize the singleton we do:

SettingsSingleton::initialize(std::make_unique<SettingsImpl>());

In the above code we initialize our singleton with the SettingsImpl class.

The implementation class

As you can see above we now need an implementation class. Our singleton is transformed in a way so every call to it will try to use the implementation. For example the value()-function now looks like this:

int SettingsSingleton::value(const ISettings::Parameter &parameter) const
{
    if (m_implementation) {
        return m_implementation->value(parameter);
    }
    return -1;
}

This assumes that our unique_ptr is initialized and the just let the implementation do whatever it likes.

When transforming the above singleton I created a SettingsImpl-class that looks like this

#pragma once

#include "isettings.h"

#include <map>

class SettingsImpl : public ISettings
{
public:
    SettingsImpl();

    bool hasValue(const Parameter &parameter) const override;
    int value(const Parameter &parameter) const override;

private:
    std::map<ISettings::Parameter, int> m_values;
};
#include "settingsimpl.h"
#include <fstream>

constexpr const auto settingsFile = "settings.data";

SettingsImpl::SettingsImpl()
{
    std::ifstream inFile;

    int parameter = 0;
    int value = 0;

    inFile.open(settingsFile, std::ifstream::in);

    while(inFile >> parameter >> value)
    {
        auto p = static_cast<ISettings::Parameter>(parameter);
        m_values[p] = value;
    }
}

/**
 * @brief SettingsImpl::hasValue
 * @param parameter Parameter to ask for
 * @return True if the parameter has a valid value, otherwise false
 */
bool SettingsImpl::hasValue(const ISettings::Parameter &parameter) const
{
    return m_values.find(parameter) != m_values.end();
}

/**
 * @brief SettingsImpl::value
 * @param parameter Parameter to ask for
 * @return value if parameter, -1 if invalid.
 *
 * Make sure to check that the parameter has a valid value
 * by calling SettingsSingleton::hasValue() before.
 */
int SettingsImpl::value(const ISettings::Parameter &parameter) const
{
    if (hasValue(parameter)) {
        return m_values.at(parameter);
    }
    return -1;
}

This implementation is exactly the code we have in our singleton from start. So no functionality is changed.

Wrapping this transformation up with a unit test

Since we transformed our classic singleton into something a bit more testable we can create a test case for PinCode where we mock the use of our SettingsSingleton class.

This test case uses GTest, but any test framework will do the job here.

#include "gtest/gtest.h"

#include <isettings.h>
#include <pincode.h>
#include <settingssingleton.h>

class SettingsMock : public ISettings
{
public:
    bool hasValue(const Parameter &parameter) const override;
    int value(const Parameter &parameter) const override;
};

bool SettingsMock::hasValue(const Parameter &parameter) const
{
    return parameter == ISettings::Parameter::PinCode;
}

int SettingsMock::value(const Parameter &parameter) const
{
    if (parameter == ISettings::Parameter::PinCode) {
        return 1234;
    }
    return -1;
}

TEST(testPinCode, sample_test)
{
    SettingsSingleton::initialize(std::make_unique<SettingsMock>());

    PinCode pinCode;
    EXPECT_EQ(pinCode.validPinCode(1234), true);
    EXPECT_EQ(pinCode.validPinCode(0), false);
    EXPECT_EQ(pinCode.validPinCode(1), false);
    EXPECT_EQ(pinCode.validPinCode(2), false);
}

In the test case we create a class called SettingsMock which is derived from ISettings. This allows us to do SettingsSingleton::initialize<SettingsMock>(); which will initialize our singleton implementation with our mocked object!

Conclusion

Once again, this is not a post pushing to more use of singletons - it’s the exact opposite!

The drawback with this is that the singleton needs to be initialized. This can, in some cases, be problematic to find the right place for doing initialization. Another drawback is the possibility to use the singleton before it’s initialized.

On the other hand getting a singleton implementation that is possible to use in unit testing is a really really good thing if your legacy code base uses singletons.

Example code

All the code for this blog post is available at my github repo. It is a CMake based project that shall be portable to any platform, but I only tested it on Linux.

It contains three branches.

  • classic-singleton - This is what we start the example with. A classic implementation of a singleton.
  • singleton-with-interface - First step of transformation by adding an interface and implementation. This is what I talk about in step 1 and 2.
  • tranformed-with-testcase - This is the final result with a simple GTest based test case on PinCode.

Scoped enum:s together with bit flags/patterns

Use scoped enums to get better type safety, but this will show you how to handle the logic operations. Continue reading

Flutter on embedded - Sneak Peek

Published on January 08, 2021