'@SpringBootTest vs @ContextConfiguration vs @Import in Spring Boot Unit Test

I'm working on a Spring Boot project. I'm writing a Unit Test code based on TDD which is a little bit difficult.

@SpringBootTest loaded all beans, which led to longer test times.

So I used the @SpringBootTest's class designation.

I completed the test normally, but I am not sure the difference between using @ContextConfiguration and using @Import.

All three options run normally. I want to know which choice is the best.

@Service
public class CoffeeService {

    private final CoffeeRepository coffeeRepository;

    public CoffeeService(CoffeeRepository coffeeRepository) {
        this.coffeeRepository = coffeeRepository;
    }

    public String getCoffee(String name){
        return coffeeRepository.findByName(name);
    }
}

public interface CoffeeRepository {
    String findByName(String name);
}

@Repository
public class SimpleCoffeeRepository implements CoffeeRepository {

    @Override
    public String findByName(String name) {
        return "mocha";
    }
}

Option 1 (SpringBootTest Annotation) - OK  
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {CoffeeService.class, SimpleCoffeeRepository.class})
public class CoffeeServiceTest {

    @Autowired
    private CoffeeService coffeeService;

    @Test
    public void getCoffeeTest() {
        String value = coffeeService.getCoffee("mocha");
        assertEquals("mocha", value);
    }
}


Option 2 (ContextConfiguration Annotation) - OK
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {SimpleCoffeeRepository.class, CoffeeService.class})
public class CoffeeServiceTest {

    @Autowired
    private CoffeeService coffeeService;

    @Test
    public void getCoffeeTest() {
        String value = coffeeService.getCoffee("mocha");
        assertEquals("mocha", value);
    }
}

Option 3 (Import Annotation) - OK
@RunWith(SpringRunner.class)
@Import({SimpleCoffeeRepository.class, CoffeeService.class})
public class CoffeeServiceTest {

    @Autowired
    private CoffeeService coffeeService;

    @Test
    public void getCoffeeTest() {
        String value = coffeeService.getCoffee("mocha");
        assertEquals("mocha", value);
    }


Solution 1:[1]

I think all 3 presented options are bad if your intent is to run a proper unit test. A unit test must be blazing fast, you should be able to run hundreds of those in a second or so (depending on the hardware of course, but you get the idea). So once you say "I start spring for each test" - it's not a unit test anymore. Starting spring for each test is a very expensive operation.

What's interesting is that your code of CoffeeService is written in a way that it's perfectly testable: Just use some library like Mockito to mock the repository class and you can test the service logic without any spring at all. You won't need any spring runner, any spring annotations. You'll also see that these tests are running much faster.

class MyServiceTest {

