本文目录导读:

- 原则一:使用“三段式”结构(AAA Pattern)
- 原则二:给测试起一个“说人话”的名字
- 原则三:每个测试只验证一个行为
- 原则四:让断言“自解释”
- 原则五:尽量减少“魔法值”和复杂准备
- 额外技巧:善用 Fixture 和 Builder 模式
- 总结:好的测试长什么样?
编写可读性高的单元测试,本质上是在用代码讲述一个故事,测试不仅是验证逻辑的工具,更是活文档,当几个月后(或别人)再看到这个测试时,应该能立刻明白被测代码在什么场景下、期望什么行为。
以下是编写高可读性单元测试的五个核心原则和具体实践,以 Java (JUnit 5) 和 Python (pytest) 为例,但思路是通用的。
使用“三段式”结构(AAA Pattern)
这是最基础、最有效的结构,将测试逻辑清晰分为三部分,形成“前提-执行-断言”的叙事流。
- Arrange(准备): 创建测试所需的所有对象和数据。
- Act(执行): 调用被测方法(通常只有一行代码)。
- Assert(断言): 验证结果是否符合预期。
不推荐(混乱):
// Java (JUnit 5)
@Test
void testUser() {
User u = new User("Alice");
String name = u.getName();
u.setName("");
assertTrue(name.equals("Alice") && u.getName().equals(""));
}
推荐(清晰):
// Java (JUnit 5)
@Test
void should_keep_original_name_when_setting_empty_name() {
// Arrange
User user = new User("Alice");
// Act
user.setName("");
// Assert
assertEquals("Alice", user.getName());
}
在 Python 的 pytest 中,通常会结合
fixture来处理 Arrange 部分。
给测试起一个“说人话”的名字
方法名应该直接描述测试场景和期望行为,不要用 test1、testFunctionality 这样无意义的名字。
推荐命名模板(采用“Given-When-Then”思想):
{被测方法}_{预期行为}_{条件}should_{预期结果}_when_{条件}
示例 (Python):
# 不推荐
def test_discount():
pass
# 推荐
def test_apply_discount_should_reduce_price_when_total_exceeds_100():
pass
# 或者
def test_apply_discount_given_total_over_100_then_price_is_reduced():
pass
关键:一个好的测试名,让人只看名字就能知道测试在测什么,甚至不需要看代码体。
每个测试只验证一个行为
如果一个测试失败,你希望立刻知道哪一个具体的行为出了问题,混合多个独立场景的测试会增加调试难度。
不推荐(一个测试测了多个事情):
// Java
@Test
void testUserFunctions() {
User u = new User();
assertNotNull(u);
assertTrue(u.isActive());
u.activate();
assertTrue(u.isActive());
}
推荐(拆分):
// Java
@Test
void should_return_active_when_user_is_newly_created() {
User user = new User();
assertTrue(user.isActive());
}
@Test
void should_remain_active_after_calling_activate_method() {
User user = new User();
user.activate();
assertTrue(user.isActive());
}
让断言“自解释”
好的断言读起来应该像英语句子一样自然,不要使用晦涩的布尔逻辑。
不推荐:
# Python (pytest)
def test_order_is_ready():
order = Order(status="ready")
# 这看起来像什么?
assert not order.status == "pending"
推荐:
# Python (pytest)
def test_order_status_is_ready():
order = Order(status="ready")
# 使用专门的方法,读起来更自然
assert order.is_ready()
# 或
assert order.status == "ready"
更进阶:使用专门化的断言库,AssertJ (Java) 或 pytest 自带的断言,它们能提供更丰富的错误信息。
// Java (JUnit 5 + AssertJ)
@Test
void should_find_user_by_id() {
User user = userService.find(1L);
// 链式调用,可读性极高,错误信息也极佳
assertThat(user)
.isNotNull()
.hasFieldOrPropertyWithValue("email", "test@example.com");
}
尽量减少“魔法值”和复杂准备
用有意义的常量或局部变量解释数字或字符串的含义。
不推荐:
# Python
def test_price_calculation():
item = Item(5, 10.0)
assert calculate(item) == 45.0
推荐:
# Python
def test_apply_percentage_discount_for_members():
# Arrange - 使用变量名解释含义
price = 100.0
discount_rate = 0.2 # 20% 会员折扣
expected_final_price = 80.0
item = Item(price, discount_rate)
# Act
final_price = calculate(item)
# Assert
assert final_price == pytest.approx(expected_final_price)
额外技巧:善用 Fixture 和 Builder 模式
复杂对象的创建会严重损害可读性。
-
Fixture (Python pytest/Java JUnit 5): 将通用的 Arrange 逻辑抽象出来。
# Python (pytest) @pytest.fixture def standard_user(): return User("Alice", "alice@test.com", active=True) def test_user_email(standard_user): assert standard_user.email == "alice@test.com" -
Test Data Builder (Java): 当对象有很多参数时,使用构建器模式清晰设置关键字段。
// Java @Test void should_reject_order_for_inactive_user() { User inactiveUser = User.builder() .active(false) .build(); // 其他字段使用默认值 Order order = new Order(inactiveUser, items); assertThrows(IllegalStateException.class, order::process); }
好的测试长什么样?
我把一个好测试的范式总结为“三行”测试(一种理想状态):
# Python (pytest)
def test_should_reject_negative_amount_during_credit():
# 1. 准备(清晰直观)
account = Account(balance=100.0)
negative_amount = -10.0
# 2. 执行(一行动作)
result = account.credit(negative_amount)
# 3. 断言(明确简洁)
assert result.is_failure() # 或 assert result == "Negative amount not allowed"
最后一个小贴士:编写测试时,假装 6 个月后的自己(或新同事)会来阅读这段代码,如果你觉得他看一眼就能明白意图,那这就是一个可读性高的测试。