27、可维护测试与测试质量提升

可维护测试与测试质量提升

1. 可维护测试要点

在测试工作中有两个基本原则需要牢记。
- 测试行为而非实现 :遵循此规则有助于实现高质量测试,这适用于各个层面的测试,并非仅针对单元测试。
- 生产代码规则 :即迪米特法则和“告知,勿询问”原则。若代码不遵循这些原则,测试时可能会遇到问题。

1.1 汽车搜索类测试练习

业务分析师经过三个月分析,确定满足以下条件的汽车可标记为“运动型”:
- 汽车为红色。
- 由法拉利制造。
- 发动机气缸数超过 6 个。

基于此,开发团队实现了 CarSearch 类:

public class CarSearch {
    private List<Car> cars = new ArrayList<Car>();
    public void addCar(Car car) {
        cars.add(car);
    }
    public List<Car> findSportCars() {
        List<Car> sportCars = new ArrayList<Car>();
        for (Car car : cars) {
            if (car.getEngine().getNbOfCylinders() > 6
                    && Color.RED.equals(car.getColor())
                    && "Ferrari".equals(car.getManufacturer().getName())) {
                sportCars.add(car);
            }
        }
        return sportCars;
    }
}

相关接口如下:

// Car 接口
public interface Car {
    Engine getEngine();
    Color getColor();
    Manufacturer getManufacturer();
}

// Engine 接口
public interface Engine {
    int getNbOfCylinders();
}

// Manufacturer 接口
public interface Manufacturer {
    String getName();
}

任务是为 CarSearch 类的 findSportCars() 方法编写测试。步骤如下:
1. 先针对 CarSearch 类的原始实现编写测试,通过 addCar() 方法传入汽车,然后验证 findSportCars() 方法是否仅返回运动型汽车。
2. 重新设计 Car 接口,使 CarSearch 类不违反迪米特法则和“告知,勿询问”原则,再次编写测试,并比较两种情况下的工作量。

1.2 栈测试练习

基于相关讨论,使用测试驱动开发(TDD)方法实现 Stack10 类和对应的 StackTest 类。要在实现代码之前编写测试,并从类的职责角度进行思考。

2. 测试质量相关内容

2.1 测试质量的重要性

开发者关注代码质量有诸多原因:
- 质量是代码编写卓越的证明,大家都希望被认可为优秀的开发者。
- 软件质量不佳可能导致严重灾难,即使所编写的软件并非用于重大项目,也不应让客户因软件缺陷而失望。
- 糟糕的代码就像回旋镖,最终会给自己带来麻烦,因此应尽可能编写高质量代码。

由于开发者编写的测试很重要,所以也需关注测试的质量。若无法保证测试的高质量,就只是获得了一种虚假的安全感。

2.2 测试前的思考

在考虑测试的设计和实现之前,应先问“是否需要”,而非“如何做”。例如,思考编写集成测试还是单元测试更合适,或者某个测试场景是否已有其他测试覆盖。无用或冗余的高质量测试依然是无用的,不应浪费时间编写本不该编写的完美测试。

2.3 测试异味

在处理生产代码时,“代码异味”用于描述代码中看起来不合理的语句,社区中已对常见的代码异味进行了总结和命名,也有工具可发现这些异味。对于测试代码,有类似的“测试异味”概念,但使用较少,且没有像生产代码那样被广泛认可的列表。

许多所谓的代码或测试异味只是更通用规则的具体表现。例如,“覆盖存根行为”的测试异味,若在 setUp() 方法中描述了一个存根的行为,又在某些测试方法中覆盖它,会降低代码可读性。若遵循避免使用“全局”对象的一般规则,就可避免这种异味。所以,应遵循最佳编程实践,让测试异味尽量不出现。

2.4 重构

提到代码异味就不得不说重构。在测试代码中也会涉及重构,它有助于提高测试的内部和外部质量。内部质量体现在重构后的测试更易理解和维护,外部质量体现在通过重构使测试更可能真正测试到有价值的内容。不过,将“重构”一词用于测试代码的修改其实不太准确,但由于大家普遍这样使用,所以仍沿用该术语。

2.5 代码质量影响测试质量

生产代码和测试代码的质量密切相关。要拥有高质量、可维护的测试,首先要编写更清晰、设计更好且松耦合的生产代码。若生产代码混乱,测试代码很可能也会如此;若生产代码良好,测试代码也有机会达到较高质量。其次,要像对待生产代码一样尊重测试代码,让测试代码遵循与生产代码相同的规则,如保持简单(KISS)、单一职责原则(SRP),避免使用不必要的技术,同时关注测试代码的质量,可通过静态分析和代码审查等方式。

