Skip to content

单元测试核心思想:FIRST 原则

测试是保障代码质量的最后一道防线。

很多人写代码很认真,但测试就随便写两个 assert 完事。等线上出了 bug,才发现那个没测到的边界条件才是罪魁祸首。

测试和代码一样,需要认真对待。

什么是单元测试

单元测试是对软件的最小可测试单元进行验证。在 Java 里,通常是对类的方法进行测试。

单元测试 vs 集成测试

对比项单元测试集成测试
测试范围单个类/方法多个组件协作
依赖Mock 外部依赖使用真实依赖
执行速度毫秒级秒到分钟
稳定性中(依赖环境)
定位问题精确模糊

FIRST 原则

F:Fast(快速)

慢的测试会被开发忽略:

java
// ❌ BAD:包含耗时操作的测试
@Test
void testBad() {
    Thread.sleep(5000);  // 模拟网络延迟 5 秒
    // 测试代码
}

// ✅ GOOD:用 Mock 替代真实调用
@Test
void testGood() {
    when(httpClient.get("http://example.com")).thenReturn(mockResponse);
    // 测试代码,毫秒级完成
}

I:Independent(独立)

测试之间不能有依赖,一个挂了不影响另一个:

java
// ❌ BAD:测试之间共享状态
class BadTest {
    private final List<String> list = new ArrayList<>();

    @Test
    void testAdd() {
        list.add("item");  // 修改了共享状态
        assertEquals(1, list.size());
    }

    @Test
    void testContains() {
        // 依赖 testAdd 先执行,不可靠
        assertTrue(list.contains("item"));
    }
}

// ✅ GOOD:每个测试独立创建自己的数据
class GoodTest {
    @Test
    void testAdd() {
        List<String> list = new ArrayList<>();  // 测试自己持有
        list.add("item");
        assertEquals(1, list.size());
    }

    @Test
    void testContains() {
        List<String> list = new ArrayList<>();  // 独立创建
        assertFalse(list.contains("item"));  // 不依赖其他测试
    }
}

R:Repeatable(可重复)

测试应该每次都跑出相同的结果:

java
// ❌ BAD:依赖外部状态
@Test
void testBad() {
    File file = new File("/tmp/test.txt");  // 文件可能不存在
}

// ✅ GOOD:创建临时文件,用完即删
@Test
void testGood() {
    Path tempFile = Files.createTempFile("test", ".txt");
    try {
        // 测试代码
    } finally {
        Files.deleteIfExists(tempFile);
    }
}

S:Self-Validating(自验证)

测试能自己判断通过还是失败:

java
// ❌ BAD:需要人工判断
@Test
void testBad() {
    System.out.println("检查输出是否正确");
    // 人来判断?自动化测试的意义何在?
}

// ✅ GOOD:用断言
@Test
void testGood() {
    assertEquals(4, 2 + 2);  // 自动化判断
}

T:Timely(及时)

TDD 主张测试先行,但即使不做 TDD,也要在写完代码后立刻补测试。

测试结构:Given-When-Then

这是最流行的测试结构,把测试分成三个清晰的阶段:

java
@Test
@DisplayName("用户注册 - 成功")
void register_Success() {
    // Given:准备测试数据
    UserRequest request = new UserRequest();
    request.setUsername("testuser");
    request.setPassword("password123");

    // When:执行被测方法
    UserResponse response = userService.register(request);

    // Then:验证结果
    assertThat(response)
        .isNotNull()
        .returns("testuser", UserResponse::getUsername)
        .returns(notNullValue(), UserResponse::getId);
}

常见测试场景

测试正常流程

java
@Test
void testCalculateTotalPrice() {
    Order order = new Order();
    order.addItem(new OrderItem("商品1", 100.0, 2));
    order.addItem(new OrderItem("商品2", 50.0, 1));

    double total = order.calculateTotalPrice();

    assertEquals(250.0, total, 0.01);  // 第三个参数是 delta,允许浮点误差
}

测试边界条件

java
@Test
void testDivideByZero() {
    assertThrows(ArithmeticException.class, () -> {
        calculator.divide(10, 0);
    });
}

@Test
void testNullInput() {
    assertThrows(IllegalArgumentException.class, () -> {
        service.process(null);
    });
}

测试异常消息

java
@Test
void testExceptionMessage() {
    Exception exception = assertThrows(RuntimeException.class, () -> {
        throw new RuntimeException("用户不存在: 12345");
    });

    assertThat(exception.getMessage())
        .contains("12345");
}

总结

测试的核心原则:

  • 快速:毫秒级完成,不拖慢开发节奏
  • 独立:不依赖其他测试的执行顺序
  • 可重复:相同输入永远得到相同结果
  • 自验证:用断言,不用人工判断
  • 及时:代码写完就补测试

不写测试的代码,就像没有安全带的赛车——快是快,但一旦出事就是大事。

基于 VitePress 构建