Closed as not planned
Description
Overview
When an existing bean of the required type is not discovered in the bean factory, Spring Boot's @SpyBean
creates a new instance to spy on by instantiating the required type using its default constructor.
Whereas, @MockitoSpyBean
requires that a bean of the required type exists and throws an IllegalStateException
if an appropriate bean cannot be found.
Consequently, @MockitoSpyBean
cannot be used to spy on a bean that does not exist.
Original Description
@MockitoSpyBean
cannot spy concrete runtime bean type, but Spring Boot's @SpyBean
can.
Example
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.0'
id 'io.spring.dependency-management' version 'latest.release'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
package com.example;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@ContextConfiguration
@SpringJUnitConfig
public class SpyBeanTests {
@MockitoSpyBean
// @SpyBean works
// @Autowired works
SubTestService testService;
@Test
void test() {
assertThat(testService.echo("test")).isEqualTo("test");
}
@Configuration
static class Config {
@Bean
TestService testService() {
return new SubTestService();
}
}
static class SubTestService extends TestService {
}
static class TestService {
String echo(String str) {
return str;
}
}
}
Test fails with:
Caused by: java.lang.IllegalStateException: Unable to select a bean to override by wrapping: found 0 bean instances of type com.example.SpyBeanTests$SubTestService (as required by annotated field 'SpyBeanTests.testService')
at org.springframework.test.context.bean.override.BeanOverrideBeanFactoryPostProcessor.wrapBean(BeanOverrideBeanFactoryPostProcessor.java:242)
at org.springframework.test.context.bean.override.BeanOverrideBeanFactoryPostProcessor.registerBeanOverride(BeanOverrideBeanFactoryPostProcessor.java:113)
at org.springframework.test.context.bean.override.BeanOverrideBeanFactoryPostProcessor.postProcessBeanFactory(BeanOverrideBeanFactoryPostProcessor.java:98)
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:363)
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:197)
at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:791)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:609)
at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:221)
at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:110)
at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.loadContext(AbstractDelegatingSmartContextLoader.java:212)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:225)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:152)
... 19 more
The workaround is changing the @Bean
method to return the most specific type, SubTestService
in this example.