Spring Boot test

1. Spring Boot testoverview

Spring Boot providing了全面 testsupport, including单元test, 集成test, API testetc. many 种testclass型.

1.1 testclass型

  • 单元test - test单个component, such asserviceclass, toolclassetc.
  • 集成test - test many 个component之间 交互, such asservice and datalibrary 集成
  • API test - test REST API 端点
  • 端 to 端test - test完整 业务流程

1.2 testtool栈

  • JUnit 5 - testframework
  • Mockito - mockframework
  • Spring Boot Test - Spring Boot testsupport
  • Testcontainers - containerizationtest
  • RestAssured - REST API test

2. 单元testBasics

2.1 添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>

2.2 writing simple 单元test

package com.example.demo.service;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorServiceTest {
    
    private CalculatorService calculatorService = new CalculatorService();
    
    @Test
    void testAdd() {
        int result = calculatorService.add(2, 3);
        assertEquals(5, result, "2 + 3 should equal 5");
    }
    
    @Test
    void testSubtract() {
        int result = calculatorService.subtract(5, 3);
        assertEquals(2, result, "5 - 3 should equal 2");
    }
    
    @Test
    void testMultiply() {
        int result = calculatorService.multiply(2, 3);
        assertEquals(6, result, "2 * 3 should equal 6");
    }
    
    @Test
    void testDivide() {
        int result = calculatorService.divide(6, 3);
        assertEquals(2, result, "6 / 3 should equal 2");
    }
    
    @Test
    void testDivideByZero() {
        assertThrows(ArithmeticException.class, () -> {
            calculatorService.divide(6, 0);
        }, "Division by zero should throw ArithmeticException");
    }
}

2.3 using Mockito formocktest

package com.example.demo.service;

import com.example.demo.repository.UserRepository;
import com.example.demo.entity.User;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void testGetUserById() {
        // 准备testdata
        User mockUser = new User();
        mockUser.setId(1L);
        mockUser.setUsername("admin");
        mockUser.setEmail("admin@example.com");
        
        // configurationmockbehavior
        when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
        
        // 执行test
        User result = userService.getUserById(1L);
        
        // verification结果
        assertNotNull(result);
        assertEquals(1L, result.getId());
        assertEquals("admin", result.getUsername());
        
        // verificationmethod调用
        verify(userRepository, times(1)).findById(1L);
    }
    
    @Test
    void testSaveUser() {
        // 准备testdata
        User userToSave = new User();
        userToSave.setUsername("newuser");
        userToSave.setEmail("newuser@example.com");
        
        User savedUser = new User();
        savedUser.setId(1L);
        savedUser.setUsername("newuser");
        savedUser.setEmail("newuser@example.com");
        
        // configurationmockbehavior
        when(userRepository.save(userToSave)).thenReturn(savedUser);
        
        // 执行test
        User result = userService.saveUser(userToSave);
        
        // verification结果
        assertNotNull(result);
        assertEquals(1L, result.getId());
        
        // verificationmethod调用
        verify(userRepository, times(1)).save(userToSave);
    }
    
    @Test
    void testDeleteUser() {
        // 执行test
        userService.deleteUser(1L);
        
        // verificationmethod调用
        verify(userRepository, times(1)).deleteById(1L);
    }
}

3. Spring Boot 集成test

集成test用于test many 个component之间 交互, 通常需要启动 Spring on under 文.

3.1 basic集成test

package com.example.demo;

import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class UserRepositoryIntegrationTest {
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void testSaveAndFindUser() {
        // creationuser
        User user = new User();
        user.setUsername("testuser");
        user.setEmail("test@example.com");
        user.setPassword("password");
        
        // 保存user
        User savedUser = userRepository.save(user);
        
        // verification保存成功
        assertNotNull(savedUser.getId());
        assertEquals("testuser", savedUser.getUsername());
        
        // finduser
        User foundUser = userRepository.findById(savedUser.getId()).orElse(null);
        
        // verificationfind结果
        assertNotNull(foundUser);
        assertEquals(savedUser.getId(), foundUser.getId());
        assertEquals("testuser", foundUser.getUsername());
    }
    
    @Test
    void testFindByUsername() {
        // creationuser
        User user = new User();
        user.setUsername("uniqueuser");
        user.setEmail("unique@example.com");
        user.setPassword("password");
        userRepository.save(user);
        
        // 按user名find
        User foundUser = userRepository.findByUsername("uniqueuser");
        
        // verification结果
        assertNotNull(foundUser);
        assertEquals("uniqueuser", foundUser.getUsername());
    }
}

