Testing methods that depend upon a singleton class

Posted on December 11, 2008

3


Singleton is probably one of the most (mis-)used design patterns. In this article I intend to lay out a scenario and then demonstrate how we can go about testing methods that depend upon a singleton class. Finally I’ll recap the whole approach in a bulleted list for easy reference.

Let’s restrict our problem to involve just 2 classes for the purpose of this article. Assume there’s already a class named Transaction and we’re interested in testing its CompleteTransaction method.

public class Transaction
{
public void CompleteTransaction()
{
DeviceManager.Instance.OpenCashDrawer();
}
}

In our test we plan to ensure that CompleteTransaction calls the OpenCashDrawer method of DeviceManager.

public sealed class DeviceManager
{
private static readonly DeviceManager instance = new DeviceManager();

private DeviceManager()
{
}

public static DeviceManager Instance
{
get
{
return instance;
}
}

public void OpenCashDrawer()
{
// Code for opening cash-drawer will go here.
Console.WriteLine(“CashDrawer opened…”);
}

}

DeviceManager is a singleton and there lies our problem. If there was an interface that DeviceManager implemented and CompleteTransaction took an object implementing that interface as argument we could have used NMock to mock that interface and created expectations in our test to expect a call to its OpenCashDrawer method when CompleteTransaction is called. We want to reduce the code churn so another alternative is needed. Changing the singleton semantics of DeviceManager or changing the CompleteTransaction method’s signature – are alternatives which will potentially require modification of a lot of code and testing it.

But having an interface in the picture will make our job easier so here’s what we can do.

  1. Extract interface: We can extract an interface containing all the methods contained in DeviceManager class. Let’s say, we’re going to call it IDeviceManager.
  2. Change types: We can then modify the type of static instance field and static Instance property to be IDeviceManager instead of DeviceManager.
  3. Plant mock: In our test for CompleteTransaction method if we could somehow plant a mock object implementing IDeviceManager in the DeviceManager.instance field, we can then create an expectation in our test saying that expect a call to OpenCashDrawer method.

Extract interface

This is easy to do in our example. In the real world, especially for a class that has been around for years, there could potentially be hundreds of methods. If that’s the case you have, take a look at the DevExpress Refactor add-in for Visual Studio which just requires you to right-click on a class and select “Extract interface”.

In our example, here’s what we end up with:

public sealed class DeviceManager : IDeviceManager
{
private static readonly DeviceManager instance = new DeviceManager();

private DeviceManager()
{
}

public static DeviceManager Instance
{
get
{
return instance;
}
}

public void OpenCashDrawer()
{
// Code for opening cash-drawer will go here.
Console.WriteLine(“CashDrawer opened…”);
}

}

public interface IDeviceManager
{
void OpenCashDrawer();
}

At this point all of your code should compile just fine.

Change types

We simply change the type of instance field and Instance property to be IDeviceManager.

public sealed class DeviceManager : IDeviceManager
{
private static IDeviceManager instance = new DeviceManager();

private DeviceManager()
{
}

public static IDeviceManager Instance
{
get
{
return instance;
}
}

public void OpenCashDrawer()
{
// Code for opening cash-drawer will go here.
Console.WriteLine(“CashDrawer opened…”);
}

}

public interface IDeviceManager
{
void OpenCashDrawer();
}

Again at this point the code should compile just fine. If it doesn’t, as would be the case where you have code that saves an instance of DeviceManager in a local variable before doing stuff with it, you’ll have to refactor that code to get rid of all compilation errors.

Plant mock

How do we plant a mock-object in the private static field DeviceManager.instance?

  1. We start by making use of the private-accessors feature of Visual Studio 2008. Right-click on DeviceManager class and select “Create Private Accessors”. You can read more about private accessors here. The basic idea is that a DeviceManager_Accessor class will be created mirroring the public methods of DeviceManager but additionally also exposing its private and protected fields. Internally a DeviceManager_Accessor object manages a DeviceManager object and forwards all method calls and field accesses to it. You can also create private-accessors from the Visual Studio command-prompt using “publicize ”.
  2. We write our test to create a mock-object implementing IDeviceManager and plant it in the DeviceManager’s static instance field using DeviceManager_Accessor.

    IDeviceManager mockDeviceManager = mocks.NewMock< IDeviceManager>();
    DeviceManager_Accessor.instance = mockDeviceManager;

  3. But there’s something to watch out for. It is possible that this test runs as part of a suite of tests so you want to ensure that DeviceManager.Instance returns a normal DeviceManager object after your test has finished executing. It is not sufficient to set DeviceManager_Accessor to null – you must restore the DeviceManager_Accessor.instance field to whatever its value was before you set it to a mock object. Since we want to ensure that the instance static field gets reset even if our test code throws an exception, we do the resetting in a finally block. Here’s what the test-method’s body would look like.

    using (Mockery mocks = new Mockery())
    {
    // save
    IDeviceManager prevDeviceManager = DeviceManager_Accessor.instance;

    try
    {
    // plant
    IDeviceManager mockDeviceManager = mocks.NewMock();
    DeviceManager_Accessor.instance = mockDeviceManager;

    // TODO: write test code here.
    }
    finally
    {
    // restore
    DeviceManager_Accessor.instance = prevDeviceManager;
    }
    }

The test

Now that we have everything in place, let’s write the actual test code. Before calling CompleteTransaction, we create an NMock expectation to ensure that OpenCashDrawer is called on the mock IDeviceManager object.

[TestMethod()]
public void CompleteTransactionTest()
{
using (Mockery mocks = new Mockery())
{
// save
IDeviceManager prevDeviceManager = DeviceManager_Accessor.instance;

try
{
// plant
IDeviceManager mockDeviceManager = mocks.NewMock();
DeviceManager_Accessor.instance = mockDeviceManager;

// we expect OpenCashDrawer to be called
Expect.Once.On(mockDeviceManager).Method(“OpenCashDrawer”);

// the actual
Transaction target = new Transaction();
target.CompleteTransaction();
}
finally
{
// restore
DeviceManager_Accessor.instance = prevDeviceManager;
}
}

}

Run the test and it should pass. Change the body of CompleteTransaction method to not make a call to OpenCashDrawer and the test should fail.

Recap of the approach

  1. Extract an interface from the singleton class.
  2. Modify the singleton class to implement the newly created interface.
  3. Modify the type of the static field (= VB.NET shared) holding a reference of the singleton class, and the return-type of the method/property returning the singleton to be same as the newly created interface.
  4. Generate an accessor assembly for the assembly containing the singleton class for use in your unit-tests.
  5. Write your unit-test to create a mock object implementing the newly introduced interface and plant it in the static field of the singleton class.
  6. Create necessary expectations on various methods on the newly introduced interface which you expect to be called by the function under test.
  7. Once your test is finished, ENSURE that the previous singleton is restored (using finally block).

If, however, you’re in the process of writing a new singleton class in your project, consider introducing an interface and create a façade which everyone would use to obtain an instance of your class implementing that interface. This will probably save you from having to revisit this article in the future :)

Click here to download a zip archive containing the code shown in this article.

About these ads
Posted in: .NET