读书笔记 -- Junit 实战(3rd)Ch05、Ch06 测试质量

发布时间 2023-11-10 01:56:40作者: bruce_he

Ch05 软件测试原则

1. 系统测试

测试替身 或 模拟对象 可以模拟复杂的真实对象的行为。

模拟对象(mock object):可以出现在单元测试级别,其作用是替代系统中不可用的部分或合并到一个测试中不切实际的部分。

测试替身(test doubles):是模拟对象,可 以可控的方式模拟真实对象的行为。

 


Ch06 测试质量

6.1 测试覆盖率度量

白盒单元测试可以提高测试覆盖率,并且可以控制每个方法的输入和辅助对象的行为(使用 stub 或 mock object)

   可以针对 protected、包私有的 和 公有的 方法编写白盒单元测试,可以提高覆盖率。

6.1.2 代码覆盖率度量工具

方式一:IdeaJ

   

方式二:命令行 + CI/CD

方式三: maven + jaCoCo

// step1: pom.xml 添加 jaCoCo 依赖
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.0.0</version>
        </plugin>
        <plugin>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
            <version>0.8.11</version>
            <executions>
                <execution>
                    <goals>
                        <goal>prepare-agent</goal>
                    </goals>
                </execution>
                <execution>
                    <id>report</id>
                    <phase>test</phase>
                    <goals>
                        <goal>report</goal>
                    </goals>
                </execution>
                <execution>
                    <id>jacoco-check</id>
                    <phase>test</phase>
                    <goals>
                        <goal>check</goal>
                    </goals>
                    <configuration>
                        <rules>
                            <rule>
                                <element>PACKAGE</element>
                                <limits>
                                    <limit>
                                        <counter>LINE</counter>
                                        <value>COVEREDRATIO</value>
                                    </limit>
                                </limits>
                            </rule>
                        </rules>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

// step2:命令行 运行 "mvn test"
> ch06-quality > mvn test

// step3: 在 \target\site\jacoco\index.html,打开该 html 即可查看结果

6.2 编写易于测试的代码

1. 理解公共 API 契约,特别是不要改变公有方法的签名;

2. 减少依赖的方法:实例化 new 对象(工厂)的方法与提供应用程序逻辑的方法隔离开。具体操作可以用 mock

// bad。缺点: 每次实例化 Vehicle 对象时,都要初始化 Driver 对象
public class Vehicle1 {

    Driver d = new Driver();
    boolean hasDriver = true;

    public void setHasDriver(boolean hasDriver) {
        this.hasDriver = hasDriver;
    }
}

// recommend:将 Driver 对象作为参数传递给 Vehicle,该用例中通过构造对象传入,即 通过依赖注入(将依赖项提供给另一个对象)
public class Vehicle2 {

    Driver d;
    boolean hasDriver = true;

    public Vehicle2(Driver d) {
        this.d = d;
    }

    private void setHasDriver(boolean hasDriver) {
        this.hasDriver = hasDriver;
    }
}

3. 创建简单的构造方法

// bad:在构造方法内将类设置成某一特定状态。缺点:每次实例化类,都将执行构造器,也就将类设置成该特定状态
public class Car {
    private int maxSpeed;

    public Car(int maxSpeed) {
        this.maxSpeed = 180;
    }
}

// recommend: 通过 setter 方法将类设置成某一状态
public class Car {
    private int maxSpeed;

    public void setMaxSpeed(int maxSpeed) {
        this.maxSpeed = maxSpeed;
    }
}

4. 遵循 “迪米特法则”(Law of Demeter,又称 “最少知识原则”):一个类只应知道其所需要知道的内容。

    *** 一个重要的概念:需要对象,但不搜索对象,而且仅要求应用程序所需要的对象。

// bad: 违反 “迪米特法则”
public class CarViolationOfDemeter {
    private Driver driver;

    // 在这里将 Context 传递过来,而且仅处理 getDriver(),那么使用 Context 类的其他变量和方法时,就得 mock object
    CarViolationOfDemeter(Context context) {
        this.driver = context.getDriver();
    }
}

// recommend: 仅在需要的时候传递对方法和构造方法的引用。
public class CarFollowDemeter {
    private Driver driver;

    public CarFollowDemeter(Driver driver) {
        this.driver = driver;
    }
}

5. 避开隐藏的依赖项和全局状态

