preface
This article is based on JUnit5. I hope you can learn how to test your own Spring Boot projects and how to pay attention to the testability of your code.
Spring Boot UnitTest
Spring Boot provides many useful tools and annotations to help us complete unit testing, including two modules: Spring-boot-test and Spring-boot-test-autoconfigure. We can introduce these two modules by relying on spring-boot-starter-test, which includes JUnit Jupiter, AssertJ, Hamcrest,Mockito, and other useful unit testing tools.
A simple example
@SpringBootTest
class UserServiceTest {
@Autowired
UserService UserService;
@Test
void findUserById(a){
User user = UserService.findUserById(3); Assertions.assertNotNull(user); }}Copy the code
One of the simplest SpringBoot unit tests is to annotate the test class with @SpringBooTtest annotations, inject UserService with @Autowired, and assert the result with Assertions.
@SpringBootTest
This annotation creates an ApplicationContext to provide a context for the test, so in the example above we can use @autowired to inject UserService. The annotation provides several properties for the user to do some custom configuration, such as:
String[] properties
andString[] value
Properties and value are aliases of each other. Make some configuration for the test environment. For example, set the web environment to reactive:
@SpringBootTest(properties = "spring.main.web-application-type=reactive")
class MyWebFluxTests {
// ...
}
Copy the code
String[] args
Introduce some parameters to the test program, such as:
@SpringBootTest(args = "--app.test=one")
class MyApplicationArgumentTests {
@Test
void applicationArgumentsPopulated(@Autowired ApplicationArguments args) {
assertThat(args.getOptionNames()).containsOnly("app.test");
assertThat(args.getOptionValues("app.test")).containsOnly("one"); }}Copy the code
Class<? >[] classes
A common way to inject a Test bean is to create a Test Spring Boot Boot class and inject it into the Test environment
SpringBootTest.WebEnvironment webEnvironment
Set the Test Web environment, which is an enumeration type, with the following parameters:
MOCK
This is the default option, which loads a Web ApplicationContext and provides a mock Web environment. The built-in container does not start.
RANDOM_PORT
Loads a WebServerApplicationContext and provide a real web environment, the built-in container will start and monitor a random port.
DEFINED_PORT
Loads a WebServerApplicationContext and provide a real web environment, the built-in container will start and monitor a custom port (8080 by default).
NONE
Loading an ApplicationContext does not provide any Web environment.
Note: Using the @Transactional annotation in the test can roll back transactions after the test is complete, but RANDOM_PORT and DEFINED_PORT provide a real Web environment that does not roll back transactions after the test is complete.
Layered testing and code testability
Layered testing
The above example is just a simple example. It is obvious that the test belongs to the service layer of a 3-tier architecture. What is layered testing?
Hierarchical testing, as the name suggests, is to write unit tests for each layer of the program. Although it takes more time to write unit tests, it can greatly ensure the stability of the code and locate bugs. If every test starts at the Controller layer, some underlying problems may be difficult to find. Therefore, it is recommended that you try to do hierarchical testing when writing unit tests.
Code testability
Code testability is simply how easy it is to write unit tests. If you find your code difficult to write unit tests, consider whether your code can still be optimized. Common test unfriendly code includes:
- Abuse of mutable global variables
- Abuse of static methods
- Use complex inheritance relationships
- Highly coupled code
Storage level test
In the case of Spring-data-JDBC, only the repository layer code is tested. The @datajdbctest annotation configes a built-in in-memory database and injects JdbcTemplate and Spring DataJdbc repositories. Other unnecessary components such as the Web layer are not introduced.
@DataJdbcTest
class UserMapperTest {
@Autowired
UserMapper userMapper;
@Test
public void test(a){
userMapper.findById(1L) .ifPresent(System.out::println); }}Copy the code
Web layer test
@ @ WebMvcTest annotation automatically scanning Controller, @ ControllerAdvice, @ JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebM Inject MockMvc vcConfigurer and HandlerMethodArgumentResolver and automatically, we can use MockMvc test on our web.
Data:
insert into USERS(`username`, `password`)
values ('1', '111'),
('2', '222'),
('3', '333'),
('4', '444'),
('5', '555');
Copy the code
Controller layer code:
@RestController
@RequestMapping("/users")
@AllArgsConstructor
@Slf4j
public class UserController {
final UserService userService;
@GetMapping("/{id}")
public User findById(@PathVariable long id){
returnuserService.findUserById(id); }}Copy the code
Test code:
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
MockMvc mvc;
@MockBean
UserService userService;
@BeforeEach
public void mock(a){
when(userService.findUserById(anyLong())).thenReturn(User.builder().username("test").password("test").build());
}
@Test
void exampleTest(a) throws Exception {
mvc.perform(get("/users/1")) .andExpect(status().isOk()) .andDo(print()); }}Copy the code
After execution, the result is:
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"timestamp":0."status":0."message":null."data": {"id":0."username":"test"."password":"test"}}
Forwarded URL = null
Redirected URL = null
Cookies = []
Copy the code
You can see that @webMvctest automatically injects MockMvc for us, but it doesn’t inject UserService, so we need to mock UserService, and in our mock() method we set up to return a Test object for any incoming Long, So we’re not going to get an object whose id is equal to 1.
Test the entire application
After the hierarchical testing is complete we may need to do a holistic test from top to bottom to verify the availability of the entire process, injecting the ApplicationContext of the entire test environment with @SpringBooTtest annotations and introducing MockMvc with @AutoConfiguRemockMVC.
The difference between @AutoconfiguRemockMVC and @webMvcTest is that @AutoConfiguRemockMVC simply infuses MockMvc while @WebMvcTest also introduces the ApplicationContext in the Web layer (note that it’s just w Eb layer context, which is why we need to mock other components.
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Test
void exampleTest(@Autowired MockMvc mvc) throws Exception {
mvc.perform(get("/users/1")) .andExpect(status().isOk()) .andDo(MockMvcResultHandlers.print()); }}Copy the code
Timestamp, status, message and so on are automatically added, so you can ignore them.
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"timestamp":0,"status":0,"message":null,"data":{"id":1,"username":"1","password":"111"}}
Forwarded URL = null
Redirected URL = null
Cookies = []
Copy the code
You can see that in the Body we get an object with id = 1, which means we got real data.