2.6 观察失败的测试

观察测试失败是检查测试质量的一种方法。若测试失败,说明它确实验证了某些行为。虽然这不足以证明测试有价值,但至少表明测试有一定作用。所以,采用 TDD 方法时,不要省略红色阶段,要亲眼见证测试失败。有时,为确保测试真正起作用,也可故意破坏生产代码。

2.7 静态分析工具

静态代码分析工具(如 PMD 或 FindBugs)常用于验证生产代码,可发现各种反模式和潜在的 bug,还能指出代码异味。通常,团队会将其作为完成标准的一部分,这些工具也常与 IDE 集成或用于持续集成过程。

自然会想到用这些工具验证测试代码,但实际上,对测试代码进行静态分析效果可能不佳。因为测试代码通常很简单,很少有嵌套的 try-catch 语句、打开的流、重用的变量、深层继承层次结构、方法的多个返回、违反 hashCode() / equals() 契约等静态分析工具擅长处理的情况。

PMD 和 FindBugs 为 JUnit 测试提供了一些规则检查,例如:
- 验证是否使用了正确的断言,如使用 assertTrue(someBoolean) 而非 assertEquals(someBoolean, true) ,使用 assertNull(someVariable) 而非 assertTrue(someVariable == null)
- 检查测试方法是否至少有一个断言。
- 确保两个双精度值以一定精度进行比较,而非精确相等。

不过,这些规则的实用性有限,且在编写本文时,没有适用于 TestNG 测试的等效规则。另外,计算代码指标(如类或方法的行数、圈复杂度)对测试代码的帮助也不大,因为测试代码本身复杂度通常较低。总之,静态分析工具对测试代码的帮助有限,可使用但不要期望过高。

2.8 代码覆盖

由于静态代码分析工具在发现测试代码异味方面表现不佳,我们转向使用代码覆盖技术的工具,这些工具常用于评估测试质量。

2.8.1 行覆盖和分支覆盖

“代码覆盖”是一个宽泛的术语,涵盖多种类型的覆盖测量。这里介绍 Cobertura 工具提供的行覆盖和分支覆盖:
- 行覆盖 :是一个简单的指标,仅表明代码的某一行(或语句)是否被执行过。若在测试执行期间某行代码被“触及”,则认为该行被覆盖。但这种覆盖方式可能会产生误导。
- 分支覆盖 :关注生产代码中的决策点,如带有逻辑 && || 运算符的 if while 语句。这是比行覆盖更强的测量方式。要满足分支覆盖,需编写测试使代码中的每个逻辑表达式都能被评估为 true false

以下代码片段展示了两者的区别:

public boolean bar(boolean a, boolean b) {
    boolean result = false;
    if (a && b) {
        result = true;
    }
    return result;
}

要获得 100% 行覆盖和分支覆盖所需的测试调用如下表所示:
| 覆盖类型 | 测试调用 |
| ---- | ---- |
| 行覆盖 | foo(true, true) |
| 分支覆盖 | foo(true, false)
foo(true, true)
foo(false, false) |

2.8.2 代码覆盖报告

测试运行后,代码覆盖工具会生成覆盖指标报告。这些报告可提供项目整体的代码覆盖情况,也可详细查看特定类的覆盖情况。报告中会显示每个包或类的行覆盖百分比、总行数、覆盖的行数,以及分支覆盖百分比、总分支数和覆盖的分支数。通常,未被执行(未被“覆盖”)的代码部分用红色表示,但并非绿色越多就越好,还需进一步分析。

综上所述,虽然代码覆盖工具能提供一些关于测试的有用信息,但它们也有局限性,不能仅依靠这些工具来评估测试质量。在测试工作中,应综合运用各种方法和原则,以实现高质量的测试。

3. 代码覆盖的深入分析

3.1 行覆盖和分支覆盖的区别

行覆盖和分支覆盖是衡量代码测试覆盖程度的重要指标,它们的区别通过下面的 mermaid 流程图可以更直观地体现:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px

    A([开始]):::startend --> B{行覆盖}:::process
    A --> C{分支覆盖}:::process
    B --> D(仅关注语句是否执行):::process
    C --> E(关注决策点逻辑表达式真假):::process
    D --> F(简单但可能误导):::process
    E --> G(更强测量方式):::process
    F --> H(示例: 一行代码执行即覆盖):::process
    G --> I(示例: if 语句真假情况都要覆盖):::process
    H --> J([结束]):::startend
    I --> J

