86

I have a Bean defined in a class decorated with @Configuration:

@Configuration
public class MyBeanConfig {
    @Bean
    public String configPath() {
        return "../production/environment/path";
    }
}

I have a class decorated with @TestConfiguration that should override this Bean:

@TestConfiguration
public class MyTestConfiguration {
    @Bean
    @Primary
    public String configPath() {
        return "/test/environment/path";
    }
}

The configPath bean is used to set the path to an external file containing a registration code that must be read during startup. It is used in an @Component class:

@Component
public class MyParsingComponent {
    private String CONFIG_PATH;
    
    @Autowired
    public void setCONFIG_PATH(String configPath) {
        this.CONFIG_PATH = configPath;
    }
}

While trying to debug this I set a breakpoint inside each method as well as the constructor of the test config class. The @TestConfiguration's constructor breakpoint is hit, so i know that my test configuration class instantiates, however the configPath method of that class is never hit. Instead, the configPath method of the normal @Configuration class is hit and the @Autowired String in MyParsingComponent is always ../production/environment/path rather than the expected /test/environment/path.

Not sure why this is happening. Any thoughts would be greatly appreciated.

Lii
  • 11,553
  • 8
  • 64
  • 88
The Head Rush
  • 3,157
  • 2
  • 25
  • 45
  • 3
    Is your test class annotated with `@Import(MyTestConfiguration.class)`? – Sam Brannen May 31 '18 at 14:17
  • 3
    @SamBrannen It wasn't there. And adding it didn't work. But changing it to `@ContextConfiguration(MyTestConfiguration.class)` did. Still don't understand why the @Primary annotation was being ignored without the `ContextConfiguration`, though. – The Head Rush May 31 '18 at 15:17
  • 1
    As documented in the Spring Boot reference manual, any beans configured in a top-level class annotated with `@TestConfiguration` will not be picked up via component scanning. So that's why you have to explicitly declare it. – Sam Brannen May 31 '18 at 16:23
  • If you class annotated with `@TestConfiguration` were a static nested class within your test class, it would be used automatically. – Sam Brannen May 31 '18 at 16:24
  • If that answers your question(s), I can make it a formal _answer_. ;-) – Sam Brannen May 31 '18 at 16:24
  • @SamBrannen That would be good. Thanks much for the help and explainations. – The Head Rush May 31 '18 at 17:15
  • I found that if you have some another class in `@ContextConfiguration(classes=...)` `@TestConfiguration` will not work anymore. So you have to add your class with configuration explicitly to `@ContextConfiguration(classes=...)`. – Valeriy K. Jan 18 '19 at 14:17

9 Answers9

119

As documented in the Detecting Test Configuration section of the Spring Boot reference manual, any beans configured in a top-level class annotated with @TestConfiguration will not be picked up via component scanning. So you have to explicitly register your @TestConfiguration class.

You can do that either via @Import(MyTestConfiguration.class) or @ContextConfiguration(classes = MyTestConfiguration.class) on your test class.

On the other hand, if your class annotated with @TestConfiguration were a static nested class within your test class, it would be registered automatically.

Sam Brannen
  • 29,611
  • 5
  • 104
  • 136
  • 1
    I try to do a test with #TestConfiguration placed on a top-level class and with #Import(MyTestConfiguration.class) and without #TestConfiguration but with #Import(MyTestConfiguration.class). The result was equal. Does it mean that #TestConfiguration is useless? – Valeriy K. Feb 14 '19 at 09:21
  • `@TestConfiguration` is still useful. Please consult the linked documentation for details. – Sam Brannen Feb 15 '19 at 10:04
  • The URL is dead – G_V Oct 17 '19 at 08:58
  • 1
    I fixed the link due to changes in Spring Boot 2.2's documentation. Thanks – Sam Brannen Oct 18 '19 at 09:29
  • I would like to reuse `@TestConfiguration` for multiple test classes hence having an abstract test class all the others do inherit might be an option. Unfortunately, `@TestConfiguration` is not picked up if it is in the abstract class the current test inherits from. @SamBrannen is it by design? How to share `@TestConfiguration` in-between test classes? – rilaby Nov 29 '20 at 13:59
  • Why I can not override a spring bean (with same name) with ```@Import``` annotation .. I can override with ```@ContextConfiguration``` and static inner class annotated as ```@TestConfiguration``` – Gursel Koca Nov 19 '21 at 12:42
  • Just a heads up, try both `@Import(Your.class)` and `@ContextConfiguration(classes = Your.class)` one after the other. Not sure why, but the former didn't work out for me. However, the latter did! – Prasannjeet Singh Mar 16 '22 at 14:24
  • It did not work for me, in Kotlin, using @Import, but it IS working using `@ContextConfiguration(classes = [TestConfiguration::class])`. – JoshGough Jul 25 '22 at 18:10
  • When using `@Import`, I also needed to annotated the overriden bean with `@ConditionalOnMissingBean(name = "beanNameToOverride")` – Blockost Dec 15 '22 at 09:00
54

Make sure that the method name of your @Bean factory method does not match any existing bean name. I had issues with method names like config() or (in my case) prometheusConfig() which collided with existing bean names. Spring skips those factory methods silently and simply does not call them / does not instantiate the beans.

If you want to override a bean definition in your test, use the bean name explicitly as string parameter in your @Bean("beanName") annotation.

Marcel Thimm
  • 541
  • 4
  • 3
  • 9
    this is the actual answer. I have a nested static class with @TestConfiguration and it wasn't working. The problem was that the factory method was named the same as the actual bean. – Sebastian Apr 19 '20 at 21:21
  • 1
    In my very peculiar case I had to add `@Primary` as well. Otherwise it would just throw `expected single matching bean but found 2`. – Ivar Apr 15 '21 at 16:17
  • Marcel, this has been your first and only answer on SO - but it's a life saver! Thank you so much <3 – Peter Wippermann Jun 23 '22 at 13:24
