To test a component/bean in a Spring Boot application, the testing part of the Spring Boot documentation provides much information and multiple ways :
@Test
, @SpringBootTest
, @WebMvcTest
, @DataJpaTest
and still many other ways.
Why provide so many ways ?
How decide the way to favor ?
Should I consider as integration tests my test classes annotated with Spring Boot test annotations such as @SpringBootTest
, @WebMvcTest
, @DataJpaTest
?
PS : I created this question because I noticed that many developers (even experienced) don't get the consequences to use an annotation rather than another.
TL-DR
- write plain unit tests for components that you can straightly test without loading a Spring container (run them in local and in CI build).
write partial integration tests/ slicing unit test for components that you cannot straightly test without loading a Spring container such as components related to JPA, controllers, REST clients, JDBC ... (run them in local and in CI build)
write some full integration tests (end-to-end tests) for some high level components where it brings values (run them in CI build).
3 main ways to test a component
- plain unit test (doesn't load a Spring container)
- full integration test (load a Spring container with all configuration and beans)
- partial integration test/ test slicing (load a Spring container with very restricted configurations and beans)
Can all components be tested in these 3 ways ?
In a general way with Spring any component can be tested in integration tests and only some kinds of components are suitable to be tested unitary(without container).
But note that with or without spring, unitary and integration tests are not opposed but complementary.
Writing a plain unit test
Using Spring Boot in your application doesn't mean that you need to load the Spring container for any test class you run.
As you write a test that doesn't need any dependencies from the Spring container, you don't have to use/load Spring in the test class.
Instead of using Spring you will instantiate yourself the class to test and if needed use a mock library to isolate the instance under test from its dependencies.
That is the way to follow because it is fast and favors the isolation of the tested component.
For example a FooService
annotated as Spring service that performs some computations and that rely on FooRepository
to retrieve some data can be tested without Spring :
@Service
public class FooService{
private FooRepository fooRepository;
public FooService(FooRepository fooRepository){
this.fooRepository = fooRepository;
}
public long compute(...){
List<Foo> foos = fooRepository.findAll(...);
// core logic
long result =
foos.stream()
.map(Foo::getValue)
.filter(v->...)
.count();
return result;
}
}
You can mock FooRepository
and unit test the logic of FooService
.
With JUnit 5 and Mockito the test class could look like :
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@ExtendWith(MockitoExtension.class)
class FooServiceTest{
FooService fooService;
@Mock
FooRepository fooRepository;
@BeforeEach
void init{
fooService = new FooService(fooRepository);
}
@Test
void compute(){
List<Foo> fooData = ...;
Mockito.when(fooRepository.findAll(...))
.thenReturn(fooData);
long actualResult = fooService.compute(...);
long expectedResult = ...;
Assertions.assertEquals(expectedResult, actualResult);
}
}
How to determinate if a component can be plain tested (without spring) or only tested with Spring?
You recognize a code to test that doesn't have any dependencies from a Spring container as the component/method doesn't use Spring feature to perform its logical.
In the previous example, FooService
performs some computations and logic that don't need Spring to be executed. Indeed with or without container the compute()
method contains the core logic we want to assert.
Reversely you will have difficulties to test FooRepository
without Spring as Spring Boot configures for you the datasource, the JPA context and instrument your FooRepository
interface to provide to it a default implementation and multiple other things.
Same thing for testing a controller (rest or MVC) : how to bind the controller to an endpoint without Spring ? How could the controller parse the HTTP request and generate an HTTP response without Spring ? You simply cannot.
Writing a full integration test
Writing an end-to-end test requires to load a container with the whole configuration and beans of the application.
To achieve that @SpringBootTest
is the way :
The annotation works by creating the ApplicationContext used in your
tests through SpringApplication
You can use it in this way to test it without any mock :
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
@SpringBootTest
public class FooTest {
@Autowired
Foo foo;
@Test
public void doThat(){
FooBar fooBar = foo.doThat(...);
// assertion...
}
}
But you can also mock some beans of the container if it makes sense :
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@SpringBootTest
public class FooTest {
@Autowired
Foo foo;
@MockBean
private Bar barDep;
@Test
public void doThat(){
Mockito.when(barDep.doThis()).thenReturn(...);
FooBar fooBar = foo.doThat(...);
// assertion...
}
}
Note the difference for mocking as you want to mock a plain instance of a Bar
class (org.mockito.Mock
annotation)and that you want to mock a Bar
bean of the Spring context (org.springframework.boot.test.mock.mockito.MockBean
annotation).
Full integration tests have to be executed by the CI builds
Loading a full spring context takes time. So you should be cautious with @SpringBootTest
as this may make unit tests execution to be very long and generally you don't want to strongly slow down the local build on the developers machine and the test feedback that matters to make the test writing pleasant and efficient for developers.
That's why "slow" tests are generally not executed on the developers machines.
So you should make them integration tests (IT
suffix instead of Test
suffix in the naming of the test class) and make sure that these are executed only in the continuous integration builds.
But as Spring Boot acts on many things in your application (rest controllers, mvc controllers, JSON serialization/deserialization, persistence, and so for...) you could write many unit tests that are only executed on the CI builds and that is not fine either.
Having end-to-end tests executed only on the CI builds is ok but having also persistence, controllers or JSON tests executed only on the CI builds is not ok at all.
Indeed, the developer build will be fast but as drawback the tests execution in local will detect only a small part of the possible regressions...
To prevent this caveat, Spring Boot provides an intermediary way : partial integration test or the slice testing (as they call it) : the next point.
Writing a partial integration test focusing on a specific layer or concern (slice testing)
As explained in the point "Recognizing a test that can be plain tested (without spring))", some components can be tested only with a running container.
But why using @SpringBootTest
that will load all beans and configurations of your application while you would need to load only few specific configuration classes and beans to test these components ?
For example why loading a full Spring JPA context (beans, configurations, in memory database, and so for) to unitary test a controller ?
And reversely why loading all configurations and beans associated to Spring controllers to unitary test a JPA repository?
Spring Boot addresses this point with the slice testing feature.
These are not as much as fast than a plain unit tests (without container) but these are really much faster than loading the whole context. So executing them on the local machine is generally very acceptable.
Each slice testing flavor loads a very restricted set of auto-configuration classes that you can modify if needed according to your requirements.
Some common slice testing features :
- Auto-configured JSON Tests : @JsonTest
To test that object JSON serialization and deserialization is working
as expected, you can use the @JsonTest annotation.
- Auto-configured Spring MVC Tests : @WebMvcTest
To test whether Spring MVC controllers are working as expected, use
the @WebMvcTest
annotation.
- Auto-configured Spring WebFlux Tests : @WebFluxTest
To test that Spring WebFlux controllers are working as expected, you
can use the @WebFluxTest
annotation.
Auto-configured Data JPA Tests : @DataJpaTest
You can use the @DataJpaTest
annotation to test JPA applications.
And you have still many other slice flavors that Spring Boot provides to you.
See the testing part of the documentation to get more details.