从流程图可以看出,行覆盖只看代码语句是否被执行,而分支覆盖要确保决策点的逻辑表达式在真假两种情况下都被测试到。例如前面提到的 bar 方法:

public boolean bar(boolean a, boolean b) {
    boolean result = false;
    if (a && b) {
        result = true;
    }
    return result;
}

行覆盖只需要调用 foo(true, true) 让代码中的语句执行一次即可,但分支覆盖需要调用 foo(true, false) foo(true, true) foo(false, false) 来确保 if 语句的所有逻辑分支都被覆盖到。

3.2 代码覆盖报告的解读

代码覆盖报告是评估测试覆盖情况的重要依据,下面以一个简化的报告为例进行说明:

包名 行覆盖百分比 总行数 覆盖行数 分支覆盖百分比 总分支数 覆盖分支数
com.example.package1 80% 100 80 60% 50 30
com.example.package2 90% 200 180 70% 80 56

从这个表格可以看出,不同包的行覆盖和分支覆盖情况不同。 com.example.package1 的行覆盖达到了 80%,但分支覆盖只有 60%,这可能意味着该包中的代码在决策点的测试不够全面,存在一些逻辑分支没有被测试到。而 com.example.package2 的行覆盖和分支覆盖相对较高,但也并非 100%,仍有改进的空间。

在解读报告时,不能仅仅关注覆盖率的数字,还需要深入分析未覆盖的部分。例如,如果某个方法的分支覆盖较低,可能需要检查该方法中的决策逻辑,增加相应的测试用例来提高覆盖程度。

4. 测试实践中的综合考量

4.1 测试方法的选择

在实际测试工作中,选择合适的测试方法至关重要。以下是选择测试方法时需要考虑的因素列表:
- 测试目标 :明确测试是为了验证功能的正确性、性能的稳定性,还是安全性等。例如,如果是验证一个简单的计算函数,单元测试可能就足够了;但如果是测试一个复杂的系统交互,可能需要集成测试。
- 代码复杂度 :代码越复杂,可能需要更全面的测试。对于嵌套层次深、逻辑复杂的代码,分支覆盖测试就显得尤为重要。
- 时间和资源 :测试需要投入时间和资源,要根据实际情况选择合适的测试方法。如果时间紧迫,可能优先选择关键功能的测试。

4.2 测试质量的提升策略

为了提升测试质量,可以采取以下策略:
- 遵循最佳实践 :如前面提到的测试行为而非实现、遵循迪米特法则和“告知,勿询问”原则等。
- 持续改进 :定期回顾测试用例,根据新的需求和代码变更更新测试。
- 团队协作 :开发人员和测试人员密切合作,共同参与测试工作,确保测试的全面性和准确性。

4.3 测试质量评估的综合方法

评估测试质量不能仅仅依赖于某一种工具或指标,而应该综合多种方法。以下是一个综合评估的流程 mermaid 流程图:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px

    A([开始]):::startend --> B{静态分析}:::process
    A --> C{代码覆盖}:::process
    A --> D{观察失败测试}:::process
    B --> E(检查代码异味和规则遵守情况):::process
    C --> F(分析行覆盖和分支覆盖):::process
    D --> G(验证测试是否能检测行为):::process
    E --> H(评估代码规范性):::process
    F --> I(判断测试覆盖全面性):::process
    G --> J(确认测试有效性):::process
    H --> K([综合评估]):::process
    I --> K
    J --> K
    K --> L([结束]):::startend

从流程图可以看出,综合评估测试质量需要结合静态分析、代码覆盖和观察失败测试等多种方法。静态分析可以检查代码的规范性和是否存在异味;代码覆盖可以了解测试的全面性;观察失败测试可以确认测试是否真正有效。通过综合这些方法,可以更准确地评估测试质量,从而采取相应的措施来提升测试水平。

4. 总结

在软件测试工作中,可维护测试和测试质量提升是两个关键的方面。牢记测试行为而非实现、遵循迪米特法则和“告知,勿询问”原则等基本规则,有助于编写高质量的可维护测试。同时,在测试过程中要关注测试质量,通过综合运用静态分析工具、代码覆盖工具以及观察失败测试等方法,全面评估和提升测试质量。

在选择测试方法时,要根据测试目标、代码复杂度和时间资源等因素进行综合考虑。对于不同的代码情况,合理选择行覆盖或分支覆盖等测试指标,深入分析代码覆盖报告,以发现测试中的不足并及时改进。

总之,高质量的测试需要不断地实践和总结经验,遵循最佳实践,持续改进测试策略和方法,才能确保软件的质量和稳定性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值