3.2 using @DataJpaTest test JPA component

package com.example.demo.repository;

import com.example.demo.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.jdbc.AutoconfigurationTestDatabase;

import static org.junit.jupiter.api.Assertions.*;

@DataJpaTest
@AutoconfigurationTestDatabase(replace = AutoconfigurationTestDatabase.Replace.NONE)
class UserRepositoryTest {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private TestEntitymanagementr entitymanagementr;
    
    @Test
    void testFindByEmail() {
        // using TestEntitymanagementr 插入testdata
        User user = new User();
        user.setUsername("testuser");
        user.setEmail("test@example.com");
        user.setPassword("password");
        entitymanagementr.persist(user);
        entitymanagementr.flush();
        
        // 执行query
        User foundUser = userRepository.findByEmail("test@example.com");
        
        // verification结果
        assertNotNull(foundUser);
        assertEquals("testuser", foundUser.getUsername());
    }
    
    @Test
    void testExistsByUsername() {
        // 插入testdata
        User user = new User();
        user.setUsername("existinguser");
        user.setEmail("existing@example.com");
        user.setPassword("password");
        entitymanagementr.persist(user);
        entitymanagementr.flush();
        
        // test存 in  user名
        boolean exists = userRepository.existsByUsername("existinguser");
        assertTrue(exists);
        
        // test不存 in  user名
        boolean notExists = userRepository.existsByUsername("nonexistentuser");
        assertFalse(notExists);
    }
}

4. REST API test

Spring Boot providing了 many 种方式来test REST API.

4.1 using MockMvc test API

package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestbuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(UserController.class)
class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Test
    void testGetUserById() throws Exception {
        // 准备testdata
        User mockUser = new User();
        mockUser.setId(1L);
        mockUser.setUsername("admin");
        mockUser.setEmail("admin@example.com");
        
        // configurationmockbehavior
        when(userService.getUserById(1L)).thenReturn(mockUser);
        
        // 执行test
        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.username").value("admin"))
            .andExpect(jsonPath("$.email").value("admin@example.com"));
    }
    
    @Test
    void testCreateUser() throws Exception {
        // 准备testdata
        User userToCreate = new User();
        userToCreate.setUsername("newuser");
        userToCreate.setEmail("new@example.com");
        userToCreate.setPassword("password");
        
        User createdUser = new User();
        createdUser.setId(1L);
        createdUser.setUsername("newuser");
        createdUser.setEmail("new@example.com");
        
        // configurationmockbehavior
        when(userService.saveUser(any(User.class))).thenReturn(createdUser);
        
        // 执行test
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(userToCreate)))
            .andExpect(status().isCreated())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.username").value("newuser"));
    }
    
    @Test
    void testUpdateUser() throws Exception {
        // 准备testdata
        User updatedUser = new User();
        updatedUser.setId(1L);
        updatedUser.setUsername("updateduser");
        updatedUser.setEmail("updated@example.com");
        
        // configurationmockbehavior
        when(userService.updateUser(eq(1L), any(User.class))).thenReturn(updatedUser);
        
        // 执行test
        mockMvc.perform(put("/api/users/1")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(updatedUser)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.username").value("updateduser"));
    }
    
    @Test
    void testDeleteUser() throws Exception {
        // 执行test
        mockMvc.perform(delete("/api/users/1"))
            .andExpect(status().isNoContent());
        
        // verificationmethod调用
        verify(userService, times(1)).deleteUser(1L);
    }
}

4.2 using TestRestTemplate test API

package com.example.demo;

