简体   繁体   中英

Injecting non-mocks defined by a @Configuration class into a unit test

Java 8, JUnit 4 and Spring Boot 2.3 here. I have a situation where I have a @Component -annotated Spring Boot class that gets @Autowired with all its dependencies (the beans are defined in a @Configuration -annotated config class):

@Configuration
public class SomeConfig {

    @Bean
    public List<Fizz> fizzes() {
        Fizz fizz = new Fizz(/*complex logic here*/);
        return new Collections.singletonList(fizz);
    }

    @Bean
    public Buzz buzz() {
        return new Buzz(/*complex logic here*/);
    }
    
}

@Component
public class ThingDoerinator {

    @Autowired
    private Lizz<Fizz> fizzes;

    @Autowired
    private Buzz buzz;

    public String doStuff() {
        // uses fizz and buzz extensively inside here...
    }
    
}

I can easily write a unit test to inject all of these dependencies as mocks:

public class ThingDoerinatorTest extends AbstractBaseTest {

    @Mock
    private List<Fizz> fizzes;

    @Mock
    private Buzz buzz;

    @InjectMocks
    private ThingDoerinator thingDoerinator;

    @Test
    public void should_do_all_the_stuff() {

        // given
        // TODO: specify fizzes/buzz mock behavior here

        // when
        String theThing = thingDoerinator.doStuff();

        // then
        // TODO: make some assertions, verify mock behavior, etc.

    }
}

And 80% of the time that works for me. However I am now trying to write some unit tests that are more like integration tests, where instead of injected mocks, I want the beans to be instantiated like normal and get wired into the ThingDoerinator class like they would be in production:

public class ThingDoerinatorTest extends AbstractBaseTest {

    @Mock
    private List<Fizz> fizzes;

    @Mock
    private Buzz buzz;

    @InjectMocks
    private ThingDoerinator thingDoerinator;

    @Test
    public void should_do_all_the_stuff() {

        // given
        // how do I grab the same Fizz and Buzz beans
        // that are defined in SomeConfig?

        // when -- now instead of mocks, fizzes and buzz are actually being called
        String theThing = thingDoerinator.doStuff();

        // then
        // TODO: make some assertions, verify mock behavior, etc.

    }
}

How can I accomplish this?

You can use SpringBootTest.

@SpringBootTest(classes = {Fizz.class, Buzz.class, ThingDoerinator.class})
@RunWith(SpringRunner.class)
public class ThingDoerinatorTest {
    @Autowired
    private Fizz fizz;

    @Autowired
    private Buzz buzz;

    @Autowired
    private ThingDoerinator thingDoerinator;
}

You don't need to mock anything, just inject your class in your test and your configuration will provide the beans to your ThingDoerinator class and your test case will work as if you are calling the ThingDoerinator. doStuff() ThingDoerinator. doStuff() method from some controller or other service.

TL;DR

I think, you are confused with tests running whole spring context and unit test which just test the function/logic we wrote without running whole context. Unit tests are written to test the piece of function, if that function would behave as expected with given set of inputs.

Ideal unit test is that which doesn't require a mock, we just pass the input and it produces the output (pure function) but often in real application this is not the case, we may find ourselves in situation when we interact with some other object in our application. As we are not about to test that object interaction but concerned about our function, we mock that object behaviours.

It seems, you have no issue with unit test, so you could mock your List of bean in your test class ThingDoerinator and inject them in your test case and your test case worked fine.

Now, you want to do the same thing with @SpringBootTest , so I am taking a hypothetical example to demonstrate that you don't need to mock object now, as spring context will have them, when @springBootTest will load whole context to run your single test case.

Let's say I have a service FizzService and it has one method getFizzes()

@Service
public class FizzService {
    private final List<Fizz> fizzes;

    public FizzService(List<Fizz> fizzes) {
        this.fizzes = fizzes;
    }

    public List<Fizz> getFizzes() {
        return Collections.unmodifiableList(fizzes);
    }
}

Using constructor injection here 1

Now, my List<Fizzes would be created through a configuration (similar to your case) and been told to spring context to inject them as required.

@Profile("dev")
@Configuration
public class FizzConfig {

