Overview

While working on #33925 and #34194, I noticed inconsistencies and peculiarities in the search algorithm for @TestBean factory methods.

Specifically, for a @TestBean field defined in a top-level class, the search for a factory method begins with the current test class (which may be a subclass of or or a @Nested class defined in the class in which the @TestBean field is declared). Whereas, for a @TestBean field defined in a @Nested test class, the search for a factory method begins with the @Nested test class.

The above leads to inconsistent factory method discovery for @TestBean factory methods.

In addition, the algorithm used for top-level classes results in "duplicate bean override" failures for cases which are clearly not duplicates but rather an "override of an override". In other words, a @TestBean declaration in a subclass (which resolves to a factory method in the subclass) currently cannot override a seemingly identical @TestBean declaration in a superclass (which resolves to a factory method in the superclass), which some might consider a bug.

Example

@SpringJUnitConfig
class BaseTests {

    @TestBean
    String enigma;

    static String enigma() {
        return "enigma in superclass";
    }

    @Test
    void test() {
        assertThat(enigma).isEqualTo("enigma in superclass");
    }

    @Nested
    class NestedTests {

        @TestBean
        String enigma;

        static String enigma() {
            return "enigma in nested class";
        }

        @Test
        void test() {
            assertThat(enigma).isEqualTo("enigma in nested class");
        }
    }

    @Configuration
    static class Config {

        @Bean
        String enigma() {
            return "enigma in @Configuration class";
        }
    }
}
class ExtendedTests extends BaseTests {

    @TestBean
    String enigma;

    static String enigma() {
        return "enigma in subclass";
    }

    @Test
    @Override
    void test() {
        assertThat(enigma).isEqualTo("enigma in subclass");
    }
}
  • BaseTests passes as expected.
  • BaseTests.NestedTests currently passes because of the aforementioned inconsistent search algorithm.
  • ExtendedTests fails with the following error.
java.lang.IllegalStateException: Duplicate BeanOverrideHandler discovered in test class example.ExtendedTests: [TestBeanOverrideHandler@2dc3271b field = java.lang.String example.ExtendedTests.enigma, beanType = java.lang.String, beanName = [null], strategy = REPLACE_OR_CREATE, factoryMethod = enigma@ExtendedTests]

The cause of the exception is due to the fact that two @TestBean override handlers resolve the same factory method enigma@ExtendedTests; however, one might expect those to resolve to enigma@BaseTests and enigma@ExtendedTests (i.e., two distinct factory methods).

Proposal

  • Resolve @TestBean factory methods consistently, beginning the search with the declaring class of the @TestBean field.

One downside to the proposal is that @TestBean factory methods would no longer be able to be resolved lazily in subclasses (somewhat like "late binding" to a concrete method), which would be a change in behavior.

Another option is to make the factory method search algorithm for @Nested test classes align with the current semantics for top-level test classes.

Related Issues

  • 33925

  • 34194

Comment From: sbrannen

Team Decision: Consistently resolve @TestBean factory methods beginning the search with the class in which the @TestBean field is declared.