Strange error when verifying against a mock that's wrapped with a Spring proxy

1,959 views
Skip to first unread message

Eric Rizzo

unread,
Nov 20, 2015, 12:02:31 PM11/20/15
to mockito
I was looking for a technique to unit test an object that is annotated with Spring's caching annotations. I found a (seemingly) nice example here. It uses the following code:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class CachingIntegrationTest {

  // Your repository interface
  interface MyRepo extends Repository<Object, Long> {

    @Cacheable("sample")
    Object findByEmail(String email);
  }

  @Configuration
  @EnableCaching
  static class Config {

    // Simulating your caching configuration
    @Bean
    CacheManager cacheManager() {
      return new ConcurrentMapCacheManager("sample");
    }

    // A repository mock instead of the real proxy
    @Bean
    MyRepo myRepo() {
      return Mockito.mock(MyRepo.class);
    }
  }

  @Autowired CacheManager manager;
  @Autowired MyRepo repo;

  @Test
  public void methodInvocationShouldBeCached() {

    Object first = new Object();
    Object second = new Object();

    // Set up the mock to return *different* objects for the first and second call
    Mockito.when(repo.findByEmail(Mockito.any(String.class))).thenReturn(first, second);

    // First invocation returns object returned by the method
    Object result = repo.findByEmail("foo");
    assertThat(result, is(first));

    // Second invocation should return cached value, *not* second (as set up above)
    result = repo.findByEmail("foo");
    assertThat(result, is(first));

    // Verify repository method was invoked once
    Mockito.verify(repo, Mockito.times(1)).findByEmail("foo");
    assertThat(manager.getCache("sample").get("foo"), is(notNullValue()));

    // Third invocation with different key is triggers the second invocation of the repo method
    result = repo.findByEmail("bar");
    assertThat(result, is(second));
  }
}


That test passes, but it's actually hiding a problem. I first noticed that the call to verify() passes no matter what value you pass to times(). So I added a call to validateMockitoUsage() right after the verify() call, and sure enough I get an UnfinishedVerificationException
I tried stepping into the Mockito code but I quickly got lost. It appears to be some interaction between Mockito and the Spring-generated proxy around the mock repo.

Any hints as to what's going on and how to correctly test that the mock repo is only called 1 time?

Thanks,
Eric

Eric Rizzo

unread,
Nov 23, 2015, 6:05:03 PM11/23/15
to mockito
It looks like what is going on is that Mockito.when() and verify() are being called on the Spring proxy that wraps the actual mock (the proxy is what's @Autowired into the repo variable, not the mock itself). To fix this, I had to create the mock outside the Spring context Configuration and hold a reference to it, so I can then call when() and verify() on that instead of repo (which is a proxy). 

It's a bit ugly in a test, though; the mock has to be a created before any @Before (because Spring's JUnit Runner injects @Autowired dependencies early), and I have to make sure to call Mockito.reset() in a @Before method. Yuck.

If anyone has better ideas about this I'd love to hear them.

Eric

Eric Lefevre-Ardant

unread,
Nov 26, 2015, 9:30:49 PM11/26/15
to moc...@googlegroups.com
I've never tried getting Spring to return a mock, but I did try to spy on a Spring-managed bean (mostly to give custom behaviour and stop calling external systems). I do not remember exactly what the symptoms were, but they might have been the same as what you got. Definitely related to the proxy created by Spring.

We got around this with this utility method:
    public static <T> T unwrapProxy(T bean) throws Exception {
        if (org.springframework.aop.support.AopUtils.isAopProxy(bean) && bean instanceof org.springframework.aop.framework.Advised) {
            org.springframework.aop.framework.Advised advised = (org.springframework.aop.framework.Advised) bean;
            bean = (T) advised.getTargetSource().getTarget();
        }
        return bean;
    }

We would use it in this way:
    @Autowired
    private UserService userService;
    @Autowired
    private HigherLevelService higherLevelService;
    @Test
    public void test() {
        this.userService = spy(unwrapProxy(userService));
        this.higherLevelService.setUserService(userService);
        higherLevelService.doSomething();
    }

In the end, we decided that this was much too ugly, so we separated all calls to external systems in a few well-defined classes that get their default implementation (that is, the external calls) replaced when testing.

For example, in the case above, UserService is actually using an LDAP server. It now points to a LdapServerDelegator (managed by Spring), which is using a LdapServerDefault which implements the LdapServer interface. However, the LdapServerDelegator comes with a setter method, which lets us pass a mock(LdapServer.class) when testing.
The part that is still ugly is that we still need to actually call the setter in many of our tests. We mitigated that by having a parent class that is inherited by all tests that use Spring; this class sets a mockLdapServer that we can use in tests to give specific behaviour.

HTH

Eric


--
You received this message because you are subscribed to the Google Groups "mockito" group.
To unsubscribe from this group and stop receiving emails from it, send an email to mockito+u...@googlegroups.com.
To post to this group, send email to moc...@googlegroups.com.
Visit this group at http://groups.google.com/group/mockito.
To view this discussion on the web visit https://groups.google.com/d/msgid/mockito/c687e62b-2a7e-468c-a05c-6fe5089e9d4a%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Reply all
Reply to author
Forward
0 new messages