特性测试(Characterization Test):遗留系统重构的“行为保险“

2025博客之星年度评选已开启 10w+人浏览 1.9k人参与

在这里插入图片描述

一、引言:重构遗留代码时,你是否踩过这些坑?

“这个函数逻辑太乱了,我重构一下,保证功能不变。”

相信每个开发者都对这句话不陌生。但现实往往是:重构后的代码语法更优雅、结构更清晰,可上线后却触发了一串线上告警——用户反馈核心功能失效,日志里满是从未见过的异常。

你对着重构前后的代码反复比对,最终发现:原来旧代码里藏着一个未被文档记录的"隐含逻辑"——当输入参数为 0 时,会自动触发兼容旧版本接口的适配处理。而你在重构时,误以为这是冗余代码,直接删除了。

类似的场景在遗留系统维护中屡见不鲜:无文档、无测试、逻辑晦涩的代码如同"黑盒",任何修改都像在拆炸弹。有没有一种方法,能给现有代码的行为"拍张快照",确保后续修改不会破坏已有功能?

答案就是特性测试(Characterization Test)维基链接)。

二、什么是特性测试?

2.1 通俗定义:给代码的"实际行为"拍张可验证的快照

特性测试的核心目标,是捕捉代码当前的实际行为,并将其固化为可自动化执行的测试用例。它不依赖预先定义的需求文档,而是基于代码"真实运行的结果"来构建测试——简单说就是:

先观察代码"实际能做什么",再把这个"能力"记录下来,后续任何修改只要破坏了这个"能力",测试就会立即报警。

用一个生活类比理解:你接手了一辆老旧汽车,不知道它的设计标准,但需要对发动机进行维修。为了避免维修后车辆无法正常行驶,你会先记录下当前车辆的关键状态:怠速时的转速、百公里油耗、最高时速、爬坡能力等。这些"当前状态记录"就相当于特性测试;维修后只要这些状态与记录不符,就说明维修出了问题。

对应到代码:你接手一个无文档的遗留函数,先输入不同的参数,记录下函数的输出结果(包括正常场景、边界场景、异常场景),再把"输入-输出"的对应关系写成测试用例。这些测试用例就是代码的"行为快照",后续重构只要改变了任何一个"输入-输出"对应关系,测试就会失败。

2.2 与传统测试的核心区别:从"验证需求"到"记录行为"

我们最熟悉的传统测试(如单元测试、功能测试),都是**“基于需求”**的——先有明确的需求文档,再根据需求编写测试用例,验证代码是否符合需求。

而特性测试是**“基于实际行为”**的——没有需求文档也能写,核心是"记录现有行为"而非"验证是否符合需求"。

对比维度特性测试传统单元测试/功能测试
编写依据代码的实际运行行为预先定义的需求规格文档
编写时机代码已存在后(常用于遗留系统)需求明确后,可与开发同步(如 TDD 模式)
核心目标保护现有行为不被破坏,降低修改风险验证代码是否符合设计需求,确保功能正确
对"缺陷"的态度暂时记录现有缺陷,避免修改时缺陷扩大识别并验证缺陷是否被修复
适用场景遗留系统重构、无文档代码维护新功能开发、需求明确的模块验证

关键提醒:特性测试不判断现有行为"是否正确",只记录"是什么"。如果现有代码存在已知缺陷,你可以先编写特性测试记录该缺陷(确保重构时不会忽略它),再修复缺陷并更新测试用例。


三、实战案例:用特性测试重构遗留订单计算函数

下面通过一个贴近电商业务的案例,完整演示如何在实际工程中使用特性测试。

案例背景:你接手了一个电商系统的遗留代码,其中 calculate_order_amount 函数负责计算订单总金额,但该函数无注释、无文档,且没有任何测试用例。现在需要重构该函数(优化逻辑冗余),同时确保不影响现有业务。

3.1 步骤 1:分析遗留代码,识别当前结构

首先,我们找到遗留代码的实现:

def calculate_order_amount(products, coupon_code=None):
    # 无任何注释,逻辑晦涩
    total = 0
    for p in products:
        if p["type"] == "normal":
            total += p["price"] * p["quantity"]
        elif p["type"] == "promotion":
            if p["price"] < 100:
                total += p["price"] * p["quantity"] * 0.9
            else:
                total += p["price"] * p["quantity"] * 0.85
    if coupon_code:
        if coupon_code.startswith("FULL"):
            threshold = int(coupon_code.split("_")[1])
            if total >= threshold:
                discount = int(coupon_code.split("_")[2])
                total -= discount
        elif coupon_code == "NEW_USER_10":
            total -= 10
            if total < 0:
                total = 0
    return total

由于没有文档,我们无法直接确定函数的设计意图,只能通过"输入不同参数,观察输出结果"的方式,反向推导并记录它的实际行为。

3.2 步骤 2:探索性运行,收集实际行为

这是特性测试区别于传统测试的关键步骤:我们不是根据需求文档预设输入输出,而是先运行代码、观察结果,再把观察到的结果固化为测试用例

第一步:设计探索性输入

基于代码结构分析,我们识别出需要探索的输入维度:

  • 商品类型:normalpromotion
  • 促销商品价格:小于 100、等于 100、大于 100(边界值)
  • 优惠券类型:无优惠券、FULL_* 满减券、NEW_USER_10 新人券
  • 商品组合:单一类型、混合类型
第二步:运行代码,记录实际输出

我们在 Python 交互环境中逐个探索,让代码告诉我们它的行为

>>> from order_utils import calculate_order_amount

# 探索1:普通商品基础计算
>>> products = [{"type": "normal", "price": 50, "quantity": 2}]
>>> calculate_order_amount(products)
100  # 记录:50*2=100,符合直觉

# 探索2:促销商品,价格 < 100
>>> products = [{"type": "promotion", "price": 80, "quantity": 3}]
>>> calculate_order_amount(products)
216.0  # 记录:80*3*0.9=216,看来是9折

# 探索3:促销商品,价格 = 100(边界值)
>>> products = [{"type": "promotion", "price": 100, "quantity": 2}]
>>> calculate_order_amount(products)
170.0  # 记录:100*2*0.85=170,边界值走85折分支

# 探索4:促销商品,价格 > 100
>>> products = [{"type": "promotion", "price": 200, "quantity": 2}]
>>> calculate_order_amount(products)
340.0  # 记录:200*2*0.85=340,确认≥100走85折

# 探索5:满减优惠券 - 纯普通商品
>>> products = [{"type": "normal", "price": 250, "quantity": 1}]
>>> calculate_order_amount(products, coupon_code="FULL_200_30")
220  # 记录:250-30=220,满减生效

# 探索6:满减优惠券 - 混合商品(这里发现意外行为!)
>>> products = [
...     {"type": "normal", "price": 150, "quantity": 1},
...     {"type": "promotion", "price": 90, "quantity": 1}
... ]
>>> calculate_order_amount(products, coupon_code="FULL_200_30")
231.0  # ⚠️ 意外!150+81=231,满足>=200,但没有减30!

# 探索7:再次验证 - 纯促销商品 + 满减
>>> products = [{"type": "promotion", "price": 250, "quantity": 1}]
>>> calculate_order_amount(products, coupon_code="FULL_200_30")
212.5  # ⚠️ 250*0.85=212.5,也没减30!满减似乎只对纯普通商品生效?

# 探索8:新人券,正常抵扣
>>> products = [{"type": "normal", "price": 50, "quantity": 1}]
>>> calculate_order_amount(products, coupon_code="NEW_USER_10")
40  # 记录:50-10=40

# 探索9:新人券,抵扣后为负
>>> products = [{"type": "normal", "price": 5, "quantity": 1}]
>>> calculate_order_amount(products, coupon_code="NEW_USER_10")
0  # 记录:5-10=-5,但返回0,有保底逻辑
第三步:分析探索结果,提炼关键发现

通过探索性运行,我们发现了几个非显而易见的行为

发现观察到的行为是否符合直觉
促销折扣边界价格 = 100 时走 85 折(>= 判断应该是 <⚠️ 需确认
满减券限制只对纯普通商品订单生效,包含促销商品时不生效⚠️ 疑似 Bug
新人券保底抵扣后金额为负时返回 0✅ 合理

这些发现正是特性测试的核心价值——它们揭示了代码中隐藏的业务规则或潜在缺陷,而这些在没有文档的情况下,只能通过实际运行来发现。

第四步:将观察结果固化为测试用例

现在,我们把探索过程中收集到的"输入-输出"对转化为自动化测试:

import pytest
from order_utils import calculate_order_amount

class TestCalculateOrderAmountCharacterization:
    """
    特性测试:记录 calculate_order_amount 的当前实际行为
    
    这些测试用例来源于对遗留代码的探索性运行,
    记录的是"代码实际做什么",而非"代码应该做什么"。
    标记 [疑似Bug] 的用例需要后续与业务方确认。
    """
    
    # --- 商品计价行为 ---
    
    def test_normal_product_calculation(self):
        """普通商品:原价计算"""
        products = [{"type": "normal", "price": 50, "quantity": 2}]
        assert calculate_order_amount(products) == 100
    
    def test_promotion_product_below_100(self):
        """促销商品:单价 < 100 时打 9 折"""
        products = [{"type": "promotion", "price": 80, "quantity": 3}]
        assert calculate_order_amount(products) == 216.0
    
    def test_promotion_product_at_boundary_100(self):
        """促销商品:单价 = 100 时打 85 折 [边界行为,需确认]"""
        products = [{"type": "promotion", "price": 100, "quantity": 2}]
        assert calculate_order_amount(products) == 170.0
    
    def test_promotion_product_above_100(self):
        """促销商品:单价 > 100 时打 85 折"""
        products = [{"type": "promotion", "price": 200, "quantity": 2}]
        assert calculate_order_amount(products) == 340.0
    
    # --- 满减优惠券行为 ---
    
    def test_full_coupon_with_normal_products_only(self):
        """满减券 + 纯普通商品:正常生效"""
        products = [{"type": "normal", "price": 250, "quantity": 1}]
        assert calculate_order_amount(products, "FULL_200_30") == 220
    
    def test_full_coupon_with_mixed_products(self):
        """满减券 + 混合商品:不生效 [疑似Bug,待确认]"""
        products = [
            {"type": "normal", "price": 150, "quantity": 1},
            {"type": "promotion", "price": 90, "quantity": 1}
        ]
        # 150 + 81 = 231,满足 >= 200,但实际未减 30
        assert calculate_order_amount(products, "FULL_200_30") == 231.0
    
    def test_full_coupon_with_promotion_products_only(self):
        """满减券 + 纯促销商品:不生效 [疑似Bug,待确认]"""
        products = [{"type": "promotion", "price": 250, "quantity": 1}]
        # 212.5 >= 200,但实际未减 30
        assert calculate_order_amount(products, "FULL_200_30") == 212.5
    
    # --- 新人优惠券行为 ---
    
    def test_new_user_coupon_normal_deduction(self):
        """新人券:正常抵扣"""
        products = [{"type": "normal", "price": 50, "quantity": 1}]
        assert calculate_order_amount(products, "NEW_USER_10") == 40
    
    def test_new_user_coupon_floor_to_zero(self):
        """新人券:抵扣后为负时返回 0"""
        products = [{"type": "normal", "price": 5, "quantity": 1}]
        assert calculate_order_amount(products, "NEW_USER_10") == 0

注意测试用例中的注释:我们用 [疑似Bug,待确认] 标记了那些不符合直觉的行为。这些注释是探索过程的重要产出,它们提醒后续开发者:这些行为需要与业务方确认后才能决定是"保留"还是"修复"。

3.3 步骤 3:重构代码,用特性测试保障行为一致

基于特性测试记录的行为,我们开始重构遗留函数——优化逻辑结构、增加注释,让代码更易维护,但必须保证重构后的函数通过所有特性测试

重构后的代码:

def calculate_order_amount(products, coupon_code=None):
    """
    计算订单总金额
    
    商品计价规则:
    - 普通商品:原价 × 数量
    - 促销商品:单价 < 100 元打 9 折,单价 ≥ 100 元打 85 折
    
    优惠券规则:
    - FULL_XXX_YY:满 XXX 减 YY(注意:从特性测试发现,混合订单时不生效,原因待查)
    - NEW_USER_10:新用户减 10 元,抵扣后金额 ≤ 0 则取 0
    """
    total = _calculate_products_total(products)
    total = _apply_coupon(total, coupon_code, products)
    return total


def _calculate_products_total(products):
    """计算商品总价"""
    total = 0
    for p in products:
        base_price = p["price"] * p["quantity"]
        if p["type"] == "normal":
            total += base_price
        elif p["type"] == "promotion":
            discount_rate = 0.9 if p["price"] < 100 else 0.85
            total += base_price * discount_rate
    return total


def _apply_coupon(total, coupon_code, products):
    """应用优惠券"""
    if not coupon_code:
        return total
    
    if coupon_code.startswith("FULL"):
        # 注意:保留原有行为——仅纯普通商品订单可用满减(从特性测试发现)
        has_promotion = any(p["type"] == "promotion" for p in products)
        if not has_promotion:
            parts = coupon_code.split("_")
            threshold, discount = int(parts[1]), int(parts[2])
            if total >= threshold:
                total -= discount
    
    elif coupon_code == "NEW_USER_10":
        total = max(total - 10, 0)
    
    return total

重构完成后,运行特性测试:

pytest test_order_characterization.py -v

如果所有测试用例都通过,说明重构后的代码保留了原有行为;如果有测试用例失败,说明重构引入了问题,需要针对性修正。

3.4 步骤 4:修复缺陷并更新测试(可选)

经过与业务方确认,"混合订单满减不生效"确实是一个 Bug,需要修复。此时的流程是:

  1. 先更新特性测试,将相关用例的预期结果修改为正确行为
  2. 再修改代码实现,移除满减的限制逻辑
  3. 运行测试,确保修复后的代码通过更新后的测试
def test_full_coupon_with_mixed_products(self):
    """满减券 + 混合商品:正常生效(已修复)"""
    products = [
        {"type": "normal", "price": 150, "quantity": 1},
        {"type": "promotion", "price": 90, "quantity": 1}
    ]
    # 修复后:150 + 81 = 231,满200减30,预期201
    assert calculate_order_amount(products, "FULL_200_30") == 201.0

这体现了特性测试的迭代性:既保护现有行为,也支持合理的行为优化。


四、工程实践:规模化应用特性测试的关键问题

在实际大型项目中,应用特性测试会面临两个核心问题:如何确保输入覆盖完整?如何在数百个函数中高效操作?

4.1 如何确保典型输入都被覆盖?

特性测试的价值依赖于输入场景的完整性。若遗漏关键输入,可能导致重构时破坏未覆盖的功能。以下三种方法可保障覆盖率:

方法一:借助代码覆盖率工具

使用覆盖率工具(如 Python 的 coverage、Java 的 JaCoCo)扫描测试执行情况,识别未被覆盖的代码分支。

# 运行测试并生成覆盖率报告
coverage run -m pytest test_order_characterization.py
coverage html

若工具提示"促销商品价格等于 100 元"的分支未被覆盖,则需补充该边界场景的测试用例。

方法二:从业务视角系统梳理输入类型

将输入场景分为三类系统设计:

输入类型说明案例中的示例
正常输入业务主流程的典型场景普通商品、促销商品、常规优惠券
边界输入临界值、极值场景商品数量为 0、价格为 0、金额刚好等于满减阈值
异常输入非法或意外的输入商品类型字段错误、优惠券格式非法

方法三:从生产数据中提炼高频场景

对于已上线系统,可从业务日志中提取真实的输入参数分布,确保高频场景被重点覆盖。例如,从订单日志中统计发现 80% 的订单使用"FULL_200_30"优惠券,则该场景必须纳入测试。

4.2 特性测试的粒度选择:集成级还是函数级?

大型项目包含大量函数,若对每个函数都编写函数级特性测试,工作量会激增。实践中建议采用**“先集成后单元”**的分层策略:

第一层:集成级特性测试(优先)

聚焦核心业务流程,通过端到端的输入输出覆盖多个函数的协同行为。例如,测试"下单 → 计算金额 → 应用优惠"的完整链路,而非单独测试每个函数。

  • 优势:用较少的测试用例覆盖核心功能,快速建立安全网
  • 适用:业务链路清晰、函数间耦合紧密的模块

第二层:函数级特性测试(按需补充)

对于逻辑复杂、独立性强的关键函数,补充函数级测试以细化行为记录。本案例中的 calculate_order_amount 就属于此类——它是独立的计算单元,逻辑复杂,值得单独测试。

  • 优势:精确定位问题,便于理解复杂逻辑
  • 适用:核心算法函数、复杂业务规则函数

4.3 提升操作效率的实用技巧

技巧说明
复用测试模板针对同类型函数(如计算类、校验类)提炼通用测试模板,统一输入场景设计和断言格式
增量式推进不追求一次性覆盖所有函数,按重构优先级逐模块推进,先测试即将重构的模块
自动化辅助生成对于简单 CRUD 函数,可结合 API 文档自动生成基础测试用例,再人工补充边界场景
结对编写由熟悉业务的同事提供输入场景,由熟悉测试的同事编写用例,提升效率和准确性

五、总结

5.1 特性测试的核心价值

特性测试的本质,是为无文档、无测试的遗留代码建立**“行为契约”**。它解决了"不知道代码实际做什么,不敢修改"的核心痛点:

价值维度说明
降低风险确保修改不会破坏现有功能,避免线上回归缺陷
辅助理解测试用例本身就是"可执行的文档",帮助开发者反向理解代码逻辑
支持迭代既保护现有行为,也允许通过更新测试用例来固化优化后的新行为

5.2 适用边界

特性测试并非"万能测试",其核心适用场景是:

  • ✅ 遗留系统重构
  • ✅ 无文档代码维护
  • ✅ 接手他人项目时建立安全网

对于新功能开发,更适合使用传统的单元测试或 TDD 模式——因为新功能有明确的需求文档,无需通过"记录实际行为"来建立测试。

5.3 最终建议

如果你经常需要维护遗留代码,建议养成 “先写特性测试,再修改代码” 的习惯:

  1. 特性测试不需要一开始就覆盖所有场景,先覆盖核心路径
  2. 关键是锁定"不能动"的核心行为
  3. 把它当作给代码买的一份"行为保险",让每一次修改都更安心
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小郎碎碎念

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值