Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Mockito "when" work on a non-mock object?

I recently saw some Mockito 1.9.5 code that worked like this:

MyObject myObject = new MyObject();
...
Mockito.when(myObject.someMethod()).thenReturn("bogus");

Since myObject is not a mock object, but is an instance of a non-mocked class, I was surprised this compiled and ran without failing the unit test. I expected I would get a failure saying something like "You asked me to set up an expectation on a non-mock object, and I expected to set expectations only on mock objects."

Why doesn't this code cause a test failure?


Update: adding more code that is necessary to actually replicate the behavior I find confusing. These examples fully illustrate my question. The following code behaves as I expected--when I run this test the test fails with a message that

when() requires an argument which has to be 'a method call on a mock'.

public class AnotherObject{
    public String doSomething(){
        return "did something";
    };
}

public class MyObject{
    private AnotherObject anotherObject = new AnotherObject();

    public void setAnotherObject(AnotherObject anotherObject) {
        this.anotherObject = anotherObject;
    }

    public String someMethod(){
        return anotherObject.doSomething();
    }
}

@Test
public void WhyDoesWhenWorkOnNonMock() throws Exception {
    MyObject myObject = new MyObject();
    Mockito.when(myObject.someMethod()).thenReturn("bogus");
}

Now if I add a couple specific lines to this contrived test, the test no longer fails even though I expected the same failure and same message as before:

public class AnotherObject{
    public String doSomething(){
        return "did something";
    };
}

public class MyObject{
    private AnotherObject anotherObject = new AnotherObject();

    public void setAnotherObject(AnotherObject anotherObject) {
        this.anotherObject = anotherObject;
    }

    public String someMethod(){
        return anotherObject.doSomething();
    }
}

@Test
public void WhyDoesWhenWorkOnNonMock() throws Exception {
    MyObject myObject = new MyObject();
    AnotherObject mockAnotherObject = Mockito.mock(AnotherObject.class);
    myObject.setAnotherObject(mockAnotherObject);
    Mockito.when(myObject.someMethod()).thenReturn("bogus");
}
like image 353
Shawn Avatar asked Sep 15 '15 22:09

Shawn


1 Answers

By incredible and fragile coincidence, probably, unless myObject was actually set to be a spy.

Mockito allows for the creation of a "spy" of a real object:

MyObject myObject = spy(new MyObject());
Mockito.when(myObject.someMethod()).thenReturn("something");

// myObject is actually a duplicate of myObject, where all the fields are copied
// and the methods overridden. By default, Mockito silently records interactions.

myObject.foo(1);
verify(myObject).foo(anyInt());

// You can stub in a similar way, though doReturn is preferred over thenReturn
// to avoid calling the actual method in question.

doReturn(42).when(myObject).bar();
assertEquals(42, myObject.bar());

Barring that, this code is probably not working the way it looks like it should. when's parameter is meaningless, and is sugar meant to hide that the mocked interaction is the most recent method call to a mock. For example:

SomeObject thisIsAMock = mock(SomeObject.class);
OtherObject notAMock = new OtherObject();

thisIsAMock.methodOne();
Mockito.when(notAMock.someOtherMethod()).thenReturn("bar");
// Because notAMock isn't a mock, Mockito can't see it, so the stubbed interaction
// is the call to methodOne above. Now methodOne will try to return "bar",
// even if it isn't supposed to return a String at all!

Mismatches like this can be easy sources of ClassCastException, InvalidUseOfMatchersException, and other bizarre errors. It's also possible that your real MyObject class takes a mock as a parameter, and that last interaction is with a mock that someMethod interacts with.


Your edit confirms my suspicions. As far as Java is concerned, it needs to evaluate the parameter to when before it can invoke when, so your test calls someMethod (real). Mockito can't see that—it can only take action when you interact with one of its mocks—so in your first example it sees zero interactions with mocks and thus it fails. In your second example your someMethod calls doSomething, which Mockito can see, so it returns the default value (null) and marks that as the most recent method call. Then the call to when(null) happens, Mockito ignores the parameter (null) and refers to the most recently called method (doSomething), and stubs that to return "bogus" from that point on.

You can see that by adding this assertion to your test, even though you never stubbed it explicitly:

assertEquals("bogus", mockAnotherObject.doSomething());

For an additional reference, I wrote a separate SO answer on Mockito matchers, for which the implementation details might be useful. See steps 5 and 6 for an expanded view of a similar problem.

like image 162
Jeff Bowman Avatar answered Nov 08 '22 04:11

Jeff Bowman