2

Mock objects introduce a good approach to do deep behavior testing of some program unit. You just should pass mocked dependency to the tested unit and check if it works with dependency as it should do.

Let you have 2 classes A and B:

public class A
{
    private B b;

    public A(B b)
    {
        this.b = b;
    }

    public void DoSomething()
    {
        b.PerformSomeAction();
        if(b.State == some special value)
        {
            b.PerformAnotherAction();
        }
    }
}


public class B
{
    public BState State { get; private set; }

    public void PerformSomeAction()
    {
        //some actions

        State = some special value;
    }

    public void PerformAnotherAction()
    {
        if(State != some special value)
        {
           fail();  //for example throw new InvalidOperationException();
        }
    }
}

Imagine class B is being tested with unit test TestB.

To unit test class A we can either pass B to it's constructor (to do state based testing) or pass B's mock to it (to do behavior based testing).

Let say we have chosen the second approach (for example we can't verify A's state directly and can do it indirectly) and created unit test TestA (which doesn't contain any reference to B).

So we will introduce an interface IDependency and classes will look like:

public interface IDependency
{
    void PerformSomeAction();
    void PerformAnotherAction();
}

public class A
{
    private IDependency d;

    public A(IDependency d)
    {
        this.d = d;
    }

    public void DoSomething()
    {
        d.PerformSomeAction();
        if(d.State == some special value)
        {
            d.PerformAnotherAction();
        }
    }
}


public class B : IDependency
{
    public BState State { get; private set; }

    public void PerformSomeAction()
    {
        //some actions

        State = some special value;
    }

    public void PerformAnotherAction()
    {
        if(State != some special value)
        {
           fail();  //for example throw new InvalidOperationException();
        }
    }
}

and unit test TestB is something similar to:

[TestClass]
public class TestB
{
    [TestMethod]
    public void ShouldPerformAnotherActionWhenDependencyReturnsSomeSpecialValue()
    {
        var d = CreateDependencyMockSuchThatItReturnsSomeSpecialValue();
        var a = CreateA(d.Object);

        a.DoSomething();

        AssertSomeActionWasPerformedForDependency(d);
    }

    [TestMethod]
    public void ShouldNotPerformAnotherActionWhenDependencyReturnsSomeNormalValue()
    {
        var d = CreateDependencyMockSuchThatItReturnsSomeNormalValue();
        var a = CreateA(d.Object);

        a.DoSomething();

        AssertSomeActionWasNotPerformedForDependency(d);
    }
}

Ok. It's a happy moment for developer - everything is tested and all tests are green. Everything is good.

But!

When someone modifies logic of class B (for example modifies if(State != some special value) to if(State != another value) ) only TestB fails.

This guy fixes this test and thinks that everything goes well again.

But if you try to pass B to constructor of A A.DoSomething will fail.

The root cause of it is our mock object. It fixed old behavior of B object. When B changed its behavior mock didn't reflect it.

So, my question is how to make mock of B follow changes of behavior of B?

gerichhome
  • 1,872
  • 2
  • 15
  • 21

2 Answers2

1

This is a question of viewpoint. Normally, you mock an interface, not a concrete class. In your example, B's mock is an implementation of IDependency, as is B. B's mock must change whenever the behaviour of IDependency changes, and you can ensure that by looking at all the implementations of IDependency when you change the defined behaviour of IDependency.

So, the enforcement is through 2 simple rules that ought to be followed in the code base:

  1. When a class implements an interface, it must fulfill all defined behaviour of the interface after modification.
  2. When you change an interface, you must adapt all implementers to fulfill the new interface.

Ideally, you have unit tests in place that test against the defined behaviour of an IDependency, which apply to both B and BMock and catch violations of these rules.

thiton
  • 35,651
  • 4
  • 70
  • 100
  • Your answer is mostly what I think about and what I want to clearify to myself. It seems that you suggest to write a set of tests that verify that Mocks implement interface as excepted. Is it a good practice? Does it introduce some maintainance problems? It looks like we should unit test test utility classes too? Instead of fixing behavior in mock we should fix it in unit test (probably abstract base unit test class that should be overriden for each interface implementation, so-called contract test). Am I right? – gerichhome Feb 14 '13 at 16:17
0

I differ from the other answer, which seems to be advocating subjecting both the real implementation and the (hand-made?) mock to a set of contract tests - which specify the behavior of the role/interface. I've never seen tests that exercise mocks - could be done though.

Normally you don't handcraft mocks - rather you use a mocking framework. So w.r.t. your example, my client tests would have inline statements as

new Mock<IDependency>().Setup(d => d.Method(params)
                       .Returns(expectedValue)

Your question is when the contract changes, how do I guarantee that the inline expectations in the client tests are also updated (or even flagged) with changes to the dependency?

The compiler won't help here. Nor would the tests. What you have is a lack of shared agreement between the client and the dependency. You'd have to manually find-and-replace (or use IDE tooling to locate all references to the interface method) and fix.

The way out is to NOT define a lot of fine-grained IDependency interfaces recklessly. Most problems can be solved with a minimal number of chunky roles (realized as interfaces) with clearly defined non-volatile behavior. You can attempt to minimize role-level changes. Initially this was a sticking point with me too - however discussions with interaction-test experts and practical experience have managed to win me over. If this does happen all too often, a quick retrospective as to the cause of fickle interfaces should yield better results.

Gishu
  • 134,492
  • 47
  • 225
  • 308