教程中心 / Java教程 / 单元测试

单元测试

20分钟 高级特性

引言:什么是单元测试?

单元测试(Unit Testing)是对软件中的最小可测试单元进行检查和验证,确保每个单元都按预期工作。

单元测试类比:汽车生产

汽车生产前对零件进行测试:

发动机测试 -> 测试发动机是否能正常工作
轮胎测试 -> 测试轮胎承压能力
刹车测试 -> 测试刹车响应距离

单元测试就像对这些零件的测试:
- 测试粒度小(一个方法、一个类)
- 测试速度快
- 测试结果准确
- 确保每个零件都没问题
- 最终组装才能正常运行!

为什么需要单元测试?

保证代码质量:及时发现bug,减少线上问题
提高重构信心:修改代码后快速验证是否破坏原有功能
文档作用:测试代码就是最好的接口使用文档
促进设计:写出可测试的代码通常意味着更好的设计

JUnit 5注解

JUnit 5是目前最新的JUnit版本,由三个模块组成:JUnit Platform + JUnit Jupiter + JUnit Vintage。

核心注解

注解说明
@Test标记测试方法(注意:来自org.junit.jupiter.api)
@BeforeEach每个测试方法执行前调用
@AfterEach每个测试方法执行后调用
@BeforeAll所有测试方法执行前调用一次
@AfterAll所有测试方法执行后调用一次
@Disabled禁用测试方法或类
@DisplayName自定义测试显示名称

测试生命周期

@BeforeAll   -> 执行一次(所有测试之前)
    ↓
@BeforeEach   -> 执行多次(每个测试之前)
    ↓
@Test         -> 测试方法1
    ↓
@AfterEach    -> 执行多次(每个测试之后)
    ↓
@BeforeEach   -> 执行多次(每个测试之前)
    ↓
@Test         -> 测试方法2
    ↓
@AfterEach    -> 执行多次(每个测试之后)
    ↓
@AfterAll     -> 执行一次(所有测试之后)

常用断言

断言(Assertion)用于验证测试结果是否符合预期。

基本断言

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

// 相等断言
assertEquals(expected, actual);
assertEquals(expected, actual, "消息");

// 布尔断言
assertTrue(condition);
assertFalse(condition);

// 空值断言
assertNull(object);
assertNotNull(object);

// 数组断言
assertArrayEquals(expectedArray, actualArray);

异常断言

// 方式1:使用assertThrows
assertThrows(ArithmeticException.class, () -> {
    int result = 10 / 0;
});

// 方式2:使用assertThrows获取异常并验证
ArithmeticException ex = assertThrows(ArithmeticException.class, () -> {
    calculator.divide(1, 0);
});
assertEquals("除数不能为零", ex.getMessage());

// 方式3:assertDoesNotThrow(不抛出异常)
assertDoesNotThrow(() -> calculator.divide(10, 2));

超时断言

import org.junit.jupiter.api.assertTimeout;
import java.time.Duration;

assertTimeout(Duration.ofSeconds(1), () -> {
    // 应该在1秒内完成
    Thread.sleep(100);
});

组合断言

// allOf:所有断言都通过才算通过
assertAll("用户信息验证",
    () -> assertNotNull(user.getName()),
    () -> assertTrue(user.getAge() >= 18),
    () -> assertEquals("email@test.com", user.getEmail())
);

完整测试示例

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

@DisplayName("计算器测试")
class CalculatorTest {

    private Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
        System.out.println("每个测试前执行");
    }

    @AfterEach
    void tearDown() {
        System.out.println("每个测试后执行");
    }

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

    @AfterAll
    static void afterAll() {
        System.out.println("所有测试后执行一次");
    }

    @Test
    @DisplayName("加法测试")
    void testAdd() {
        int result = calculator.add(2, 3);
        assertEquals(5, result, "2+3应该等于5");
    }

    @Test
    @DisplayName("减法测试")
    void testSubtract() {
        assertEquals(5, calculator.subtract(10, 5));
        assertEquals(-1, calculator.subtract(4, 5));
    }

    @Test
    @DisplayName("乘法测试")
    void testMultiply() {
        assertEquals(6, calculator.multiply(2, 3));
        assertEquals(0, calculator.multiply(0, 100));
    }

    @Test
    @DisplayName("除法测试")
    void testDivide() {
        assertEquals(2, calculator.divide(10, 5));
    }

    @Test
    @DisplayName("除数为零应抛出异常")
    void testDivideByZero() {
        ArithmeticException ex = assertThrows(ArithmeticException.class, () -> {
            calculator.divide(1, 0);
        });
        assertEquals("除数不能为零", ex.getMessage());
    }

    @Disabled("暂时禁用此测试")
    @Test
    @DisplayName("被禁用的测试")
    void disabledTest() {
        fail("这个测试不应该被执行");
    }
}

