Spies created with @SpyBean are not added to the MockitoBeans which are then reset at the end of test execution like Mocks are, rendering them useless for invocation counts.

Compare from org.springframework.boot.test.mock.mockito.MockitoPostProcessor:

private void registerMock(ConfigurableListableBeanFactory beanFactory,
        BeanDefinitionRegistry registry, MockDefinition definition, Field field) {
    RootBeanDefinition beanDefinition = createBeanDefinition(definition);
    String beanName = getBeanName(beanFactory, registry, definition, beanDefinition);
    beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(1,
            beanName);
    if (registry.containsBeanDefinition(beanName)) {
        registry.removeBeanDefinition(beanName);
    }
    registry.registerBeanDefinition(beanName, beanDefinition);
    Object mock = createMock(definition, beanName);
    beanFactory.registerSingleton(beanName, mock);
    this.mockitoBeans.add(mock);
    this.beanNameRegistry.put(definition, beanName);
    if (field != null) {
        this.fieldRegistry.put(field, new RegisteredField(definition, beanName));
    }
}

and

private void registerSpy(SpyDefinition definition, Field field, String beanName) {
    this.spies.put(beanName, definition);
    this.beanNameRegistry.put(definition, beanName);
    if (field != null) {
        this.fieldRegistry.put(field, new RegisteredField(definition, beanName));
    }
}

You can see for @MockBean that we register the mocks (this.mockitoBeans.add(mock);) but for @SpyBean the mock is never registered, and hence is not reset when the beans contained in the MockitoBeans instance are.

Comment From: wilkinsona

I can't reproduce the problem described here. These tests pass:

@RunWith(SpringRunner.class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class SpyBeanResetTests {

    @SpyBean
    private SomeService someService;

    @Test
    public void test001() {
        this.someService.doSomething();
    }

    @Test
    public void test002() {
        verify(this.someService, times(0)).doSomething();
    }

    @Configuration
    static class Config {

        @Bean
        SomeService someService() {
            return new SomeService();
        }

    }

    static class SomeService {

        void doSomething() {

        }

    }

}

They pass because the spied bean is part of the application context and ResetMocksTestExecutionListener finds it in the application context and resets it.

@mjgp2 Do you have an example that reproduces the behaviour you've described?

The support for @MockBean and @SpyBean has evolved in 1.4.x, and it looks to me like MockitoBeans may actually now be redundant but I'd like to be a bit more certain before removing it.

Comment From: mjgp2

Looks like this was fixed in 1.4.2 👍

I can remove the workarounds from my tests and they now pass. Hoorah.

Comment From: attacco

Well, Spring, your annotations @SpyBean and @MockBean completely doesn't work in your own example if you will extend SpyBeanResetTests from AbstractJUnit4SpringContextTests.

So, this code will fail in spring-boot 1.5.4.

@RunWith(SpringRunner.class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class SpyBeanResetTests extends AbstractJUnit4SpringContextTests {

    @SpyBean
    private SomeService someService;

    @Test
    public void test001() {
        this.someService.doSomething();
    }

    @Test
    public void test002() {
        verify(this.someService, times(0)).doSomething();
    }

    @Configuration
    static class Config {

        @Bean
        SomeService someService() {
            return new SomeService();
        }

    }

    static class SomeService {

        void doSomething() {

        }

    }

}
test001: 
java.lang.NullPointerException
    at ... SpyBeanResetTests.test001

test002: 
org.mockito.exceptions.misusing.NullInsteadOfMockException: 
Argument passed to verify() should be a mock but is null!

Comment From: wilkinsona

@attacco That's to be expected as extending AbstractJUnit4SpringContextTests changes the default test execution listeners (this is described in its javadoc). @MockBean and @SpyBean rely on MockitoTestExecutionListener to inject the mocks and spies.

Comment From: attacco

Ok, that is a code snippet:

@RunWith(SpringRunner.class)
@TestExecutionListeners({ ServletTestExecutionListener.class, DirtiesContextBeforeModesTestExecutionListener.class,
    DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class })
public abstract class AbstractJUnit4SpringContextTests implements ApplicationContextAware { ... }

As far, as i understand @TestExecutionListeners's java-doc, this declaration means, that AbstractJUnit4SpringContextTests replaces default listeners and it's happens due to the two reasons:

  1. AbstractJUnit4SpringContextTests doesn't extends any superclass (i.e., there no superclass with own @TestExecutionListeners annotation;
  2. Property mergeMode not specified, and thus, it fallbacks to default MergeMode.REPLACE_DEFAULTS. This property taken into account, when @TestExecutionListeners is declared on a class that does not inherit listeners from a superclass. And it's our case, due to the [1].

In turn, MergeMode.REPLACE_DEFAULTS means, that all default listeners will be replaces.

Logically, all is correct. But, it looks a little bit confusing, because convenient 'all-in-one' class AbstractJUnit4SpringContextTests provided by Spring, destroys it's default behaviour and moreover makes impossible usage of spring-provided annotations @SpyBean and @MockBean.

It wasn't obvious, and i spent about 5 hours, while fighting with this strange behaviour.

Comment From: nightswimmings

With

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional (might be this?)

@SpyBean(reset = MockReset.AFTER) is not resetting the bean after each test.

Tested in 1.5.14.RELEASE

Comment From: wilkinsona

@nightswimmings If you believe you've found some unexpected behaviour, please open a new issue with a small sample (something we can unzip or git clone) that shows the mocks not being reset.

Comment From: spyro2000

My @SpyBean instances are also not reset after a defined some mock behavior on them. Even @SpyBean(reset = MockReset.AFTER)did not work.

Comment From: wilkinsona

@spyro2000 As I said above, if you believe you've found some unexpected behaviour, please open a new issue with a small sample (something we can unzip or git clone) that shows the mocks not being reset.