原因:一旦提供了对全局对象的访问权限,不仅共享了这个对象,也共享了这个对象所引用的任何对象。

// 实际应用:Reservation 依赖 DBManager,且它是一个 全局状态。 
public void makeReservation() {
    Reservation reservation = new Reservation();
    reservation.makeReservation();
        }

public class Reservation {
    public void makeReservation() {
        manager.initDatabase();  // manager is a reference to a global DBManager, already initialized
        // require the global DBManager to do more action
    }
}

// recommend:通过一个参数来调用 依赖项 DBManager,将其注入 Reservation 对象中
public void makeReservation() {
    DBManager manager = new DBManager();
    manager.initDatabase();
    Reservation reservation = new Reservation(manager);
    reservation.makeReservation();
}

6. 优先使用泛型

在应用程序中使用静态代码和不使用多态性,意味着应用程序和测试都没有代码重用,应尽量避免。

// 合并两个 Set 的方法,在编译时将报 warning 信息
public class UnionOfTwoSets {
    public static Set union(Set s1, Set s2) {
        Set result = new HashSet(s1);
        result.addAll(s2);
        return result;
    }
}

// 编译: javac UnionOfTwoSets.java -Xlint:unchecked
// 说明,上述方法是否为 static 都会警告

 

// recommend: 使用泛型方法
public class UnionOfTwoSetsWithGeneric {
    public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
        Set<E> result = new HashSet<E>(s1);
        result.addAll(s2);
        return result;
    }
}

7. 组合优于继承

许多人选择将继承作为代码重用机制,但实际上组合可能更易于测试。运行时,代码不能改变继承的层次结构,但可以组合不同的对象。

继承 只适用于 子类是超类的子类型的情况。跨越包的界限来继承一般的具体类,有可能存在风险。

// 关于 继承和组合 的相关解释(来自 baidu chat,非常棒的说明文档 :)):
在面向对象编程中,我们不能改变一个类的继承层次结构,这个结构在类定义时就已经确定了。这是因为在大多数面向对象编程语言(如Python,Java,C++)中,一个类只能继承自一个父类。
然而,我们可以通过组合不同的对象来创建新的功能。这种技术被称为"对象组合",它是面向对象设计的基本原则之一,也是符合开闭原则的(一个模块对扩展开放,对修改封闭)。
例如,假设我们有一个Engine类和一个Wheel类,我们不能让Wheel继承自Engine,但是我们可以通过在Wheel类中包含一个Engine对象来让Wheel使用Engine的功能。 在Python中,这可以通过使用组合来实现:
class Engine: def start(self): print("Engine started.") class Wheel: def __init__(self): self.engine = Engine() def start(self): self.engine.start() wheel = Wheel() wheel.start() # Output: Engine started.
在这个例子中,Wheel类不能继承自Engine类,但是我们通过在Wheel的构造函数中创建一个Engine对象并将其存储在Wheel的一个属性中,从而使Wheel能够使用Engine的功能。然后,
我们可以在Wheel类中定义一个start方法,这个方法会调用Engine的start方法。这样,当我们调用wheel.start()时,就会输出
"Engine started."。

8. 多态优于条件

// bad: 条件语句。如果新增一个文档格式,就要多写一条 case 语句
public class DocumentPrinterWithSwitch {
    private Document document;

    public enum DocumentType {
        WORD_DOCUMENT("Word Document"),
        PDF_DOCUMENT("PDF Document");

        private final String type;

        DocumentType(String type) {
            this.type = type;
        }

        // Add a method to return the corresponding enum value for comparison in switch statement.
        public static DocumentType fromString(String type) {
            for(DocumentType dt : values()) {
                if (dt.type.equals(type)) {
                    return dt;
                }
            }
            return null; // Or throw an exception if no match is found.
        }
    }

    public DocumentPrinterWithSwitch(Document document) {
        this.document = document;
    }

    public void printDocument() {
        switch (DocumentType.fromString(document.getDocumentType())) {
            case WORD_DOCUMENT:
                printWORDDocument();
                break;
            case PDF_DOCUMENT:
                printPDFDocument();
                break;
            default:
                printBinaryDocument();
                break;
        }
    }

    public void printWORDDocument() {
    }

    public void printPDFDocument() {
    }

    public void printBinaryDocument() {
    }
}
// recommend:使用多态
public class DocumentPrinterWithSwitchPolymorphism {
    public void printDocument(Document document) {
        document.printDocument();
    }
}

abstract class Document {
    public abstract void printDocument();
}

class WordDocument extends Document {
    @Override
    public void printDocument() {
        printWORDDocument();
    }

    public void printWORDDocument() {
    }
}

class PDFDocument extends Document {
    @Override
    public void printDocument() {
        printPDFDocument();
    }

    public void printPDFDocument() {
    }
}

6.3 TDD 测试驱动开发

TDD:一种编程实践,旨在引导开发人员先编写测试,再编写代码,使软件通过这些测试,然后,开发人员应该检查并重构代码,以“清理乱局”并做出改进。其目标:编写可运行的整洁代码。

 

传统的开发周期流程:[ 编码、测试、(重复)]

TDD:[ 测试、编码、重构、(重复)]

  • 重构:在不改变代码的外部行为的情况下改进软件系统内部结构的过程。

 

TDD 的两个核心步骤:

  • 在编写新代码之前先编写一个失败的测试;
  • 编写能够通过测试的最少代码;

6.4 BDD 行为驱动开发

BDD:一种方法论,用于开发直接满足业务需求的 IT 解决方法。BDD 的理念由业务策略、需求和目标驱动,这些被细化并转换为 IT 解决方法。

验收测试 是 BDD 的表达风格。验收测试的关键词:Given、When 和 Then。

 

TDD 用于构建高质量的软件,BDD 则用于构建值得构建的软件,以解决用户的问题。

6.5 突变测试

突变测试(Mutation Testing)是一种测试技术,其基本思想是自动地或半自动地生成一些修改过的版本(称为“突变体”),对程序进行测试。如果这些突变体在测试用例上产生不同的结果,那么就认为这个程序具有较高的鲁棒性;反之,如果测试用例的结果没有变化,那么就认为这个程序存在潜在的错误或缺陷。

 

在Java中,可以使用一些突变测试框架来进行测试,比如:

  1. JUnit-Mutant:这是一个基于JUnit的突变测试框架,可以与常见的构建工具(如Maven和Gradle)集成。JUnit-Mutant提供了多种突变策略,如改变操作数、交换操作顺序等,以检测代码中的潜在错误。
  2. Mutation testing Java Agent:这是一个Java代理,可以在运行时对Java类进行修改。它支持多种突变策略,如改变变量值、删除语句等。
  3. PIT:PIT是一个基于Java的突变测试框架,支持多种突变策略,如改变操作数、交换操作顺序、删除语句等。PIT可以与常见的构建工具(如Maven和Gradle)集成,并且提供了可视化界面。

使用这些框架进行突变测试可以帮助开发人员发现代码中的潜在错误和缺陷,提高程序的鲁棒性和可靠性。

在Java编程中,鲁棒性是指程序在遇到错误、异常或不可预期的情况时,能够稳定、正常地运行的特性。一个具有鲁棒性的Java程序应当能够有效地处理错误,而不是轻易崩溃或出现意料之外的行为。

为了提高Java程序的鲁棒性,可以采取以下几种策略:
异常处理:使用try-catch语句来捕获和处理可能出现的异常,防止程序因未处理的异常而崩溃。
输入验证:在程序接收到用户输入之前,先对输入进行验证,确保输入的数据是合法和有效的。
错误处理:在程序中加入错误处理逻辑,以便在发生错误时能够进行适当的处理,而不是直接崩溃。
代码重试:对于可能会失败的操作,可以尝试多次执行,以提高程序的鲁棒性。
日志记录:记录程序运行时的日志信息,以便在出现问题时能够快速定位和解决问题。
单元测试:编写单元测试来测试程序的各个部分,确保它们能够在各种情况下正常工作。
容错机制:在程序中加入容错机制,以便在出现问题时能够自动恢复程序的正常运行。
总之,提高Java程序的鲁棒性需要从多个方面入手,包括异常处理、输入验证、错误处理、代码重试、日志记录、单元测试和容错机制等。

6.6 开发周期中的测试

 

几个观点:

  • JUnit 一个强大的优点是使测试用例易于自动化;
  • 使用旧的测试防止新的变化是 回归测试 的一种形式;
  • 确保回归测试进行的最佳方法之一是将 测试套件自动化