13
  • Test configuration has to be explicitly imported in the test via @Import({MyTestConfiguration.class}).
  • The name of the @Bean methods in @Configuration and @TestConfiguration have to be different. At least it makes difference in Spring Boot v2.2.
  • Also make sure spring.main.allow-bean-definition-overriding=true otherwise the bean could not be overriden.
Maksim Sorokin
  • 2,334
  • 3
  • 34
  • 61
  • 12
    Your 2nd and 3rd bullet statements fo conflict with each other - the bean override mechanism implies the bean names do match. – rilaby Nov 29 '20 at 13:37
12

For me worked this code:

  @TestConfiguration // 1. necessary
  public class TestMessagesConfig {

    @Bean
    @Primary // 2. necessary
    public MessageSource testMessageSource() { // 3. different method name than in production code e.g. add test prefix

    }
  }
Andrew Sneck
  • 724
  • 9
  • 18
  • for me too this only seems to be working. Tried the above 2 answers but none of them worked. Not sure why the Main Configuration is read by Spring even for test case when we have static nested class with @TestConfiguration annotation. – Gaurav Parek May 08 '22 at 17:01
6

I struggled with a related problem, whereby even though I was using an inner static class, my test bean was not being registered.

It turns out, You still need to add your inner static class to the @ContextConfiguration class array, otherwise the beans inside the @TestConfiguration doesn't get picked up.

public interface Foo {
    String execute();
}
public class FooService {
    private final Foo foo;

    FooService(Foo foo) {
        this.foo = foo;
    }

    public String execute() {
        return foo.execute();
    }
}
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {FooService.class, FooTest.FooTestConfig.class})
public class FooTest {
    @Autowired
    FooService fooService;

    @Test
    void test() {
        Assertions.assertEquals("MY_TEST_BEAN", fooService.execute());
    }

    @TestConfiguration
    static class FooTestConfig {
        @Bean
        public Foo getFooBean() {
            return () -> "MY_TEST_BEAN";
        }
    }
}
Didier Breedt
  • 511
  • 5
  • 6
  • 1
    this is the only thing that helped, thank you for your answer. – Eugene Sep 12 '22 at 16:48
  • In case of `@TestConfiguration` being a static inner class in your test class, no need to explicitly register the configuration class, it will be picked up automatically. – Hasintha Abeykoon Oct 27 '22 at 13:35
2

I came across a similar issue recently and got it sorted out by annotating my testing bean with @Primary as well as @Bean. Not sure why it's required, which seems not documented in the Spring doc. The version of my SpringBoot is 2.0.3.

snowfox
  • 1,978
  • 1
  • 21
  • 21
2

In my case replacing @Import(TestConfig.class) with @ContextConfiguration(classes=TestConfig.class) did the trick. For some reason, some of the beans from TestConfig but 1 wasn't until I replaced @Import with @ContextConfiguration. This was also mentioned in some comments that were hidden because they had no upvotes.

Procrastinator
  • 2,526
  • 30
  • 27
  • 36
JavaDevOps
  • 41
  • 1
2

I found it odd how several answers stated that the names of the @Beans have to be different from each other. How would that make one override the other?
There wasn't one specific answer that worked for me, but I've solved the issue by combining some of their advices.

Here's what worked for me.

Main configuration class:

@Configuration
public class SpringConfiguration {

    @Bean
    BeanInterface myBean() {
        return new BeanImplementation();
    }
    
    @Bean
    OtherClass otherBean() {
        return new OtherClass();
    }
}

Test configuration class:

@TestConfiguration
public class TestSpringConfiguration {

    @Bean
    @Primary
    BeanInterface myBean() {
        return new TestBeanImplementation();
    }
}

Test class:

@SpringBootTest(classes = TestSpringConfiguration.class, 
        properties = "spring.main.allow-bean-definition-overriding=true")
public class Tests {
    
    @Test
    public void test() {
        // do stuff
    }
}

In this way, the "myBean" bean instance is the one defined in the TestSpringConfiguration class, while "otherBean" is the one defined in the SpringConfiguration class, since it's not overridden.
If I gave two different names to the "myBean" beans, the "real" one would still be initialized and, in my case, would give an error during tests, since it needs something that's only available at runtime in its proper environment.
Once I gave both the same name, Spring would throw an error saying that they were conflicting. Hence why I had to specify the property spring.main.allow-bean-definition-overriding=true in the @SpringBootTest annotation of the test class.



By the way, if you're NOT using Spring Boot, I guess these alternative annotations could work for you:

@ExtendWith(value = SpringExtension.class)
@ContextConfiguration(loader = AnnotationConfigContextLoader.class, // <- not sure about this one
        classes = { SpringConfiguration.class, TestSpringConfiguration.class })
public class Tests {
    
    @Test
    public void test() {
        // do stuff
    }
}

Then, you would still have to set the property spring.main.allow-bean-definition-overriding=true in the test application.yml or application.properties file, or in some other way via code on startup.

Note: I'm not 100% sure that you would need the loader = AnnotationConfigContextLoader.class thing. Try without it, first. I needed it in a project of mine which had Spring without Boot, but I can't remember whether it's a standard thing to set or I needed it for some specific reason.

nonzaprej
  • 1,322
  • 2
  • 21
  • 30
0

In my case it was an issue with @RunWith(SpringRunner.class), I'm not exactly sure why it wasn't working, I was following this - Testing in Spring Boot

But after replacing that with @ExtendWith(SpringExtension.class) the inner static @TestConfiguration class created the beans as expected.

Maybe a version mismatch - I'm using Spring Boot 2.7.2.