Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@MockBean does not work with request-scoped Supplier<T> without explicit name #30043

Open
mwisnicki opened this issue Jan 23, 2023 · 5 comments · May be fixed by #34537
Open

@MockBean does not work with request-scoped Supplier<T> without explicit name #30043

mwisnicki opened this issue Jan 23, 2023 · 5 comments · May be fixed by #34537
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: bug A general bug

Comments

@mwisnicki
Copy link

mwisnicki commented Jan 23, 2023

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());
    }

}
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jan 23, 2023
@wilkinsona wilkinsona self-assigned this Feb 8, 2023
@wilkinsona
Copy link
Member

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 spring-projects/spring-boot#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.

@wilkinsona
Copy link
Member

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.

@wilkinsona
Copy link
Member

wilkinsona commented Feb 8, 2023

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> {

	}

}

@wilkinsona wilkinsona removed their assignment Feb 8, 2023
@bclozel bclozel transferred this issue from spring-projects/spring-boot Feb 27, 2023
@bclozel bclozel added the in: core Issues in core modules (aop, beans, core, context, expression) label Feb 27, 2023
@snicoll snicoll self-assigned this Oct 4, 2023
@snicoll
Copy link
Member

snicoll commented Jan 5, 2024

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?

@snicoll snicoll added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Jan 5, 2024
@snicoll snicoll added this to the 6.x Backlog milestone Jan 5, 2024
@sbrannen sbrannen changed the title @MockBean does not work with request-scoped Supplier<T> without explicit name @MockBean does not work with request-scoped Supplier<T> without explicit name Jan 5, 2024
@bclozel bclozel changed the title @MockBean does not work with request-scoped Supplier<T> without explicit name @MockBean does not work with request-scoped Supplier<T> without explicit name Feb 14, 2024
@snicoll snicoll removed their assignment Aug 20, 2024
@jhoeller jhoeller modified the milestones: 6.x Backlog, General Backlog Oct 1, 2024
@currenjin
Copy link

currenjin commented Feb 28, 2025

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.

@currenjin currenjin linked a pull request Mar 5, 2025 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: bug A general bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants