2. 被测单元边界识别:如何划定测试范围、识别外部依赖、判定隔离点
单元测试的首要步骤不是写代码,而是划定边界。错误的边界会导致测试脆弱、执行缓慢或根本不验证任何逻辑。本章提供一套可操作的边界识别方法,助你精准圈定被测单元、列出所有外部依赖,并快速判定哪些点需要隔离。
2.1 边界识别的三层视角
| 视角层 | 关注点 | 典型判断依据 |
|---|---|---|
| 函数/方法层 | 单一职责、入参/出参 | 是否只做一件事?副作用能否被参数取代? |
| 类/模块层 | 内部状态、协作对象 | 是否有可观测的状态变迁?是否依赖接口/抽象? |
| 服务/组件层 | 进程边界、I/O、网络 | 是否读写磁盘、数据库、外部API? |
从最细粒度开始,逐层向上确认:若被测单元直接调用外部系统,则必须在那一层引入隔离点。
2.2 外部依赖识别清单
任何被测单元“不可控且可能不稳定的对外交互”都应标记为外部依赖:
- 文件系统(读/写/删除/是否存在)
- 数据库 / Redis / 消息队列
- 第三方 HTTP API / gRPC 服务
- 系统时间(
DateTime.Now、time.Now()) - 随机数生成器
- 环境变量 / 系统配置
- 其他业务类(若行为未固化、经常改动)
实用技巧:在代码审查时用“// DEP”注释标记每行外部调用,然后统计——若一个函数内出现超过 2 个不同的 DEP,就应考虑拆分或注入。
2.3 判定隔离点的三问测试
针对每个已识别的外部依赖,依次回答三个问题:
- 是否影响测试确定性? ——相同代码在不同执行环境下是否可能返回不同结果?
- 是否引入速度惩罚? ——单次调用耗时 > 10ms?
- 是否造成测试间干扰? ——两次运行是否留下残留状态?
只要有一个回答为“是”,该依赖点就必须被隔离(用 Stub 或 Mock 替换)。
常见错误:初学者常把“所有的私有方法”都当作依赖。私有方法不是外部依赖,它们是实现细节,不应被隔离。隔离点仅针对 可拦截的外部边界(接口、虚方法、委托、抽象类)。
2.4 边界识别流程图
2.5 实战案例:订单计算函数
// 原始代码(Python)
class OrderCalculator:
def __init__(self):
self.discount_service = DiscountService() # 外部HTTP调用
self.tax_api = TaxApi() # 外部API
self.file_logger = FileLogger("orders.log") # 文件系统
def calculate_total(self, items, user_id):
sub_total = sum(item.price for item in items)
discount = self.discount_service.get_discount(user_id) # DEP1
tax_rate = self.tax_api.lookup_tax("CN") # DEP2
total = sub_total * (1 - discount) * (1 + tax_rate)
self.file_logger.write(f"User {user_id} total: {total}") # DEP3
return total
边界识别结果:
| 交互 | 类型 | 三问结果 | 隔离点? |
|---|---|---|---|
discount_service.get_discount | 外部HTTP | Y/Y/Y | 是 → Mock |
tax_api.lookup_tax | 外部API | Y/Y/Y | 是 → Stub |
file_logger.write | 文件系统 | N/Y/Y | 是 → Stub |
sum(item.price ...) | 内部计算 | N/N/N | 否 → 保留 |
核心原则:每个被标记“是”的隔离点,都需要在测试替身(Stub 或 Mock)中替换。边界越清晰,后续第六章的 Stub/Mock 选择越简单。
2.6 常见错误与调试技巧
- 过度隔离:将 getter/setter、纯数学函数也替换,导致测试与实现耦合过强。修正:只隔离“有外部依赖”的调用。
- 漏隔离:忘记系统时间或随机数。修正:使用三问测试逐一审查所有参数和调用。
- 边界漂移:重构后依赖关系改变,但测试代码未更新隔离点。修正:每次重构后重新运行“识别清单 + 三问测试”。
调试时可在关键隔离点加一行日志输出,确认测试执行路径确实绕过真实依赖:
# 调试日志示例(运行测试时)
[MOCK] DiscountService.get_discount called with user_id=42, returning 0.1
[STUB] TaxApi.lookup_tax called with "CN", returning 0.13
[PROD] FileLogger.write SKIPPED by stub
PASS: test_calculate_total_with_discount
确保日志中出现 [MOCK] / [STUB] 标记,表明隔离生效;若出现真实调用的日志,则代表隔离失效,需重新检查构造注入或接口实现。