Skip to content

Testing with spies

Mockito spies allows dynamic modification of real objects behavior (configured same as mocks, but, by default, all methods work as in raw bean).

Spies declared with a @SpyBean annotation.

Important

Spy creation requires real bean instance and so guicey use AOP to intercept real bean access and redirecting all calls through a dynamically created (on first access) spy object. This means that spies would only work with guice-managed beans.

If you need to spy for a manual instance - use partial mocks

Warning

Spies will not work for HK2 beans

Mockito documentation is written in the Mockito class javadoc.
Additional docs could be found in mockito wiki
Also, see official mockito refcard and baeldung guides.

Setup

Requires mockito dependency (version may be omitted if dropwizard BOM used):

testImplementation 'org.mockito:mockito-core'

Usage

Suppose we have a service:

public static class Service {

    public String get(int id) {
        return "Hello " + id;
    }
}

Spying it:

@TestGuiceyApp(App.class)
public class Test {

    @SpyBean
    Service spy;

    // NOT the same instance as spy (but calls on both objects are equivalent)
    @Inject
    Service service;

    @BeforeEach
    public void setUp() {
        // IMPORTANT: spies configured in reverse order to avoid accidental method call            
        doReturn("bar1").when(spy).get(11);
    }

    @Test
    public void test() {
        // stubbed result
        Assertions.assertEquals("bar1", s1.get(11));
        // real method result (because argument is different)
        Assertions.assertEquals("Hello 10", s1.get(10)); 
    }
}

Here doReturn refer to Mockito.doReturn used with static import.

Note

As real guice bean used under the hood, all AOP, applied to the original bean, will work.

Tip

Calling guice proxy service.get(11) and spy object directly spy.get(11) is equivalent (because guice returns AOP proxy which redirects call to the spy)

See other examples in mocks section.

Asserting calls

Tip

If you want to use spies to track bean access (verify arguments and response) then try trackers which are better match for this case.

As mocks, spies could be used to assert calls:

// method Service.get(11) called on mock just once
verify(spy, times(1)).get(11);

These assertions would fail if method was called more times or using different arguments.

Method result capture

Verifying method return value with spies is a bit clumsy:

public static class ResultCaptor<T> implements Answer {
    private T result = null;
    public T getResult() {
        return result;
    }

    @Override
    public T answer(InvocationOnMock invocationOnMock) throws Throwable {
        result = (T) invocationOnMock.callRealMethod();
        return result;
    }
}

@TestGuiceyApp(App.class)
public class Test {
    ResultCaptor<String> resultCaptor = new ResultCaptor<>();
    // capture actual argument value (just to show how to do it)
    ArgumentCaptor<Integer> argumentCaptor = ArgumentCaptor.forClass(Integer.class);

    @SpyBean 
    Service spy;

    @BeforeAll
    public void setUp() {
        doAnswer(resultCaptor).when(spy).get(argumentCaptor.capture());
    }

    public void test() {
        // call method
        Assertions.assertThat(spy.get(11)).isEqualTo("bar");
        // result captured
        Assertions.assertThat(resultCaptor.getResult()).isEqualTo("bar");
        Assertions.assertThat(argumentCaptor.getValue()).isEqualTo(11);

        verify(spy, Mockito.times(1)).get(11);
    }
}

Why would you need that? It is often useful when verifying indirect bean call. For example, if we have SuperService which internally calls Service and so there is no other way to verify service call result correctness other than spying it (or use tracker).

Pre initialization

As spy object creation is delayed until application startup, it is impossible to configure spy before application startup (as with mocks). Usually it is not a problem, if target bean is not called during startup.

If you need to modify behavior of spy, used during application startup (e.g. by some Managed), then there is a delayed initialization mechanism:

// extra class required to overcome annotation limitation
public class Initializer implements Consumer<Service> {
    // real spy could be created ONLY after injector startup
    @Override
    public void accept(Service spy) {
        doReturn("spied").when(service).get(11); 
    }

}

@TestGuiceyApp(App.class)
public class Test {

    @SpyBean(initializers = Initializer.class)
    Service spy;

    ...
}

Here, Initializer would be called just after spy creation (on first access).

And so any Managed, calling it during startup would use completely configured spy:

@Singleton
public static class Mng implements Managed {
    @Inject
    Service service;

    @Override
    public void start() throws Exception {
        // "spied" result
        service1.get(11);
    }
}

Spies reset

Spies are re-set automatically after each test method (and that's why it makes sense to declare mock behavior in test setup method - execured before each test method).

Note

Spy could be reset manually at any time with Mockito.reset(spy)

Spies automatic reset could be disabled with autoReset option:

@SpyBean(autoReset = false)
Service spy;

Spies report

Same as for mocks, a usage report could be printed after each test @SpyBean(printSummary = true)

\\\------------------------------------------------------------/ test instance = 285bf5ac /
@SpyBean stats on [After each] for SpySummaryTest$Test1#test():

    [Mockito] Interactions of: ru.vyarus.dropwizard.guice.test.jupiter.setup.spy.SpySummaryTest$Service$$EnhancerByGuice$$60e90c@40fe8fd5
     1. spySummaryTest$Service$$EnhancerByGuice$$60e90c.foo(
        1
    );
      -> at ru.vyarus.dropwizard.guice.test.jupiter.setup.spy.SpySummaryTest$Test1.test(SpySummaryTest.java:50)

Debug

When extension debug is active:

@TestGucieyApp(value = App.class, debug = true)
public class Test 

All recognized spy fields would be logged:

Applied spies (@SpyBean) on SpySimpleTest:

    #spy2                          Service2                     (r.v.d.g.t.j.s.s.SpySimpleTest) 
    #spy1                          Service1                     (r.v.d.g.t.j.s.s.SpySimpleTest)