    @Bean
    public List<Fizz> allFizzes() {
        return asList(Fizz.of("Batman"), Fizz.of("Aquaman"), Fizz.of("Wonder Woman"));
    }
}

Ignore the @Profile annotation for now 2

Now I would have spring boot test to check that if this service will give same list of fizzes which I provided to the context, when it will run in production

@ActiveProfiles("test")
@SpringBootTest
class FizzServiceTest {
    @Autowired
    private FizzService service;

    @Test
    void shouldGiveAllTheFizzesInContext() {
        List<Fizz> fizzes = service.getFizzes();

        assertThat(fizzes).isNotNull().isNotEmpty().hasSize(3);
        assertThat(fizzes).extracting("name")
                          .contains("Wonder Woman", "Aquaman");
    }

}

Now, when I ran the test, I saw it works, so how did it work, let's understand this. So when I run the test with @SpringBootTest annotation, it will run similar like it runs in production with embedded server.

So, my config class which I created earlier, will be scanned and then beans created there would be loaded into the context, also my service class annotated with @Service annotation, so that would be loaded as well and spring will identify that it needs List<Fizz> and then it will look into it's context that if it has that bean so it will find and inject it, because we are supplying it from our config class.

In my test, I am auto wiring the service class, so spring will inject the FizzService object it has and it would have my List<Fizz> as well (explained in previous para).

So, you don't have to do anything, if you have your config defined and those are being loaded and working in production, those should work in your spring boot test the same way unless you have some custom setup than default spring boot application.

Now, let's look at a case, when you may want to have different List<Fizz> in your test, in that case you will have to exclude the production config and include some test config.

In my example to do that I would create another FizzConfig in my test folder and annotate it with @TestConfiguration

@TestConfiguration
public class FizzConfig {
    @Bean
    public List<Fizz> allFizzes() {
        return asList(Fizz.of("Flash"), Fizz.of("Mera"), Fizz.of("Superman"));
    }
}

And I would change my test a little

@ActiveProfiles("test")
@SpringBootTest
@Import(FizzConfig.class)
class FizzServiceTest {
    @Autowired
    private FizzService service;

    @Test
    void shouldGiveAllTheFizzesInContext() {
        List<Fizz> fizzes = service.getFizzes();
        assertThat(fizzes).extracting("name")
                          .contains("Flash", "Superman");
    }

}

Using different profile for tests, hence @ActiveProfile annotation, otherwise the default profile set is dev which will load the production FizzConfig class as it would scan all the packages ( 2 Remember the @Profile annotation above, this will make sure that earlier config only runs in production/dev env ). Here is my application.yml to make it work.

spring:
  profiles:
    active: dev
---
spring:
  profiles: test

Also, notice that I am importing my FizzConfiguration with @Import annotation here, you can also do same thing with @SpringBootTest(classes = FizzConfig.class) .

So, you can see test case has different values than in production code.

Edit

As commented out, since you don't want test to connect to database in this test, you would have to disable auto configuration for spring data JPA, which spring boot by default does that.

3 You can create a another configuration class like

@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
public TestDataSourceConfiguration {}

And then include that with your @SpringBootTest , this will disable default database setup and then you would be left with last piece of puzzle which is any repository interaction you might be having in your class.

Because now, you don't have auto configuration in your test case, those repository are just interfaces, which you can easily mock inside your test class

@ActiveProfiles("test")
@SpringBootTest
@Import(FizzConfig.class, TestDataSourceConfiguration.class)
class FizzServiceTest {
    @MockBean
    SomeRepository repository;

    @Autowired
    private FizzService service;

    @Test
    void shouldGiveAllTheFizzesInContext() {
        // Use Mockito to mock behaviour of repository
        Mockito.when(repository.findById(any(), any()).thenReturn(Optional.of(new SomeObject));
 
        List<Fizz> fizzes = service.getFizzes();
        assertThat(fizzes).extracting("name")
                          .contains("Flash", "Superman");
    }

}

1 Constructor injection is advisable instead of `@Autowired` in production code, if your dependencies are really required for that class to work, so please avoid it if possible.
3 Please note that you create such configuration only in test package or mark with some profile, do not create it in your production packages, otherwise it will disable database for your running code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM