Alica's dev blog
Refactoring out hidden test dependency

Writing tests in a legacy codebase can sometimes be more difficult that writing the actual code that is to be tested.

Depending on the type of the test (and the already existing test setup), you may get lost in the dependency hell when trying to provide everything that is needed for your particular class or method.

Recently, I got into situation when there was a merge conflict on a line that created a singleton object for the whole test suite. First, I tried to resolve it just by changing the line with the conflict, but neither option worked:

  • I kept my change, but it broke a test that was recently added to the other branch.
  • I took incoming change, but it broke a test that I added in my branch.

I ended up refactoring the “hidden” dependency out to the constructor so that the singleton object was no longer needed in my test.

Before the refactoring

There is a Factory that creates and returns singleton objects. This factory is a global object.

public static class Factory : IFactory
{
    public static IObjectType GiveMeSingletonObject() { ... }

    public static IAnotherObjectType GiveMeAnotherSingletonObject() { ... }
}

The object that we have issues with implements interface IObjectType which has a method MyMethod.

public interface IObjectType
{
    int MyMethod(int myParameter);
}

All tests in the test suite have access to MockFactory instead of Factory (they both implement the same interface IFactory). Every method of the factory creates and returns a mocked singleton object.

The CreateMock<T> method uses the mocking framework inside (Rhino Mocks in our case).

Here comes the critical part:

  • if you give an interface as T, it won’t provide any default implementation for the methods – you can mock it where needed
  • if you give a class (with non-virtual methods) as T, it will provide the actual implementation from the class and won’t let you mock it

Therefore, with a singleton object, you can either have the actual implementation or a mocked implementaion across the whole test suite – but always just one of them.

public static class MockFactory : IFactory
{
    public static IObjectType GiveMeSingletonObject()
    { 
        // this is the dilemma:
        // return CreateMock<IObjectType>();
        // return CreateMock<ObjectType>();
    }

    public static IAnotherObjectType GiveMeAnotherSingletonObject() { ... }
}

Here, we have a test where we want to have the mocked/stubbed implementation of MyMethod. The method is called inside of DoSomething and we just want it to return a constant value.

[Test]
public void TestForFirstClass 
{
    var singletonObject = MockFactory.GiveMeSingletonObject();
    singletonObject.Stub(obj => obj.MyMethod(Arg<int>.Is.Anything)).Return(42);

    var object1 = new FirstClassToBeTested(MockFactory.GiveMeAnotherSingletonObject());
    object1.DoSomething(); // inside, it calls IFactory.GiveMeSingletonObject().MyMethod(param) and it returns stubbed value 42
    ...
}

But we also have this test, where DoSomethingElse method needs to call the actual implementation of MyMethod:

[Test]
public void TestForSecondClass 
{
    var object2 = new SecondClassToBeTested();
    object2.DoSomethingElse(123); // inside, it calls IFactory.GiveMeSingletonObject().MyMethod(param) and it returns the actual result
    ...
}

So what do we do now? Apparently, we need both working at the same time, but we can’t have both working at the same time. We must look for the solution elsewhere…

First, let’s make one thing clear: To have a global object accessible and used all around the codebase is not good. Most of the times, it’s an anti-pattern. It’s ugly, it makes tracing the dependencies difficult and it makes testing a pain (as we see in our case).

Now, let’s have a look at the FirstClassToBeTested (the class that needs the mocked implementation of MyMethod). More specifically, let’s have a look at its constructor:

public class FirstClassToBeTested
{
    IObjectType _myObject;
    IAnotherObjectType _anotherObject;

    public FirstClassToBeTested(IAnotherObjectType anotherObject) {
        _myObject = Factory.GiveMeSingletonObject();
        _anotherObject = anotherObject;
    }
}

The answer is already there: the class depends on IAnotherObjectType object as well, but doesn’t take it from the global Factory – it takes it from the constructor and leaves it to the caller of the constructor to provide that object. We will do the same with IObjectType.

Note: I didn’t make up this constructor just so that this post can have a happy ending. This is what I found when I was solving the real problem.

After the refactoring

As we said, we let the creator of FirstClassToBeTested to provide value for _myObject.

public class FirstClassToBeTested
{
    IObjectType _myObject;
    IAnotherObjectType _anotherObject;

    public FirstClassToBeTested(IObjectType myObject, IAnotherObjectType anotherObject) {
        _myObject = myObject;
        _anotherObject = anotherObject;
    }
}

With that, we can create our own mock object in the test (that doesn’t depend on what MockFactory returns) and inject this object into FirstClassToBeTested via constructor.

[Test]
public void TestForFirstClass 
{
    var mockObject = CreateMock<IObjectType>();
    mockObject.Stub(obj => obj.MyMethod(Arg<int>.Is.Anything)).Return(42);

    var object1 = new FirstClassToBeTested(mockObject, MockFactory.GiveMeAnotherSingletonObject());
    object1.DoSomething(); // inside, we call mockObject.MyMethod() and it returns the stubbed value 42
    ...
}

And we call CreateMock<ObjectType> (not CreateMock<IObjectType>) in MockFactory. Therefore TestForSecondClass will work correctly as well.

public static class MockFactory : IFactory
{
    public static IObjectType GiveMeSingletonObject()
    { 
        return CreateMock<ObjectType>(); // no dilemma anymore
    }

    public static IAnotherObjectType GiveMeAnotherSingletonObject() { ... }
}

Conclusion

Hidden dependencies make testing very difficult. If possible, avoid introducing them. If the dependencies already exist, try to refactor them out to the constructor. They may still get hidden by whoever is calling the constructor, but at least you make the problem one class smaller.


Last modified on 2024-04-10