Integration tests assume that you have a Spring Application context, or even a live web application with endpoints you can hit. Oftentimes we will have components that only make sense to test with a live Spring ApplicationContext.
The easiest test you can write is something like this:
@SpringBootTest
public class MyIT {
@Test
public void gotAnAppctx(ApplicationContext ctx) {
assertNotNull(ctx);
}
}
This has some key advantages:
-
Easy to write your first few tests before you iron out what you’re trying to accomplish and why
-
Likely Just Works™
-
Usually the exact same environment at runtime and test time, easier to configure
-
Super easy to test a full flow from end to end
-
Good starting point allowing you to iterate quickly
There are also some disadvantages:
-
Slow as it can start up the entirety of Spring Boot’s AutoConfiguration ecosystem. This test only ensures that the ApplicationContext
is injected; this can be done faster (as written, this takes 10s to run in a non-trivial application like the Auth Services)
-
Easier for your test to fail in an esoteric way that doesn’t really inform the maintainer why it’s failing (although targeted configuration can have this effect too)
-
Initializes a lot of extra things that you are not trying to test
A faster test would only use @ContextConfiguration
to do the same thing:
@ContextConfiguration
@ExtendWith(SpringExtension.class)
public class MyIT {
@Test
public void gotAnAppctx(ApplicationContext ctx) {
assertNotNull(ctx);
}
}
This test runs in 1s and still provides the exact same verification. Faster is better. But, there are plenty of legitimate cases to use @SpringBootTest
, so don’t feel like you have to completely avoid it.
Configuration Slicing
Spring Boot ships a fair amount of annotations designed to only start up exactly what you need. Generally, integration testing is usually silo’d to something like "the controller layer" or "the data layer". Here’s an example:
@DataJpaTest
public class DataIT {
@Test
public void queryMethodWorks(SomeRepository repo) {
repo.findByNameIgnoreCase("A Name");
}
}
Note the lack of @SpringBootTest
on this test. This test only boots up exactly what you would need within the Spring ApplicationContext
to perform a @DataJpaTest
, which excludes anything in the controller layer, like Spring MVC.
If you only want to boot up the controller layer to do some testing with MockMVC:
@WebMvcTest
public class NavigationEndpointTest {
@Autowired
MockMvc mockMvc;
@Test
public void requiresAuthentication() throws Exception {
mockMvc.perform(get("/navigation")).andDo(print())
.andExpect(status().isUnauthorized());
}
}
However, this test could be better by further restricting the controller to test in @WebMvcTest(NavigationEndpoint.class)
since it will run faster (assuming there are other controllers in your project).
Spring has a number of other of these types of 'Configuration Slicing' annotations in the spring-boot-test-autoconfigure
, which is automatically brought in via spring-boot-starter-test
.
Configuration Slicing vs @SpringBootTest
The next obvious question is "Okay, well which annotation should I be using for my test?" Generally, software development is done in 3 phases:
-
Make it correct and easy to read your code; naive solutions,
-
Optimize for performance
-
GOTO 1
Practically, this can be applied to tests as well. Likely an @SpringBootTest
is the best starting point, and then tests can be optimized from there. However, if you know all you are going to test is an @Controller
, consider starting with @WebMvcTest
and expanding as you need it.
Configuration Slicing with @SpringBootTest
There are some slice annotations that use a different bootstrapper than the SpringBootTestContextBootstrapper.class
. If this happens, the normal Spring Boot Autoconfiguration may or may not be loaded. An example is the @WebMvcTest
. This uses a WebContextBootstrapper
which is similar but not the exact same. Therefore, it might make more sense to use this:
@SpringBootTest
@AutoConfigureMockMvc
public class MyIT { }
Read the Javadocs for the annotations you are using as they usually have descriptions and where and how to use them.
What if I don’t have an @SpringBootApplication
in src/main/java
?
In general, adding an @SpringBootApplication
as either a static inner class of your test or something that can be referred to from multiple tests solely in src/test/java
is fine. However, a well-optimized test might not have to include any @SpringBootApplication
to run at all.
Customizing Configuration Slicing
When you start to use configuration slicing, you might notice that your configuration classes that hook up other functionality are not included, causing missing beans, etc. The issue is that Spring does not yet know about your classes
If we look at the @WebMvcTest
annotation, you will see the following Spring annotations:
@BootstrapWith(WebMvcTestContextBootstrapper.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(WebMvcTypeExcludeFilter.class)
@AutoConfigureCache
@AutoConfigureWebMvc
@AutoConfigureMockMvc
@ImportAutoConfiguration
public @interface WebMvcTest {
Going further, @AutoConfigureWebMvc
is annotated like this:
@ImportAutoConfiguration
public @interface AutoConfigureWebMvc {
@ImportAutoConfiguration
is the magic that reads out of META-INF/spring.factories
and allows additional classes to participate in Spring Boot’s auto configuration. In the naked version of the annotation, this will look for keys in META-INF/spring.factories
that correspond to the fully-qualified class name of the class it resides on, so org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc
:
org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc=\
will.be.an.AutoConfigurationClass
Practical Example
Let’s say you have this example:
// JpaConfig.java
package com.broadleafcommerce.catalog.provider.jpa;
@Configuration
@EnableJpaRepositories(repositoryFactoryBeanClass = JpaTrackableRepositoryFactoryBean.class)
public class JpaConfig { }
==================
// JpaProductRepository.java
package com.broadleafcommerce.catalog.provider.jpa.repository;
@Repository
@Narrow(JpaNarrowExecutor.class)
public interface JpaProductRepository extends ProductRepository<JpaProduct> { }
You were a good citizen and made sure this was in a normal src/main/resources/META-INF/spring.factories
to participate in normal AutoConfiguration:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.broadleafcommerce.catalog.provider.jpa.JpaConfig
Because of that, we can write a test that does this:
@SpringBootTest
public class ProductRepoIT {
@Test
public void gotARepo(JpaProductRepository repo) {
assertNotNull(repo);
}
}
@DataJpaTest
public class ProductRepoIT {
@Test
public void gotARepo(JpaProductRepository repo) {
// failed ApplicationContext startup
assertNotNull(repo);
}
}
@DataJpaTest
is annotated like this:
@BootstrapWith(DataJpaTestContextBootstrapper.class)
@ExtendWith({SpringExtension.class})
@OverrideAutoConfiguration(
enabled = false
)
@TypeExcludeFilters({DataJpaTypeExcludeFilter.class})
@Transactional
@AutoConfigureCache
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@ImportAutoConfiguration
public @interface DataJpaTest {
Which means we can add an entry in src/test/resources/META-INF/spring.factories
under the AutoConfigureDataJpa
fully qualified class name key to participate in this "slice":
org.springframework.boot.test.autoconfigure.data.jpa.AutoConfigureDataJpa=\
com.broadleafcommerce.catalog.provider.jpa.JpaConfig
Avoiding AutoConfiguration
There is an argument for ignoring the complexity in spring.factories
of the autoconfiguration altogether. The previous example can manually @Import
the @Configuration
class to make it arguably simpler to grok:
@DataJpaTest
@Import(JpaConfig.class)
public class ProductRepoIT {
@Test
public void gotARepo(JpaProductRepository repo) {
// success!
assertNotNull(repo);
}
}