Using Appropriate Annotations in Spring Testing
Summary of the annotations we should use in each testing scenario using JUnit 5 and Spring Test Framework to eliminate redundant annotations and negative outputs.
Join the DZone community and get the full member experience.
Join For Free1. Introduction
There's heavy use of annotations in Spring Framework. This makes confusion about what annotations should be used especially in tests. It sometimes ends up with adding redundant annotations or the tests not working as expected. On StackOverflow, you can easily find the developers who make mistakes with Spring annotations or someone who makes things work but does not fully understand why.
To make things clear, in this article, I would summarize the annotations we should use in each testing scenario using JUnit 5 and Spring Test Framework.
2. Spring Framework and Spring Boot
Before looking into the Spring test, we need to know how Spring works and the Spring Framework and Spring Boot relationship.
Spring is the most popular application framework of Java. It simplifies the Java EE development by providing a dependency injection feature and supports many popular technologies such as Spring JDBC, Spring MVC, Spring Test.
To start a Spring application, you need to create an ApplicationContext which is an IoC container of all bean objects used in the application. Here is an example:
@Configuration
// This is the primary configuration class of the application. From here, Spring
// will scan all declared components and make them available in ApplicationContext.
// We can use @Import(OtherConfig.class) to import other configurations
// into the primary one.
// We can also use @ComponentScans (e.g. @ComponentScans("services"))
// to ask Spring to scan for all components (e.g. classes having @Component, @Service)
// in a package and add them into the service container (i.e. ApplicationContext)
public class Config {
@Bean
public HelloService helloService() {
// assume we have HelloService interface and HelloServiceImpl class
return new HelloServiceImp();
}
}
public class MainApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Config.class);
// Use container API to get back the service we need
var helloService = context.getBean(HelloService.class);
System.out.print(helloService.hi());
}
}
From the example, above you can foresee that we need to do many manual steps to get a Spring application up and running, especially the enterprise application with many external dependencies such as DB, message queue, third-party APIs.
Spring Boot makes things easier by doing all auto configurations for us. Here is the code for the same example in Spring Boot:
@SpringBootApplication
public class MainApplication {
@Bean
public HelloService helloService() {
return new HelloServiceImp();
}
public static void main(String[] args) {
Application context = SpringApplication.run(MainApplication.class);
var helloService = context.getBean(HelloService.class);
System.out.println(helloService.hi());
}
}
Looking into SpringBootApplication annotation you can see that there is a meta-annotation @SpringBootConfiguration which again includes @Configuration. That explains why Spring can still find the primary configuration class and load HelloService bean with `@SpringBootApplication`.
Spring tends to group multiple annotations into one to make things simpler. This grouping generates many new annotations which sometimes makes developers confused and add redundant annotations (e.g. adding both @SpringBootApplication and @Configuration to the same class)
Visit the following links in case you want to learn more about Spring Framework and Spring Boot:
Overall, we need to have 1 primary configuration to create an ApplicationContext for any Spring application.
3. Write a Basic Unit Test in Spring Framework
Following is the most basic setup to write a test in Spring:
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes={TestConfig.class})
public class TodoServiceTest {
@Autowired
private TodoService todoService;
// ...
}
With this code, a Spring ApplicationContext will be created using TestConfig as the primary configuration. It then gets the TodoService instance from the container and injects it into the test class. This matches with what we discussed so far: Spring application needs to create ApplicationContext using 1 primary configuration.
There's a short form for the setup above which if you look inside the @SpringJUnitConfig you can see 2 meta-annotations: @ExtendWith and @ContextConfiguration
@SpringJUnitConfig(TestConfig.class)
public class TodoServiceTest {
// ...
}
If the configuration class is not specified, Spring will look for configuration embedded in the test class
@SpringJUnitConfig
public class TodoServiceTest {
@Configuration
static class TestConfig {
@Bean
public TodoService todoService() {
// ...
}
}
// ...
}
Notice that, in both cases, ApplicationContext is instantiated only once and shared among all the test methods in the class.
Using @TestPropertySource to pass in custom properties for testing. Has higher precedence than sources
@SpringJUnitConfig(TestConfig.class)
@TestPropertySource(properties={"username=foo", "password=bar"},
locations="classpath:todo-test.properties")
public class TodoServiceTests {
// ...
}
Please click on this link to view the full code.
4. Testing with Spring Boot
Spring Boot introduces new annotations. The followings are some popular ones:
- @SpringBootTest
- @WebMvcTest
- @DataJpaTest, @DataJdbcTest, @JdbcTest
- @MockBean
4.1. Integration Test with @SpringBootTest
Unlike @SpringJUnitConfig, @SpringBootTest, by default, starts the whole application context the same as when you run your Spring Boot application. With this annotation, Spring will search for a class with @SpringBootConfiguration and use it as the primary configuration to create ApplicationContext. It also does the auto-configuration for TestRestTemplate which we can wire into the test class and use to call APIs. Following is an example:
@SpringBootApplication // includes meta annotation @SpringBootConfiguration
public class SpringtestingApplication {
// ...
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TodoControllerTest {
@Autowired
TestRestTemplate restTemplate;
// ...
}
Please click on this link to view the full code.
4.2. Unit test for controller layer with @WebMvcTest
Tests with @WebMvcTest will apply only configuration relevant to MVC tests. The full configuration will be disabled. Spring Test Framework also auto-configures MockMvc which we can inject into the test class and use to call tested APIs.
@WebMvcTest(TodoController.class)
public class TodoControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private TodoService todoService;
// ...
}
Please click on this link to view the full code.
5. Conclusion
To summarize:
- Use @SpringJUnitConfig to write unit-test
- Use @WebMvcTest with MockMvc to write unit-test for controller layer
- Use @SpringBootTest with TestRestTemplate to write integration-test for Spring Boot application
Published at DZone with permission of Tho Luong. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments