8

I have been unable to find a way to use "Deep Stubs" for stubbing methods on a spy in Mockito. What I'm looking to do is something like this:

@Spy private Person person = //retrieve person

    @Test
    public void testStubbed() {
        doReturn("Neil").when(person).getName().getFirstName();
        assertEquals("Neil", person.getName().getFirstName());
    }

The code above compiles with no issues, but upon running the test, it fails saying that the return type (the Name class, in this case) cannot be returned by getName().

Normally, when mocking, you have to use
@Mock(answer = Answers.RETURNS_DEEP_STUBS) for each mocked object. However, spy does not seem to have anything like this.

Has anyone ever successfully done deep stubbed mocking using a spy?

The error I'm receiving is listed below:

String cannot be returned by getName()
getName() should return Name

Due to the nature of the syntax above problem might occur because of:
1. Multithreaded testing 
//I'm not doing multithreaded testing
2. A spy is stubbed using when(spy.foo()).then() syntax. It is safer to stub spies with doReturn|Throw() family of methods 
//As shown above, I'm already using the doReturn family of methods. 
Jared Burgett
  • 141
  • 1
  • 8

4 Answers4

5

While I'd still like to know if there's a better way to do this, I'd like to post a solution for anyone who comes looking.

The solution below works fine, by requiring you to create a new mock (or even a real object/spy) for each level of your dependencies. In other words, instead of chaining your method calls to create your stub, you mock each level individually.

@Spy private Person person = //retrieve person
@Mock private Name name;

@Test
public void testStubbed() {
    doReturn(name).when(person).getName();
    doReturn("Neil").when(name).getName();
    assertEquals("Neil", person.getName().getFirstName());
}
Jared Burgett
  • 141
  • 1
  • 8
1

You can get a little closer to the deep stubs you want by using doAnswer(RETURNS_DEEP_STUBS), but you can't override arbitrarily-deep method calls without taking care to stub their parent calls. I'd stick to manual single-level-deep mocks as you do in your answer, or use even less mocking if possible.


A spy's default behavior is to delegate to its real method call, which will typically return a real object (like your Name) and not a Mockito spy. This means that you won't normally be able to change those objects' behavior using Mockito: a spy isn't really the same class as the object being spied on, but rather is a generated subclass where every field value is copied from the spied-on value. (The copying is an important feature, because a delegating spy would have very unintutitive behavior regarding this, including for method calls and field values.)

Foo foo = new Foo();
foo.intValue = 42;
foo.someObject= new SomeObject();

Foo fooSpy = Mockito.spy(foo);
// Now fooSpy.intValue is 42, fooSpy.someObject refers to the exact same
// SomeObject instance, and all of fooSpy's non-final methods are overridden to
// delegate to Mockito's behavior. Importantly, SomeObject is not a spy, and
// Mockito cannot override its behavior!

So this won't work:

doReturn("Neil").when(person).getName().getFirstName();
//   Mockito thinks this call ^^^^^^^^^ should return "Neil".

And neither will this:

doReturn("Neil").when(person.getName()).getFirstName();
//    The object here ^^^^^^^^^^^^^^^^ won't be a mock, and even if Mockito
//    could automatically make it a mock, it's not clear whether that
//    should be the same spy instance every time or a new one every time.

In your situation, I'd choose the following, in order from most preferable to least:

  1. Create a real Name object and install it using doReturn. It looks like Name is a data object (aka value object) after all, which likely means it has no dependencies, solid behavior, and difficult-to-mock state transitions. You may not be gaining anything by mocking it.

  2. Create a mock Name and install it as you do in your answer. This is particularly useful if Name is more complicated than it looks to be, or if it doesn't actually exist in the first place.

  3. Replace getName to return a deep stub...

    doAnswer(RETURNS_DEEP_STUBS).when(person).getName();
    

    ...which you can then override...

    doReturn("Neil").when(person.getName()).getFirstName();
    

    ...even for arbitrarily deep values.

    doReturn("Gaelic").when(person.getName()
                                  .getEtymology()
                                  .getFirstNameEtymology())
        .getOrigin();
    

As a final editorial, one of the hazards of partial mocks is that it makes it really hard to tell which behavior is real and which is faked; this might make it hard for you to guarantee that the behavior you're testing is prod behavior and not mock behavior. Another hazard of deep stubbing is that you may be violating the Law of Demeter by definition. If you find yourself using this kind of technique often in tests, it may be time to consider rearchitecting your system under test.

Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
0

TLDR: Just create a Mock with Answers.RETURNS_DEEP_STUBS for the first called object from the spy and stub the invocation(s) you need.

@Spy 
private Person person = new Person();

@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Name name;

@Test
public void testStubbed() {
    doReturn(name).when(person).getName();
    when(name.getX().getY().getZ().getFirstName()).thenReturn("Neil");
    assertEquals("Neil", name.getX().getY().getZ().getFirstName());   
}
Sergio
  • 3,317
  • 5
  • 32
  • 51
-1
@Test
public void listTypeTest() throws Exception {
    doCallRealMethod().when(listType).setRecordCount(new BigInteger("556756756756"));
    listType.setRecordCount(new BigInteger("556756756756"));
    doCallRealMethod().when(listType).getRecordCount();
    assertEquals(new BigInteger("556756756756"), listType.getRecordCount());
}
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Sreddy
  • 1
  • Can you elaborate a bit on this answer? It doesn't look like there is anything related to the original question. – stdunbar Oct 06 '20 at 14:24