spring-projects / spring-framework

Spring Framework
https://spring.io/projects/spring-framework
Apache License 2.0
56.75k stars 38.15k forks source link

Support `@MockitoBean` at the class level on test classes #33925

Open nmck257 opened 5 days ago

nmck257 commented 5 days ago

MockitoBean is designed to replace the now-deprecated MockBean annotation. MockBean could target either a field or a type. Currently, MockitoBean can only target a field. This issue proposes adding the support to target types to MockitoBean, similar to MockBean.

(pasting some prose from #29917 comments)

This feature is useful for test cases which want to stub out some set of beans (ie to avoid some side effect of regular instantiation), but didn't care about specifying behavior.

Suppose you have multiple @SpringBootTest classes, and want to stub out the same set of beans for each of them. With an annotation targeting the class itself, you could define a meta-annotation containing those repeated invocations of MockBean and reuse wherever needed. If we only have support for mocking beans as fields, then the next alternative would be a common superclass which declares those annotated fields. That pattern pushes you to (single) inheritance, whereas the class annotation pattern was composition-friendly.

Subjectively, I also think that if the test developer's intent is not to define any behavior for the mocked bean, then it's easier to read and maintain if we can avoid adding an unused field to the class scope.

Here's some Kotlin code to help demonstrate the point above:

@MockBeans(
    MockBean(MyServiceWithOutOfProcessSideEffectsOnBoot::class),
    MockBean(MyPreemptiveClientAuthTokenFetcher::class),
    MockBean(MyServiceWhichLoadsOneMillionThingsIntoMemoryOnBoot::class),
)
annotation class MockExpensiveDependencies

@SpringBootTest
@MockExpensiveDependencies
class BusinessLogicControllerTest {
    // ...
}

@SpringBootTest
@MockExpensiveDependencies
class SomeOtherTest {
    // ...
}

vs

open class MockExpensiveDependenciesBase {
    @MockBean
    private lateinit var foo: MyServiceWithOutOfProcessSideEffectsOnBoot
    @MockBean
    private lateinit var bar: MyPreemptiveClientAuthTokenFetcher
    @MockBean
    private lateinit var gav: MyServiceWhichLoadsOneMillionThingsIntoMemoryOnBoot
}

@SpringBootTest
class BusinessLogicControllerTest : MockExpensiveDependenciesBase() {
    // ...
}

@SpringBootTest
class SomeOtherTest : MockExpensiveDependenciesBase() {
    // ...
}
OrangeDog commented 5 days ago

Here's a couple of other cases that @MockBean allowed.

Including mocks via additional config (not just other annotations):

@TestConfiguration
@MockBean({
    MyServiceWithOutOfProcessSideEffectsOnBoot.class,
    MyPreemptiveClientAuthTokenFetcher.class
})
@EnableSomething
@Import(OtherStuff.class)
public class SharedTestConfiguration { }

@SpringTestAnnotation
@Import({SharedTestConfiguration.class, SomeOtherConfig.class})
public class SomeTests {

If one of these mocks were autowired, stub behaviour could then be specified. This doesn't work when returning a mock from a @Bean method, depending on how beans are proxied:

@SpringTestAnnotation
@MockBean(MyServiceWithOutOfProcessSideEffectsOnBoot.class)  // actually declared elsewhere
public class SomeTests {
    @Autowired private MyServiceWithOutOfProcessSideEffectsOnBoot myService;

    @Test
    public void onlyThisTestSpecificallyWantsToStubIt() {
        doReturn(true).when(myService).isSomething();
    }
jack5505 commented 3 days ago

Can I take this issue to work on this

sbrannen commented 1 day ago

Hi @jack5505,

The team has not yet made a decision regarding this issue, and it's highly unlikely that the implementation would be performed by anyone outside the core team.

Thanks for the offer anyway.