import com.example.demo.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {
    
    @LocalServerPort
    private int port;
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    private String getBaseUrl() {
        return "http://localhost:" + port + "/api/users";
    }
    
    @Test
    void testGetAllUsers() {
        ResponseEntity<User[]> response = restTemplate.getForEntity(getBaseUrl(), User[].class);
        
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertNotNull(response.getBody());
    }
    
    @Test
    void testCreateAndGetUser() {
        // creationuser
        User user = new User();
        user.setUsername("testuser");
        user.setEmail("test@example.com");
        user.setPassword("password");
        
        ResponseEntity<User> createResponse = restTemplate.postForEntity(getBaseUrl(), user, User.class);
        assertEquals(HttpStatus.CREATED, createResponse.getStatusCode());
        
        User createdUser = createResponse.getBody();
        assertNotNull(createdUser);
        assertNotNull(createdUser.getId());
        
        // 获取user
        ResponseEntity<User> getResponse = restTemplate.getForEntity(getBaseUrl() + "/" + createdUser.getId(), User.class);
        assertEquals(HttpStatus.OK, getResponse.getStatusCode());
        
        User retrievedUser = getResponse.getBody();
        assertNotNull(retrievedUser);
        assertEquals(createdUser.getId(), retrievedUser.getId());
        assertEquals(createdUser.getUsername(), retrievedUser.getUsername());
    }
    
    @Test
    void testUpdateUser() {
        // 先creationuser
        User user = new User();
        user.setUsername("updatableuser");
        user.setEmail("update@example.com");
        user.setPassword("password");
        
        User createdUser = restTemplate.postForEntity(getBaseUrl(), user, User.class).getBody();
        assertNotNull(createdUser);
        
        // updateuser
        createdUser.setUsername("updateduser");
        createdUser.setEmail("updated@example.com");
        
        HttpHeaders headers = new HttpHeaders();
        HttpEntity<User> requestEntity = new HttpEntity<>(createdUser, headers);
        
        ResponseEntity<User> updateResponse = restTemplate.exchange(
            getBaseUrl() + "/" + createdUser.getId(),
            HttpMethod.PUT,
            requestEntity,
            User.class
        );
        
        assertEquals(HttpStatus.OK, updateResponse.getStatusCode());
        User updatedUser = updateResponse.getBody();
        assertNotNull(updatedUser);
        assertEquals("updateduser", updatedUser.getUsername());
    }
}

5. testbest practices

5.1 test命名规范

  • testclass名: 被testclass名 + Test
  • testmethod名: test + 被testmethod名 + test场景
  • usingdescribes性 method名, such as testSaveUserWithValidData()

5.2 teststructure

@Test
void testMethodName() {
    // Arrange - 准备testdata and environment
    // Act - 执行被testmethod
    // Assert - verification结果
    // Verify - verification交互 (such as果using了mock) 
}

5.3 test覆盖率

using JaCoCo 生成testcoverage report:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.8</version>
    <executions>
        <execution>
            <id>prepare-agent</id>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

runtest并生成报告:

mvn test

5.4 parameter化test

package com.example.demo.service;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.*;

class CalculatorServiceTest {
    
    private CalculatorService calculatorService = new CalculatorService();
    
    @ParameterizedTest
    @CsvSource({
        "2, 3, 5",
        "-1, 1, 0",
        "0, 0, 0",
        "100, 200, 300"
    })
    void testAdd(int a, int b, int expected) {
        int result = calculatorService.add(a, b);
        assertEquals(expected, result, a + " + " + b + " should equal " + expected);
    }
    
    @ParameterizedTest
    @CsvSource({
        "5, 3, 2",
        "0, 0, 0",
        "10, 5, 5",
        "-5, -3, -2"
    })
    void testSubtract(int a, int b, int expected) {
        int result = calculatorService.subtract(a, b);
        assertEquals(expected, result, a + " - " + b + " should equal " + expected);
    }
}

6. advancedtesttechniques

6.1 using Testcontainers forcontainerizationtest

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.18.3</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.18.3</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mysql</artifactId>
    <version>1.18.3</version>
    <scope>test</scope>
</dependency>
package com.example.demo;

import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.junit.jupiter.api.Assertions.*;

@Testcontainers
@SpringBootTest
class UserRepositoryContainerTest {
    
