强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Java 完全指南 / 24 - 测试:JUnit 5、Mockito、Testcontainers

24 - 测试:JUnit 5、Mockito、Testcontainers

JUnit 5 基础

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

class CalculatorTest {
    private Calculator calc;

    @BeforeAll
    static void initAll() {
        System.out.println("所有测试前执行一次");
    }

    @BeforeEach
    void setUp() {
        calc = new Calculator();
    }

    @Test
    void testAdd() {
        assertEquals(5, calc.add(2, 3));
        assertNotEquals(6, calc.add(2, 3));
    }

    @Test
    void testDivide() {
        assertEquals(2.5, calc.divide(5, 2), 0.001);
        assertThrows(ArithmeticException.class, () -> calc.divide(1, 0));
    }

    @Test
    @DisplayName("测试字符串非空")
    void testString() {
        String result = calc.format(42);
        assertNotNull(result);
        assertTrue(result.contains("42"));
        assertAll("格式化",
            () -> assertNotNull(result),
            () -> assertFalse(result.isEmpty())
        );
    }

    @Test
    @Disabled("待修复")
    void disabledTest() { }

    @ParameterizedTest
    @CsvSource({"1,2,3", "0,0,0", "-1,1,0", "100,200,300"})
    void testAddParameterized(int a, int b, int expected) {
        assertEquals(expected, calc.add(a, b));
    }

    @ParameterizedTest
    @ValueSource(ints = {2, 4, 6, 8})
    void testEven(int n) {
        assertEquals(0, n % 2);
    }

    @AfterEach
    void tearDown() { }

    @AfterAll
    static void tearDownAll() { }
}

JUnit 5 断言

方法说明
assertEquals(expected, actual)相等
assertNotEquals不相等
assertTrue / assertFalse布尔断言
assertNull / assertNotNull空断言
assertThrows(Exception, Executable)异常断言
assertAll(Executable...)组合断言(全部执行)
assertTimeout(Duration, Executable)超时断言
assertIterableEquals集合断言

Mockito

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock
    private UserRepository userRepository;

    @Mock
    private EmailService emailService;

    @InjectMocks
    private UserService userService;

    @Captor
    private ArgumentCaptor<User> userCaptor;

    @Test
    void testRegister() {
        // Stubbing
        when(userRepository.existsByEmail("[email protected]")).thenReturn(false);
        when(userRepository.save(any(User.class))).thenAnswer(inv -> {
            User u = inv.getArgument(0);
            u.setId(1L);
            return u;
        });

        // 执行
        User result = userService.register("张三", "[email protected]");

        // 验证
        assertNotNull(result);
        assertEquals("张三", result.getName());

        verify(userRepository).save(userCaptor.capture());
        assertEquals("[email protected]", userCaptor.getValue().getEmail());

        verify(emailService).sendWelcome("[email protected]");
        verify(emailService, never()).sendError(any());
    }

    @Test
    void testDuplicateEmail() {
        when(userRepository.existsByEmail("[email protected]")).thenReturn(true);
        assertThrows(BusinessException.class,
            () -> userService.register("测试", "[email protected]"));
    }
}

Mockito 方法速查

方法说明
mock(Class)创建 Mock 对象
when(...).thenReturn(...)配置返回值
when(...).thenThrow(...)配置抛异常
when(...).thenAnswer(...)动态返回
verify(mock).method(...)验证方法调用
verify(mock, times(2))调用次数
verify(mock, never())从未调用
any() / anyString()参数匹配器
spy(object)部分 Mock

Spring Boot 测试

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIntegrationTest {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    void testGetUser() throws Exception {
        when(userService.findById(1L))
            .thenReturn(Optional.of(new User(1L, "张三", "[email protected]")));

        mockMvc.perform(get("/api/users/1")
                .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("张三"));
    }

    @Test
    void testCreateUser() throws Exception {
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"name": "新用户", "email": "[email protected]"}
                    """))
            .andExpect(status().isCreated());
    }
}

Testcontainers

import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@SpringBootTest
@Testcontainers
class UserRepositoryTest {
    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

    @DynamicPropertySource
    static void configure(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", mysql::getJdbcUrl);
        registry.add("spring.datasource.username", mysql::getUsername);
        registry.add("spring.datasource.password", mysql::getPassword);
    }

    @Autowired
    private UserRepository userRepository;

    @Test
    void testSaveAndFind() {
        User user = new User("张三", "[email protected]");
        User saved = userRepository.save(user);
        assertNotNull(saved.getId());

        Optional<User> found = userRepository.findById(saved.getId());
        assertTrue(found.isPresent());
        assertEquals("张三", found.get().getName());
    }
}

测试金字塔

         /  E2E  \          少量
        / 集成测试 \         适量
       /  单元测试  \        大量
类型工具速度成本
单元测试JUnit + Mockito毫秒级
集成测试Spring Boot Test + Testcontainers秒级
E2E 测试Selenium / Playwright分钟级

⚠️ 注意事项

  1. 测试类命名XxxTestXxxTests
  2. 测试方法命名methodName_scenario_expectedResult
  3. 每个测试独立 — 不依赖执行顺序。
  4. Mock 不要过度 — 只 Mock 外部依赖,不要 Mock 被测类本身。

💡 技巧

  1. @DataJpaTest 只测试 JPA 层 — 自动配置嵌入式数据库。
  2. @WebMvcTest 只测试控制器 — 不启动完整上下文。
  3. AssertJ 流畅断言
    assertThat(users).hasSize(3).extracting("name").contains("张三", "李四");
    

🏢 业务场景

  • CI/CD 流水线: 每次提交自动运行单元和集成测试。
  • 回归测试: 确保新功能不破坏已有逻辑。
  • TDD: 测试驱动开发,先写测试再实现。

📖 扩展阅读