参数化测试

JUnit 5支持参数化测试,用不同参数多次运行同一测试。

Maven依赖

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>

参数化测试示例

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

@ParameterizedTest
@ValueSource(ints = {0, 1, 2, 10, 100})
@DisplayName("验证正整数加0等于自身")
void testAddZero(int number) {
    Calculator calc = new Calculator();
    assertEquals(number, calc.add(number, 0));
}

@ParameterizedTest
@CsvSource({
    "1, 2, 3",
    "10, 20, 30",
    "100, 200, 300"
})
@DisplayName("加法参数化测试")
void testAddParameterized(int a, int b, int expected) {
    Calculator calc = new Calculator();
    assertEquals(expected, calc.add(a, b));
}

Mock对象(Mockito)

使用Mockito可以创建Mock对象,模拟依赖的外部服务。

Maven依赖

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.7.0</version>
    <scope>test</scope>
</dependency>

Mockito基本用法

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

class UserServiceTest {

    @Test
    void testGetUser() {
        // 1. 创建Mock对象
        UserRepository mockRepo = Mockito.mock(UserRepository.class);

        // 2. 设置Mock行为
        User expectedUser = new User(1L, "张三");
        when(mockRepo.findById(1L)).thenReturn(expectedUser);

        // 3. 使用Mock对象
        UserService service = new UserService(mockRepo);
        User user = service.getUserById(1L);

        // 4. 验证结果
        assertEquals("张三", user.getName());

        // 5. 验证方法调用(可选)
        verify(mockRepo, times(1)).findById(1L);
    }

    @Test
    void testSaveUser() {
        UserRepository mockRepo = Mockito.mock(UserRepository.class);
        UserService service = new UserService(mockRepo);

        User newUser = new User(null, "李四");
        User savedUser = new User(2L, "李四");

        when(mockRepo.save(newUser)).thenReturn(savedUser);

        User result = service.createUser(newUser);

        assertNotNull(result.getId());
        assertEquals("李四", result.getName());
        verify(mockRepo, times(1)).save(newUser);
    }
}

常用Mockito方法

方法说明
mock(Class)创建Mock对象
when().thenReturn()设置方法返回值
when().thenThrow()设置方法抛出异常
verify(mock).method()验证方法被调用
times(n)验证调用次数
never()验证从未调用
any()匹配任意参数

测试最佳实践

每个测试只验证一个行为:测试应该独立、快速
测试命名清晰:使用@DisplayName或清晰的方法名
Arrange-Act-Assert模式:准备 -> 执行 -> 验证
测试真实的异常情况:边界条件、null值、异常
避免测试逻辑:测试不应该包含复杂的if-else

测试命名建议

// 好命名
@Test
void testAdd_NegativeNumbers_ReturnsNegativeSum() {}

@Test
void testFindById_WhenUserExists_ReturnsUser() {}

@Test
void testWithdraw_WhenBalanceInsufficient_ThrowsException() {}

// 不好命名
@Test
void test1() {}
@Test
void testAdd() {}
@Test
void testFind() {}

小结

  • 单元测试验证最小可测试单元的正确性
  • @Test标记测试方法,@BeforeEach/AfterEach处理生命周期
  • 使用断言验证结果:assertEquals、assertThrows等
  • @Disabled可临时禁用测试
  • 参数化测试用不同参数多次运行测试
  • Mockito用于模拟依赖的外部对象
  • 测试应遵循Arrange-Act-Assert模式
☕ Java 在线代码编辑器
📝 运行结果