    @Test
    public void test_my_service_get_coffee_logic() {
          
           // setup:
           CoffeeRepository repo = Mockito.mock(CoffeeRepository.class);
           Mockito.when(repo.findByName("mocha")).thenReturn("coffeeFound");

           CoffeeService underTest = new CoffeeService(repo);

           // when:
           String actualCoffee  =  underTest.getCoffee("mocha");

           // then:
           assertEquals(actualCoffee, "coffeeFound");
    }
}
 

Now regarding spring test library

You can think about it as a way to test code that requires some interconnections with other components and its problematic to mock everything out. Its a kind of integration test inside the same JVM. All the ways that you've presented run an Application Context and this is a very complicated thing actually under the hood, there are entire sessions on youtube about what really happens during the application context startup - although, beyond the scope of the question, the point is that it takes time to execute the context startup

@SpringBootTest goes further and tries to mimic the processes added by Spring Boot framework for creating the context: Decides what to scan based on package structures, loads external configurations from predefined locations optionally runs autoconfiguration starters and so on and so forth.

Now the application context that might load all the beans in the application can be very big, and for some tests, it's not required. Its usually depends on what is the purpose of the test

For Example, if you test rest controllers (that you've placed all the annotations correctly) probably you don't need to start up DB connections.

All the ways you've presented filter what exactly should be run, what beans to load and to inject into each other.

Usually, these restrictions are applied to "layers" and not to single beans (layers = rest layer, data layer and so forth).

The second and third methods are actually the same, they are different ways to "filter" the application context preserving only the necessary beans.

Update:

Since you've already done the performance comparison of the methods:

Unit test = very fast test, it's purpose is to verify the code you've written (or one of your colleagues of course) So if you run Spring its automatically means a relatively slow test. So to answer your question

Whether using @ContextConfiguration can be a "Unit Test"

No, it cannot, it's an integration test that runs only one class in spring.

Usually, we don't run only one class with Spring Framework. What is the benefit of running it inside the spring container if you only want to test the code of one class (a unit)? Yes, in some cases it can be a couple of classes, but not tens or hundreds.

If you run one class with spring then, in any case, you'll have to mock all its dependencies, the same can be done with mockito...

Now regarding your questions

@ContextConfiguration vs. @SpringBootTest technical differences.

@SpringBootTest is relevant only if you have a Spring Boot application. This framework uses Spring under the hood but, in a nutshell, comes with many pre-defined recipes/practices of how to write the "infrastructure" of the application:

  • configuration management,
  • package structure,
  • pluggability
  • logging
  • database integration etc.

So Spring Boot establishes well-defined processes to deal with all the aforementioned items, and if you want to start the test that will mimic the spring boot application, then you use @SpringBootTest annotation. Otherwise (or in case you have only spring driven application and not a spring boot) - don't use it at all.

@ContextConfiguration is an entirely different thing though. It just says what beans would you like to use in Spring driven application (it also works with spring boot)

Is "Unit Test" the correct way to use @ContextConfiguration? Or not?

As I said - all the spring test related stuff is for integration testing only, so no, it's a wrong way to be used in unit tests. For unit tests go with something that doesn't use spring at all (like mockito for mocks and a regular junit test without spring runner).

Solution 2:[2]

Like @MarkBramnik says if you're intent is to write a unit test you have to mock other components that uses the specific one you're testing. @SpringBootTest is recommended if you want to write an integration test that simulated the application process. @ContextConfiguration is used when you @Autowired a component in your unit test and you have to set the configuration of that class, or the class where you created the bean.

Solution 3:[3]

The answer of @MarkBramnik is the simplest I ever read on Spring testing. From the Spring official documentation, all has been said and your tests is about 2 categories :

  • Unit Tests : which doesn't required the Spring context to be loaded. At this point, you don't need any Spring TestContext Framework annotation. You can simply use, JUnit, TestNG, Mockito etc...

The POJOs that make up your application should be testable in JUnit or TestNG tests, with objects instantiated by using the new operator, without Spring or any other container. You can use mock objects (in conjunction with other valuable testing techniques) to test your code in isolation

  • Integration Tests : which required you to load a part or all your context, so you use all the needed Spring annotations

For certain unit testing scenarios, however, the Spring Framework provides mock objects and testing support classes, which are described in this chapter

From Spring documentation :

Dependency injection should make your code less dependent on the container than it would be with traditional Java EE development. The POJOs that make up your application should be testable in JUnit or TestNG tests, with objects instantiated by using the new operator, without Spring or any other container. You can use mock objects (in conjunction with other valuable testing techniques) to test your code in isolation. If you follow the architecture recommendations for Spring, the resulting clean layering and componentization of your codebase facilitate easier unit testing. For example, you can test service layer objects by stubbing or mocking DAO or repository interfaces, without needing to access persistent data while running unit tests.

True unit tests typically run extremely quickly, as there is no runtime infrastructure to set up. Emphasizing true unit tests as part of your development methodology can boost your productivity. You may not need this section of the testing chapter to help you write effective unit tests for your IoC-based applications. For certain unit testing scenarios, however, the Spring Framework provides mock objects and testing support classes, which are described in this chapter.

If your application is based on the Spring recommanded architecture (Repository > Service > Controller > etc...), you should have the following rules (in my opinion) :

  • test your Respository : use @DataJpaTest annotation for test slice.
  • test your Service layer : use JUnit and Mockito. Here you will mock your Repository
  • test your Controller layer : use @WebMvcTest annotation for test slice or use JUnit and Mockito. Here you will mock your Service in both cases
  • test a Component, such as a third party library wrapper or load some specific beans: use @ExtendWith(SpringExtension.class) and @ContextConfiguration/@Import or @SpringJUnitWebConfig which is the combinaison of the both.
  • test an integration with LDAP or any external API etc... : use @DataLdapTest test slice or associated annotation, or just mock it with WireMock or any mocking tool.
  • do an integration test : use @SpringBootTest

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Harry Coder
Solution 2 Harry Coder
Solution 3