3

Preface: I am aware that the following is a very dirty hack. We will refactor things (so we can write non-hacky tests, use Mocks and possibly even get rid of the Singletons), but first we need test coverage...

As said before, we have a significant number of "Singleton" type classes in our project, i.e. something along the lines of.

class SomeSingleton
{
  public:
    static SomeSingleton& getSingleton()
    { 
        if (m_singletonInstance == nullptr)
            m_singletonInstance = new SomeSingleton();
        return *m_singletonInstance;
    }

  protected:
    SomeSingleton();
    virtual ~SomeSingleton();

    static SomeSingleton* m_singletonInstance = nullptr;
}

In order to properly run our unit tests the Singleton needs to be "reset" (or data from our previous tests may persist in the singleton, influencing the current test). Unfortunately there is no feasible way to do that (the Singleton instance and the destructor are protected).

We do not wish to change our production code to contain test-only functionality (i.e. no forward declaring a ::test::SomeSingletonResetter as a friend in SomeSingleton, no introducting a public void resetSingleton_forTestUseOnly() function etc.) - and we also wish to avoid any significant changes to the production code until we have test coverage in place.

So, here's the dirty hack we are considering: In the test project we derive a very simple wrapper class (i.e. no members) from the Singleton with a static function that deletes the singleton - however as the destructor cannot be called even from the derived class (it is protected) we would static_cast the singleton to the derived class and delete that:

class SomeSingletonWrapper : public SomeSingleton
{
  public:
    SomeSingletonWrapper();
    ~SomeSingletonWrapper() override;

    static void reset()
    {
        //can't delete/call destructor on SomeSingleton since it is protected
        delete static_cast<SomeSingletonWrapper*>(m_singletonInstance);
        m_singletonInstance = nullptr;
    }
}

Our thoughts are, the classes are essentially the same size, and the derive classes' destructor will call the base destructor, so the class gets destroyed properly. Can this work (until we can refactor things) or will it horribly explode in our faces? And is there a better (less hacky) way to archive this (without majorly modifying the production code)?

Edit:

I am aware that the alternative would have been to simply not call the destructor (and only set m_singletonInstance = nullptr) but that would be even worse since I now leak memory with every single test and those singletons keep floating around until the test app terminates, doing god-knows-what... brrr....

CharonX
  • 2,130
  • 11
  • 33
  • Dependency inversion is the solution. The 'D' in SOLID. – Eljay Feb 25 '19 at 13:40
  • Shouldn't you mock `SomeSingleton`? Otherwise your tests depend on it in addition to the code you're actually testing. – rustyx Feb 25 '19 at 13:49
  • @rustyx I want to mock it, but I want/need to have tests in place before I change the code so I become able to mock it... – CharonX Feb 25 '19 at 14:32
  • 1
    @Eljay This is the intention, but in order to move away from `getSingleton()` calls I need to change the code quite a bit. And in order to do that I want to have tests in place, to make sure I don't break things. And in order to have tests... – CharonX Feb 25 '19 at 14:34

1 Answers1

2

According to the standard 5.3.5.2:

In the first alternative (delete object), the value of the operand of delete may be a null pointer value, a pointer to a non-array object created by a previous new-expression, or a pointer to a subobject (1.8) representing a base class of such an object (Clause 10). If not, the behavior is undefined.

Since you are deleting a super-class, not a sub-class, and the pointer was not previously created as an instance of that super-class, you are in undefined territory. So you may get lucky and it will work sometimes on some systems with some compiler, but there is no guarantee.

Can you override getSingleton()? It's not virtual or static in the code given, but it should probably be one of those. If the former, there's an easier solution (override it in your wrapper). If not, you might consider trying to replace the created value with your wrapper:

class SomeSingletonWrapper : public SomeSingleton
{
public:
    static void injectWrapper()
    {
        // Should be null or else someone used it before we replaced it
        if( m_singletonInstance != nullptr ) 
        {
            throw std::exception( "Singleton wrapping failed!" );
        }
        m_singletonInstance = new SomeSingletonWrapper();
    }

    static void resetWrapper()
    {
        auto wrapper = dynamic_cast<SomeSingletonWrapper*>( m_singletonInstance );
        if( wrapper == nullptr )
        {
            throw std::exception( "Singleton wrapping failed in the reset!" );
        }

        delete wrapper;
        m_singletonInstance = nullptr;
    }
}

Then in your test, replace it before anyone uses it (but beware the static order initialization fiasco where something else might get the old instance first):

class MyTest
{
public:
    void testSetup()
    {
        SomeSingletonWrapper::injectWrapper();    
    }

    void testCleanup()
    {
        SomeSingletonWrapper::resetWrapper();
    }

    void testMySingletonUse()
    {
        auto& single = SomeSingleton::getInstance();
        ASSERT( dynamic_cast<SomeSingletonWrapper*>( &single ) ); // Yay! It worked!
        // More testy stuff using 'single'
    }
}
metal
  • 6,202
  • 1
  • 34
  • 49
  • Ah, apologies, the getSingleton **is** static (I had the feeling I was forgetting something when I assembled the example code) - I've updated the question – CharonX Feb 25 '19 at 14:15
  • Thanks, excellent idea with the injectWrapper, but the `delete m_singletonInstance` is what is giving me the worst headache - `m_singletonInstance` is of type `SomeSingleton*` - which has a protected destructor, so any attempt to `delete m_singletonInstance` (even - weirdly enough - from within SomeSingletonWrapper) makes the compiler complain that it is protected. Argh. – CharonX Feb 25 '19 at 14:23
  • @CharonX: My bad. I fixed it in the above. Now, the cast is checked and is valid because of the inject function. – metal Feb 25 '19 at 14:26
  • Yes, that works and avoids the undefined behaviour (aka horrible, dirty hack) issue! Awesome & thanks! – CharonX Feb 25 '19 at 14:29
  • You're welcome. I made one minor change: the public destructor in the wrapper was no longer needed since the derived class is deleting the wrapper, which only requires its own destructor. – metal Feb 25 '19 at 15:43