Trying to define mock for request-scoped supplier does not work unless I explicitly name the mock.
The problem with hardcoding bean name is that name can be dependent on configuration (for example via use conditions).
If there is no RequestScope
or I use custom interface there is no such problem.
Spring Boot 2.7.8 + JDK17
Simplified program:
package com.example.testspringoverridebeantest;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.springframework.web.context.annotation.RequestScope;
import java.util.function.Supplier;
@SpringBootApplication
public class TestSpringOverrideBeanTestApplication {
@Bean
@RequestScope
Supplier<String> word() {
return () -> "app";
}
@Bean
@RequestScope
@Profile("test") // just an example condition
@Primary
Supplier<String> testWord() {
return () -> "testapp";
}
public static void main(String[] args) {
SpringApplication.run(TestSpringOverrideBeanTestApplication.class, args);
}
}
@Service
record Greeter(Supplier<String> word) {
String hello() {
return "hello, " + word.get();
}
}
package com.example.testspringoverridebeantest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import java.util.function.Supplier;
@SpringBootTest
class TestSpringOverrideBeanTestApplicationTests {
@Autowired
Greeter greeter;
@MockBean// (name = "testWord") // unbreaks but the name can vary!
Supplier<String> word;
@Test
void canOverrideBeanForTest() {
Mockito.when(word.get()).thenReturn("test");
Assertions.assertEquals("hello, test", greeter.hello());
}
}
Comment From: wilkinsona
Thanks for the reproducer.
When the bean's type is Supplier<String>
, MockitoPostProcessor
asks the bean factory for the names of all beans of type Supplier<String>
. The result is a single name: scopedTarget.word
. This is then filtered out due to the fix for https://github.com/spring-projects/spring-boot/issues/5724. If I update the reproducer to introduce a custom WordSupplier
interface and replace Supplier<String>
with WordSupplier
, when asked for the names of all beans of type WordSupplier
, the bean factory responds with both scopedTarget.word
and word
. After filtering, we're left with word
and the mocking works as expected. I need to dig a bit more, but this looks like a Framework limitation or bug.
Comment From: wilkinsona
word
is a org.springframework.aop.scope.ScopedProxyFactoryBean
. When the type is Supplier<String>
, AbstractBeanFactory.isTypeMatch("word", java.util.function.Supplier<java.lang.String>, false)
is called. It ends up checking if Supplier<String>
is assignable from Supplier
. It isn't so the word
bean is skipped. If there are no generics in the type signature of the request-scoped bean, this type match succeeds. We'll need to get the Framework team to investigate.
Comment From: wilkinsona
More minimal test that shows the difference in Framework's behavior:
package com.example;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ResolvableType;
import org.springframework.web.context.annotation.RequestScope;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
class RequestScopedBeansOfTypeTests {
@Test
void requestScopedGenericSupplier() {
ResolvableType type = ResolvableType.forClassWithGenerics(Supplier.class, String.class);
assertBeansAreFound(GenericSupplierConfiguration.class, type);
}
@Test
void requestScopedCustomSupplier() {
ResolvableType type = ResolvableType.forClass(CustomSupplier.class);
assertBeansAreFound(CustomSupplierConfiguration.class, type);
}
void assertBeansAreFound(Class<?> config, ResolvableType type) {
try (AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext()) {
context.register(config);
context.refresh();
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
String[] names = beanFactory.getBeanNamesForType(type, true, false);
assertThat(names).containsExactlyInAnyOrder("scopedTarget.requestScopedBean", "requestScopedBean", "bean");
}
}
@Configuration(proxyBeanMethods = false)
static class GenericSupplierConfiguration {
@Bean
@RequestScope
Supplier<String> requestScopedBean() {
return () -> "value";
}
@Bean
Supplier<String> bean() {
return () -> "value";
}
}
@Configuration(proxyBeanMethods = false)
static class CustomSupplierConfiguration {
@Bean
@RequestScope
CustomSupplier requestScopedBean() {
return () -> "value";
}
@Bean
CustomSupplier bean() {
return () -> "value";
}
}
static interface CustomSupplier extends Supplier<String> {
}
}
Comment From: snicoll
There's indeed a problematic shortcut with this case as the factory bean creates a proxy for the scope that does not carry the full generic information that is required for the algorithm to match. Looking at the underlying bean definition that's created, I can see that the the resolvedTargetType
is the factory bean class, but I wonder if it shouldn't be the beanClass
instead, with the target type being the return type of the method with its full generic information.
Even if we did that, we still need to modify the algorithm, perhaps checking higher in the stack if the type to match has a generic.
Thoughts @jhoeller?
Comment From: currenjin
Hello, I'd like to work on this issue. I've reproduced the problem with request-scoped Supplier<T>
beans not being found by @MockBean
without an explicit name.
I'm planning to work on a solution that improves type matching for scoped proxy beans with generic types. I'll explore both:
1. Modifying MockitoPostProcessor
to consider beans that might be request-scoped when dealing with generic types
2. Enhancing AbstractBeanFactory.isTypeMatch
to better handle generic information for proxy beans
I'll submit a PR once I have a working solution with appropriate test coverage. Please let me know if you have any specific guidance or if someone else is already working on this.