50

I have a Spring Boot 1.4.2 application. Some code that is used during startup looks like this:

@Component 
class SystemTypeDetector{
    public enum SystemType{ TYPE_A, TYPE_B, TYPE_C }
    public SystemType getSystemType(){ return ... }
}

@Component 
public class SomeOtherComponent{
    @Autowired 
    private SystemTypeDetector systemTypeDetector;
    @PostConstruct 
    public void startup(){
        switch(systemTypeDetector.getSystemType()){   // <-- NPE here in test
        case TYPE_A: ...
        case TYPE_B: ...
        case TYPE_C: ...
        }
    }
}

There is a component that determines the system type. This component is used during startup from other components. In production, everything works fine.

Now I want to add some integration tests using Spring 1.4's @MockBean.

The test looks like this:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = MyWebApplication.class, webEnvironment = RANDOM_PORT)
public class IntegrationTestNrOne {
    @MockBean 
    private SystemTypeDetector systemTypeDetectorMock;

    @Before 
    public void initMock(){
       Mockito.when(systemTypeDetectorMock.getSystemType()).thenReturn(TYPE_C);
    }

    @Test 
    public void testNrOne(){
      // ...
    }
}

Basically, the mocking works fine. My systemTypeDetectorMock is used and if I call getSystemType -> TYPE_C is returned.

The problem is that the application doesn't start. Currently springs working order seems to be:

  1. create all Mocks (without configuration all methods return null)
  2. start the application
  3. call @Before-methods (where the mocks would be configured)
  4. start test

My problem is that the application starts with an uninitialized mock. So the call to getSystemType() returns null.

My question is: How can I configure the mocks before application startup?

EDIT: If somebody has the same problem, one workaround is to use @MockBean(answer = CALLS_REAL_METHODS). This calls the real component and in my case, the system starts up. After startup, I can change the mock behavior.

Saikat
  • 14,222
  • 20
  • 104
  • 125
Marcel
  • 4,054
  • 5
  • 36
  • 50
  • You can inject mock and call initialization code by hand as described in this answer: http://stackoverflow.com/a/31587946/3440376 – tan9 Feb 14 '17 at 03:31
  • Thanks for the workaround. Using `Answers.CALLS_REAL_METHODS` I was able provide a fake implementation in a subclass taking effect before the set up of other beans, and avoid use of `@Primary`. – Alexander Taylor Mar 12 '22 at 01:23

6 Answers6

56

In this case you need to configure mocks in a way we used to do it before @MockBean was introduced - by specifying manually a @Primary bean that will replace the original one in the context.

@SpringBootTest
class DemoApplicationTests {

    @TestConfiguration
    public static class TestConfig {

        @Bean
        @Primary
        public SystemTypeDetector mockSystemTypeDetector() {
            SystemTypeDetector std = mock(SystemTypeDetector.class);
            when(std.getSystemType()).thenReturn(TYPE_C);
            return std;
        }

    }

    @Autowired
    private SystemTypeDetector systemTypeDetector;

    @Test
    void contextLoads() {
        assertThat(systemTypeDetector.getSystemType()).isEqualTo(TYPE_C);
    }
}

Since @TestConfiguration class is a static inner class it will be picked automatically only by this test. Complete mock behaviour that you would put into @Before has to be moved to method that initialises a bean.

Maciej Walkowiak
  • 12,372
  • 59
  • 63
  • 3
    it worked after setting `spring.main.allow-bean-definition-overriding=true` – Chris Ociepa Nov 03 '20 at 16:51
  • this is working in Spring Boot 2.0.9... as it is in the original answer – Andrew Sneck Jul 29 '21 at 13:42
  • this works, but how to change the stub for every test case ? – Ryuzaki L Dec 01 '21 at 10:04
  • 1
    Move `when` out from the `@Bean` annotated method, and call `reset` on this bean in `@BeforeEach` method. – Maciej Walkowiak Dec 01 '21 at 13:33
  • If you, like me, write a test with a limited Spring context, ie. `@SpringBootTest(classes = {...})`, you have to add `@ContextConfiguration(classes = {DemoApplicationTests.TestConfig.class})` to your test class as well for the bean to be autowirable. – Ben Jun 29 '23 at 08:48
12

I was able to fix it like this

@RunWith(SpringRunner.class)
@SpringBootTest(classes = MyWebApplication.class, webEnvironment = RANDOM_PORT)
public class IntegrationTestNrOne {
    // this inner class must be static!
    @TestConfiguration
    public static class EarlyConfiguration {
       @MockBean 
       private SystemTypeDetector systemTypeDetectorMock;

       @PostConstruct 
       public void initMock(){
          Mockito.when(systemTypeDetectorMock.getSystemType()).thenReturn(TYPE_C);
       }
    }

    // here we can inject the bean created by EarlyConfiguration
    @Autowired 
    private SystemTypeDetector systemTypeDetectorMock;

    @Autowired
    private SomeOtherComponent someOtherComponent;

    @Test 
    public void testNrOne(){
       someOtherComponent.doStuff();
    }
}
lance-java
  • 25,497
  • 4
  • 59
  • 101
4

You can use the following trick:

@Configuration
public class Config {

    @Bean
    public BeanA beanA() {
        return new BeanA();
    }

    @Bean
    public BeanB beanB() {
        return new BeanB(beanA());
    }
}

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {TestConfig.class, Config.class})
public class ConfigTest {

    @Configuration
    static class TestConfig {

        @MockBean
        BeanA beanA;

        @PostConstruct
        void setUp() {
            when(beanA.someMethod()).thenReturn(...);
        }
    }
}

At least it's working for spring-boot-2.1.9.RELEASE

wk.
  • 71
  • 3
-1

Spring's initialization is triggered before @Before Mockito's annotation so the mock is not initialized at the time the @PostConstruct annotated method is executed.

Try to 'delay' your system detection using @Lazy annotation on the SystemTypeDetector component. Use your SystemTypeDetector where you need it, keep in mind that you cannot trigger this detection in a @PostConstruct or equivalent hook.

Florian Lopes
  • 1,093
  • 1
  • 13
  • 20
-2

I think that it's due to the way you autowire your dependencies. Take a look at this (specially the part about 'Fix #1: Solve your design and make your dependencies visible'). That way you can also avoid using the @PostConstruct and just use the constructor instead.

Ahmed Sayed
  • 1,429
  • 12
  • 12
-4

What U are using, is good for a unit tests:

org.mockito.Mockito#when()

Try to use the following methods for mocking spring beans when the context is spined-up:

org.mockito.BDDMockito#given()

If u are using @SpyBean, then u should use another syntax:

willReturn(Arrays.asList(val1, val2))
        .given(service).getEntities(any());
Niki.Max
  • 163
  • 1
  • 5
  • 1
    This is not relevant to the question. The question is about MockBean for spring-boot tests, not simple mocking in a regular JUnit test. – ScrappyDev Sep 17 '20 at 17:40