8

To avoid the Spring context reloading again and again, I have moved @MockBean annotated injection to a parent class, something like this.

@SpringBootTest
.......
public abstract  BaseTest {
    @MockBean
    protected OneService oneService;

This serves the test classes which need a mock for OneService. However, for the test that I would like also extended from BaseTest, but inject the real OneService via @Autowired will not work, as it will inherit the mock injection from the BaseTest.

public AnotherTest extends BaseTest {
    @Autowired
    protected OneService oneService;

Even I used @Autowired annotation, the oneService field will be the mock instance inherited from BaseTest.

Is there a way I can force inject to use autowired?

Sam Brannen
  • 29,611
  • 5
  • 104
  • 136
user1619397
  • 680
  • 2
  • 11
  • 23
  • Based on a quick glance of the Spring Boot Test internals, I don't think it's possible to achieve that currently. The `DefinitionsParser` appears not to take into account that a field may be overridden by a subclass: https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/DefinitionsParser.java – Sam Brannen Jan 28 '21 at 14:55
  • Try constructor injection that way might be possible to supply the bean from child to parent have a look at that approach, might help. – piyush tyagi Jan 28 '21 at 15:04
  • thanks. is there an alternative I am trying to do? I was thinking to use @ Qualifier, but it means I need to initialize the bean as a mock and actual OneService with a qualifier respectively. I am struggling to get it to work. I might miss understanding somewhere. The @ MockBean inject automatically to context, so I can do nothing, so I can only update the OneService implmentation with qualifier and change all places using it? – user1619397 Jan 28 '21 at 15:13

1 Answers1

6

In Java there is technically no way to override a field. When you declare a field with the same name in a subclass, that field shadows the field with the same name in the superclass. So that is one hurdle.

The second hurdle stems from the fact that there is no way in Spring Boot's testing support to turn off mocking of a bean in a subclass if the superclass configures the mocking via @MockBean.

Thus, the solution is to invert what you are trying to do by having the real bean injected via @Autowired in the superclass and then turn on mocking for the specific bean type in the subclass.

  1. Declare the @Autowired field in the superclass but do not redeclare the field in the subclass.
  2. Declare @MockBean at the class level in the subclass, specifying which classes (i.e., bean types) to be mocked.

That last step results in the inherited field being injected with a mock in the subclass.

The following two classes demonstrate this technique in practice.

@SpringBootTest(classes = BaseTests.Config.class)
class BaseTests {

    @Autowired
    protected Service service;

    @Test
    void service() {
        assertThat(service.getMessage()).isEqualTo("real");
    }

    @Configuration
    static class Config {

        @Bean
        Service service() {
            return new Service();
        }
    }

    static class Service {
        String getMessage() {
            return "real";
        }
    }

}
@MockBean(classes = BaseTests.Service.class)
class MockBeanTests extends BaseTests {

    @BeforeEach
    void setUpMock() {
        when(service.getMessage()).thenReturn("mock");
    }

    @Test
    @Override
    void service() {
        assertThat(service.getMessage()).isEqualTo("mock");
    }

}
Sam Brannen
  • 29,611
  • 5
  • 104
  • 136