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:
- AbstractJUnit4SpringContextTests doesn't extends any superclass (i.e., there no superclass with own
@TestExecutionListeners
annotation; - Property
mergeMode
not specified, and thus, it fallbacks to defaultMergeMode.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.