16、测试驱动重构:测试金字塔实战、单元测试编写策略(JUnit/pytest)、测试替身(Mock/Stub/Fake)、重构中的测试保护网
说实话,很多团队不敢重构,不是因为代码烂,而是因为没测试。代码烂可以忍,但改出 bug 没人扛得住。我见过太多项目,重构到一半,测试全红,然后回滚,然后继续烂下去。
所以这一章,咱们聊聊怎么用测试给重构铺一条安全的路。说白了,就是给你的代码装个安全气囊。
16.1 测试金字塔:别把鸡蛋放在一个篮子里
先说说测试金字塔。这个概念你可能听过,但真正用好的团队不多。我见过不少项目,单元测试没几个,集成测试倒写了一堆。跑一次要半小时,谁还愿意跑?
测试金字塔长这样:
我个人习惯的比例是:70% 单元测试,20% 集成测试,10% 端到端测试。为什么?因为单元测试跑得快,几秒钟就能跑完。你想想看,每次改完代码,几秒钟就知道有没有搞坏东西,这感觉多爽。
核心原则:重构时,单元测试是你的第一道防线。集成测试是第二道。端到端测试是最后一道。别指望端到端测试帮你抓 bug,它太慢了,跑一次够你喝杯咖啡。
16.2 单元测试编写策略:JUnit 和 pytest
单元测试怎么写?我总结了一个口诀:「一个方法,一个行为,一个断言」。别在一个测试里测一堆东西,那样出了问题你都不知道是哪个环节挂了。
16.2.1 Java 版:JUnit 5
先看个 Java 的例子。假设我们有个订单服务,要计算折扣:
public class OrderService {
public double calculateDiscount(Order order) {
if (order.getTotalAmount() > 1000) {
return order.getTotalAmount() * 0.1;
}
return 0;
}
}
对应的单元测试:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class OrderServiceTest {
@Test
void shouldGive10PercentDiscountWhenAmountExceeds1000() {
OrderService service = new OrderService();
Order order = new Order(1500);
double discount = service.calculateDiscount(order);
assertEquals(150.0, discount, 0.01);
}
@Test
void shouldGiveNoDiscountWhenAmountIsBelow1000() {
OrderService service = new OrderService();
Order order = new Order(500);
double discount = service.calculateDiscount(order);
assertEquals(0.0, discount, 0.01);
}
}
注意看,每个测试方法的名字都写得很清楚。我见过有人写 test1、test2,跑挂了还得去翻代码才知道测的啥。别这样,名字就是文档。
16.2.2 Python 版:pytest
Python 这边,pytest 用起来更简洁。同样的逻辑:
# order_service.py
class OrderService:
def calculate_discount(self, order):
if order.total_amount > 1000:
return order.total_amount * 0.1
return 0
# test_order_service.py
import pytest
def test_should_give_10_percent_discount_when_amount_exceeds_1000():
service = OrderService()
order = Order(total_amount=1500)
discount = service.calculate_discount(order)
assert discount == 150.0
def test_should_give_no_discount_when_amount_is_below_1000():
service = OrderService()
order = Order(total_amount=500)
discount = service.calculate_discount(order)
assert discount == 0.0
小技巧:pytest 支持参数化测试,可以少写很多重复代码。比如测边界值,用 @pytest.mark.parametrize 一行搞定。
16.3 测试替身:Mock、Stub、Fake 怎么选?
重构时最头疼的是什么?外部依赖。数据库、第三方 API、文件系统……这些东西在测试里不好搞。这时候就需要测试替身了。
我简单说说三者的区别:
| 类型 | 作用 | 什么时候用 |
|---|---|---|
| Stub | 提供预设的返回值 | 你只关心输入输出,不关心方法有没有被调用 |
| Mock | 验证行为(比如方法被调用了多少次) | 你需要确保某个方法确实被调用了 |
| Fake | 轻量级的真实实现(比如内存数据库) | 你需要一个能正常工作的替代品 |
举个例子。假设你的订单服务要调用支付网关:
public class OrderService {
private PaymentGateway paymentGateway;
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public boolean processOrder(Order order) {
// 业务逻辑...
return paymentGateway.charge(order.getAmount());
}
}
用 Mockito 写个 Mock:
@Test
void shouldChargePaymentWhenProcessingOrder() {
PaymentGateway mockGateway = mock(PaymentGateway.class);
when(mockGateway.charge(anyDouble())).thenReturn(true);
OrderService service = new OrderService(mockGateway);
Order order = new Order(500);
boolean result = service.processOrder(order);
assertTrue(result);
verify(mockGateway).charge(500); // 验证确实调用了 charge 方法
}
避坑指南:我曾经在一个项目里过度使用 Mock,结果测试全绿,但一上线就挂。为什么?因为 Mock 的行为和真实实现不一致。记住:Mock 只验证交互,不验证逻辑。关键的业务逻辑,该用集成测试还是得用。
16.4 重构中的测试保护网
好了,前面铺垫了这么多,终于到正题了。重构时怎么用测试保护自己?我总结了一个三步法:
- 先写测试,再重构 —— 这是铁律。没有测试保护,别动代码。
- 小步提交,频繁运行 —— 改一行代码,跑一次测试。别攒一堆再跑,到时候都不知道哪出问题了。
- 测试也重构 —— 代码重构了,测试也得跟着改。别让测试变成技术债务。
我举个例子。假设你要把一个大的 if-else 改成策略模式。步骤是这样的:
// 重构前
public double calculatePrice(String userType, double amount) {
if ("VIP".equals(userType)) {
return amount * 0.8;
} else if ("NORMAL".equals(userType)) {
return amount * 0.9;
} else {
return amount;
}
}
先写测试:
@Test
void shouldApplyVIPDiscount() {
PriceCalculator calculator = new PriceCalculator();
assertEquals(80, calculator.calculatePrice("VIP", 100));
}
@Test
void shouldApplyNormalDiscount() {
PriceCalculator calculator = new PriceCalculator();
assertEquals(90, calculator.calculatePrice("NORMAL", 100));
}
@Test
void shouldNotApplyDiscountForGuest() {
PriceCalculator calculator = new PriceCalculator();
assertEquals(100, calculator.calculatePrice("GUEST", 100));
}
然后开始重构。每次只改一点点,改完就跑测试。直到所有测试都通过,重构完成。
记住:测试保护网不是一次性的。它需要维护。每次重构完,检查一下测试覆盖率。如果发现有些分支没测到,补上。我习惯在重构前后各跑一次覆盖率报告,确保没有遗漏。
最后说一句。测试驱动重构,说白了就是「先有安全,再求效率」。没有测试的重构,就像走钢丝没有安全网。你技术再好,也难免失手。所以,别偷懒,把测试写好。你的代码会感谢你的。
公众号:蓝海资料掘金营,微信deep3321