    @Container
    @ServiceConnection
    static MySQLContainer<?> mysqlContainer = new MySQLContainer<>("mysql:8.0")
            .withDatabaseName("testdb")
            .withUsername("testuser")
            .withPassword("testpassword");
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void testSaveAndFindUser() {
        // creationuser
        User user = new User();
        user.setUsername("containertest");
        user.setEmail("container@example.com");
        user.setPassword("password");
        
        // 保存user
        User savedUser = userRepository.save(user);
        
        // verification保存成功
        assertNotNull(savedUser.getId());
        
        // finduser
        User foundUser = userRepository.findById(savedUser.getId()).orElse(null);
        
        // verificationfind结果
        assertNotNull(foundUser);
        assertEquals(savedUser.getUsername(), foundUser.getUsername());
    }
}

6.2 testexceptionprocessing

package com.example.demo.service;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserServiceExceptionTest {
    
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void testGetUserByIdNotFound() {
        // configurationmockbehavior
        when(userRepository.findById(999L)).thenReturn(Optional.empty());
        
        // 执行test并verificationexception
        UserNotFoundException exception = assertThrows(UserNotFoundException.class, () -> {
            userService.getUserById(999L);
        });
        
        assertEquals("User not found with id: 999", exception.getMessage());
    }
    
    @Test
    void testSaveUserWithExistingUsername() {
        // 准备testdata
        User user = new User();
        user.setUsername("existinguser");
        user.setEmail("new@example.com");
        
        // configurationmockbehavior
        when(userRepository.existsByUsername("existinguser")).thenReturn(true);
        
        // 执行test并verificationexception
        UserAlreadyExistsException exception = assertThrows(UserAlreadyExistsException.class, () -> {
            userService.saveUser(user);
        });
        
        assertEquals("Username 'existinguser' already exists", exception.getMessage());
    }
}

6.3 testasynchronousmethod

package com.example.demo.service;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.concurrent.ListenableFuture;

import java.util.concurrent.ExecutionException;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class AsyncServiceTest {
    
    @Autowired
    private AsyncService asyncService;
    
    @Test
    void testAsyncMethod() throws ExecutionException, InterruptedException {
        // 执行asynchronousmethod
        ListenableFuture<String> future = asyncService.performAsyncTask("test");
        
        // 获取结果 (会阻塞直 to completion) 
        String result = future.get();
        
        // verification结果
        assertEquals("completed: test", result);
    }
    
    @Test
    void testCompletableFuture() throws Exception {
        // 执行asynchronousmethod
        CompletableFuture<String> future = asyncService.performCompletableFutureTask("test");
        
        // using CompletableFuture API
        String result = future.thenApply(s -> s.toUpperCase())
                             .get();
        
        // verification结果
        assertEquals("COMPLETED: TEST", result);
    }
}

7. runtest

7.1 using Maven runtest

# run所 has test
mvn test

# runspecifictestclass
mvn test -Dtest=UserServiceTest

# runspecifictestmethod
mvn test -Dtest=UserServiceTest#testGetUserById

# runtest并生成coverage report
mvn test jacoco:report

# 跳过test
mvn package -DskipTests

# 只编译test, 不run
mvn test-compile

7.2 using Gradle runtest

# run所 has test
gradle test

# runspecifictestclass
gradle test --tests UserServiceTest

# runspecifictestmethod
gradle test --tests UserServiceTest.testGetUserById

# 生成coverage report
gradle jacocoTestReport

# 跳过test
gradle build -x test

7.3 in IDE inruntest

  • IntelliJ IDEA - right 键点击testclass or method, 选择 "Run"
  • Eclipse - right 键点击testclass or method, 选择 "Run As" > "JUnit Test"
  • VS Code - installation Java Test Runner scale, 点击test旁edge run按钮

8. testbest practicessummarized

  • writing清晰, 简洁 test用例
  • test应该 is 独立 , 不依赖于othertest
  • usingdescribes性 testmethod名
  • 覆盖正面 and 负面场景
  • usingmockobject隔离被testcomponent
  • 保持testcode readable 性
  • 定期runtest
  • usingcontinuous integrationtool自动runtest
  • 追求合理 test覆盖率, 而不 is 100%
  • test应该 fast 速run