Ole Rasmussen
2012-03-15 17:09:51 UTC
I just finished the GOOS book and while I find the mock-focused
practice attractive for many reasons, there is one special scenario (issue
really) I cannot get out of my head.
When we unit test our objects we want to mock or stub the collaborators.
Mocking/stubbing may seem innocent but if we take a more detailed look at
what's actually happening it seems we might be creating grounds for a very
annoying problem in the future.
Assume we're writing a test for an object B that has interface A as
collaborator. Mocking A requires looking at the interface specification and
"simulating" part of that in the mock behavior. The important part is that
whatever behavior we are simulating it is correct according to the
interface. At the time we are writing the test this is obviously the case
because we are looking directly at the interface and "copying" its
protocols (hopefully).
Now imagine we finished writing the test for object B that mocks interface
A. At some point later in time we figure out we need to change interface A.
We make the change, run out unit tests, and see most tests for classes
implementing interface A fail. This is good and expected. We fix the
classes and run the tests once more. Everything is green.
At this point in time, even though all tests are green there is a
substantial flaw in our code; namely that object B doesn't work. The unit
test for object B is based on the earlier version of interface A that
worked differently than what it does now. The core of the problem is that
the assumptions/simulations we made in the mock for interface A at the time
we wrote the unit test for object B aren't necessarily true anymore. The
surrounding world has changed.
For a concrete example take this Java code:
public class ARuntimeException extends RuntimeException {}
public class BRuntimeException extends RuntimeException {}
public class CRuntimeException extends RuntimeException {}
public interface AInterface {
int func() throws ARuntimeException;
}
public class BClass {
public void doSomething(AInterface arg) throws BRuntimeException {
try {
arg.func();
}
catch (ARuntimeException e) {
throw new BRuntimeException();
}
}
}
public class BTest {
@Test(expected = BRuntimeException.class)
public void
doSomethingThrowsBExceptionWhenCollaboratorThrowsAException() throws
Exception {
AInterface aStub = org.mockito.Mockito.mock(AInterface.class);
when(aStub.func()).thenThrow(new ARuntimeException());
new BClass().doSomething(aStub);
}
}
If we change the interface the tests still run but on a false assumption:
public interface AInterface {
int func() throws CRuntimeException;
}
I hope I have described the problem clearly, but to sum up: when mocking an
interface we make assumptions about it's behavior. When the interface
changes those assumptions are no later true.
I understand that the UNIT tests for object B theoretically shouldn't fail
because even though interface A changes object B still works isolated.
However, I also consider the UNIT tests for object B a kind of integration
test between object B and the mock. The whole reason the unit tests have
any value to us, is that this "integration test" is based on a mock that
actually does what the interface says it should. But when we change the
interface it doesn't, so the unit test theoretically is completely wrong.
The question is if we can do something about this? I would like my tests to
alert me if something like this happens. Is this not possible? What do you
guys do about it? I'm sure it must be a problem for most of the test and
especially TDD practitioners.
practice attractive for many reasons, there is one special scenario (issue
really) I cannot get out of my head.
When we unit test our objects we want to mock or stub the collaborators.
Mocking/stubbing may seem innocent but if we take a more detailed look at
what's actually happening it seems we might be creating grounds for a very
annoying problem in the future.
Assume we're writing a test for an object B that has interface A as
collaborator. Mocking A requires looking at the interface specification and
"simulating" part of that in the mock behavior. The important part is that
whatever behavior we are simulating it is correct according to the
interface. At the time we are writing the test this is obviously the case
because we are looking directly at the interface and "copying" its
protocols (hopefully).
Now imagine we finished writing the test for object B that mocks interface
A. At some point later in time we figure out we need to change interface A.
We make the change, run out unit tests, and see most tests for classes
implementing interface A fail. This is good and expected. We fix the
classes and run the tests once more. Everything is green.
At this point in time, even though all tests are green there is a
substantial flaw in our code; namely that object B doesn't work. The unit
test for object B is based on the earlier version of interface A that
worked differently than what it does now. The core of the problem is that
the assumptions/simulations we made in the mock for interface A at the time
we wrote the unit test for object B aren't necessarily true anymore. The
surrounding world has changed.
For a concrete example take this Java code:
public class ARuntimeException extends RuntimeException {}
public class BRuntimeException extends RuntimeException {}
public class CRuntimeException extends RuntimeException {}
public interface AInterface {
int func() throws ARuntimeException;
}
public class BClass {
public void doSomething(AInterface arg) throws BRuntimeException {
try {
arg.func();
}
catch (ARuntimeException e) {
throw new BRuntimeException();
}
}
}
public class BTest {
@Test(expected = BRuntimeException.class)
public void
doSomethingThrowsBExceptionWhenCollaboratorThrowsAException() throws
Exception {
AInterface aStub = org.mockito.Mockito.mock(AInterface.class);
when(aStub.func()).thenThrow(new ARuntimeException());
new BClass().doSomething(aStub);
}
}
If we change the interface the tests still run but on a false assumption:
public interface AInterface {
int func() throws CRuntimeException;
}
I hope I have described the problem clearly, but to sum up: when mocking an
interface we make assumptions about it's behavior. When the interface
changes those assumptions are no later true.
I understand that the UNIT tests for object B theoretically shouldn't fail
because even though interface A changes object B still works isolated.
However, I also consider the UNIT tests for object B a kind of integration
test between object B and the mock. The whole reason the unit tests have
any value to us, is that this "integration test" is based on a mock that
actually does what the interface says it should. But when we change the
interface it doesn't, so the unit test theoretically is completely wrong.
The question is if we can do something about this? I would like my tests to
alert me if something like this happens. Is this not possible? What do you
guys do about it? I'm sure it must be a problem for most of the test and
especially TDD practitioners.