views:

41

answers:

3

Since the datastructure of my application domain is becoming pretty complex as of late, I started reading up on mock objects. Soon a simple question came to my mind, but the answer has proven to be quite the headache so far. So here goes:

We have a class 'Foo' with 'bar' as one of its methods:

class Foo {
    public String bar(int i){
        if(i == 1) return "arrr!";
    }
}

And we have a class Pirate calling Foo.bar(1); in one of its methods:

class Pirate {
    public String yell(){
        Foo foo = new Foo();
        return foo.bar(1);
    }

Now we mock the Foo class in the unit test of the Pirate class because Foo happens to have a plethora of other dependencies:

@Test
public void returnsPirateString() {
    Pirate blackBeard = new Pirate();
    Foo fooMock = mock(Foo.class);
    fooMock.expectAndReturn("bar",1,"arrr!"); //expects 'bar' function to be called once and returns "arrr!"
    assertEquals(blackBeard.yell(),"arrr!");
}

What happens now, is that if we refactor method bar to return null instead of "arrr!", our test will keep running happily while our program does not work the way we want it to. This may result in a possible debugging nightmare.

Using a mockist approach instead of the classical testing approach to unit testing, most of the time all "helper" objects get mocked, and only the tested object remains unmocked, so the previously stated problem can occur quite often as well.

What can be done to prevent this problem while mocking?

A: 

You should be testing the 'helper object' in isolation as well. Once both of those are covered and tested, then you can be sure that both interact with each other in the expected way.

Changing your 'helper object' is something that should be done with tests against that helper object to confirm it still behaves as expected.

If you are concerned about the specific runtime behavior of the combination of helper and primary class, then you should use integration tests, or some other test at a higher level, to assert the two work together as expected.

Pete
Yes, but that kinda defeats the use of mocking, since you'll have to create your datastructure anyway for a different test. In that case I could just as well create an Object mother and use this mother in both tests instead of mocking.
Infern0o
I'm not sure I'm following... The point of mocking is to test your different classes in isolation, so you can focus on testing the specific class under test. When testing the helper, you should be testing that helper in complete isolation from any other real class.
Pete
Yes and by doing that your test wouldn't point out any problems while in reality the class fails.
Infern0o
@Infern0o: the idea here is that `Pirate` has not failed: `Foo` has.
Jeff Sternal
The 'problem' you are referring to is a problem in the *interaction* between the two concrete classes. The test for the class that consumes the helper should be "assert that class returns the result from the helper" *whatever* that value is. If you need the 'toplevel' class to specifically return a value, that is something you should target in an integration test.
Pete
+2  A: 

In your test, you are testing the yell() method of Pirate class which uses Foo. So you have to mock the behavior of Foo's bar method. To make sure your bar method is functioning correctly, you need another test case to test the bar method of Foo.

@Test
public void testBar() {
   //make sure bar retrun "arrr"!
}

Now if your bar method returns null, this test case will fail!

Sasi
Yes, but what if bar is now supposed to return null? Foo's test would fail, but all other unit-tests with the mocked Foo class won't complain, while those tests might fail as well when using the real object instead of the mocked one.
Infern0o
Your current test case for the yell method tests yell() when bar behaves in a certain manner. To guard against different behaviors of bar, you will need more test cases for yell() with each test case testing yell for each behavior of bar. So you would have test cases like testYellWhenBarReturnsNormally(), testYellWhenBarThrowsException(), testYellWhenBarReturnsNull(). I know this sounds like a lot of work, but you would need to exercise some judgement to determine what test cases you need!
Sasi
But then you're basicly black box testing instead of white box testing...
Infern0o
Well..I would say you are 'unit' testing the method yell().
Sasi
A: 

The test returnsPirateString is not a false positive - it's testing what happens when a Pirate's Foo instance returns 'arrr!'

In other words, when you're testing Pirate.yell, it doesn't matter what Foo.bar returns, unless it creates a special boundary condition (and you should probably already have a test that documents what yell does when Foo returns null).

Pirate.yell is not responsible for guaranteeing any particular return value for Foo.bar, so its unit tests should not expect any particular return values.You should even make a point of changing your test to use something other than the current return value of Foo.bar.

Jeff Sternal
So what you're saying is when I'm using mocked objects I should be testing for every possible special value / boundary case? Also note that Bar originally was designed to return 'arrr!' and suddenly has been refactored to return null instead. You'd have to trace down all unit-tests where this function has been mocked according to the original definition, and change them to the refactored definition...
Infern0o
No, you just have to write enough unit tests to cover the interesting cases. `Pirate` is not in the business of guaranteeing `Foo.bar`'s return values (unless you make that part of its responsibilities by adding assertions to the actual code). I wouldn't write **any** unit tests for trivial facade methods like the one in the example.
Jeff Sternal
Ofcourse not, I just wanted to provide a simple to the point code example. :D And indeed, it is not in the Pirate's business of guaranteeing Foo.bar's return values, but in practice Pirate.yell() will fail because of Foo.bar's return value.
Infern0o
What I'm saying is that, given the structure of that example, `Pirate.yell` *has not failed* in that case -- `Foo.bar` failed, and you should have another unit test to document that.
Jeff Sternal
Yeah but this is what would happen in your test suite: Since returning null has become expected behaviour for Foo.bar the test for bar will be successful when bar returns null. The test for Pirate.yell would be successful as well since Foo.bar has been mocked. So your test suite would from that point on be successful, while your application would fail.
Infern0o
Returning `null` does **not** become the expected behavior for `Foo.bar`: the unit test just documents that we know what `yell` will do *if that happens*.
Jeff Sternal