2. 被测单元边界识别:如何划定测试范围、识别外部依赖、判定隔离点

单元测试的首要步骤不是写代码,而是划定边界。错误的边界会导致测试脆弱、执行缓慢或根本不验证任何逻辑。本章提供一套可操作的边界识别方法,助你精准圈定被测单元、列出所有外部依赖,并快速判定哪些点需要隔离。

2.1 边界识别的三层视角

视角层关注点典型判断依据
函数/方法层单一职责、入参/出参是否只做一件事?副作用能否被参数取代?
类/模块层内部状态、协作对象是否有可观测的状态变迁?是否依赖接口/抽象?
服务/组件层进程边界、I/O、网络是否读写磁盘、数据库、外部API?

从最细粒度开始,逐层向上确认:若被测单元直接调用外部系统,则必须在那一层引入隔离点。

2.2 外部依赖识别清单

任何被测单元“不可控且可能不稳定的对外交互”都应标记为外部依赖:

  • 文件系统(读/写/删除/是否存在)
  • 数据库 / Redis / 消息队列
  • 第三方 HTTP API / gRPC 服务
  • 系统时间(DateTime.Now、time.Now())
  • 随机数生成器
  • 环境变量 / 系统配置
  • 其他业务类(若行为未固化、经常改动)
实用技巧:在代码审查时用“// DEP”注释标记每行外部调用,然后统计——若一个函数内出现超过 2 个不同的 DEP,就应考虑拆分或注入。

2.3 判定隔离点的三问测试

针对每个已识别的外部依赖,依次回答三个问题:

  1. 是否影响测试确定性? ——相同代码在不同执行环境下是否可能返回不同结果?
  2. 是否引入速度惩罚? ——单次调用耗时 > 10ms?
  3. 是否造成测试间干扰? ——两次运行是否留下残留状态?

只要有一个回答为“是”,该依赖点就必须被隔离(用 Stub 或 Mock 替换)。

常见错误:初学者常把“所有的私有方法”都当作依赖。私有方法不是外部依赖,它们是实现细节,不应被隔离。隔离点仅针对 可拦截的外部边界(接口、虚方法、委托、抽象类)。

2.4 边界识别流程图

被测单元边界识别流程 1. 定位被测单元 2. 列出所有交互对象 是内部实现? (字段/私有方法) 忽略 (不隔离) 3. 外部依赖 → 三问测试 (确定|速度|干扰) 判定为隔离点 (使用 Stub / Mock) 关键判断 “内部实现”指: · 私有字段/方法 · 不产生副作用的计算 · 不依赖外部状态

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外部HTTPY/Y/Y是 → Mock
tax_api.lookup_tax外部APIY/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] 标记,表明隔离生效;若出现真实调用的日志,则代表隔离失效,需重新检查构造注入或接口实现。