协作对象对测试带来的问题

在本节中,我们将了解随着我们的软件发展成一个更大的代码库时出现的挑战。我们将回顾协作对象的含义,然后我们将看看两个难以测试的协作示例。

随着我们的软件系统的发展,我们很快就会超出单个类(或函数)所能容纳的范围。我们将把代码分成多个部分。如果我们选择一个对象作为我们的测试对象,那么它依赖的任何其他对象都是协作者。我们的 TDD 测试必须考虑到这些协作者的存在。有时,这很简单,正如我们在前面的章节中看到的那样。

不幸的是,事情并不总是那么简单。有些协作使测试难以——甚至不可能——编写。这些类型的协作者引入了我们必须应对的不可重复行为,或者提出了难以触发的错误。

让我们通过一些简短的示例来回顾这些挑战。我们将从一个常见问题开始:一个表现出不可重复行为的协作者。

测试不可重复行为的挑战

我们已经了解到 TDD 测试的基本步骤是 ArrangeActAssert。我们要求对象行动,然后断言预期的结果会发生。但是,当结果不可预测时会发生什么?

为了说明这一点,让我们回顾一个掷骰子并显示我们掷出的数字的文本字符串的类:

package examples;

import java.util.random.RandomGenerator;

public class DiceRoll {
    private final int NUMBER_OF_SIDES = 6;
    private final RandomGenerator rnd = RandomGenerator.getDefault();

    public String asText() {
        int rolled = rnd.nextInt(NUMBER_OF_SIDES) + 1;

        return String.format("You rolled a %d", rolled);
    }
}

这段代码足够简单,其中只有几行可执行代码。遗憾的是,编写简单并不总是测试简单。我们如何为这个编写测试?具体来说——我们如何编写断言?

在之前的测试中,我们总是确切地知道断言中期望什么。在这里,断言将是一些固定文本加上一个随机数。我们事先不知道那个随机数会是什么。

测试错误处理的挑战

测试处理错误条件的代码是另一个挑战。这里的困难不在于断言错误是否被处理,而在于如何触发协作对象内部发生该错误。

为了说明这一点,让我们想象一个代码,当我们的便携设备电池电量低时警告我们:

public class BatteryMonitor {
    public void warnWhenBatteryPowerLow() {
        if (DeviceApi.getBatteryPercentage() < 10) {
            System.out.println("Warning - Battery low");
        }
    }
}

前面的 BatteryMonitor 代码中有一个 DeviceApi 类,这是一个库类,让我们可以读取手机上剩余的电池电量。它提供了一个静态方法来实现这一点,称为 getBatteryPercentage()。这将返回一个 0100% 的整数。我们想要为其编写 TDD 测试的代码调用 getBatteryPercentage(),如果它小于 10%,将显示警告消息。但编写这个测试有一个问题:我们如何强制 getBatteryPercentage() 方法作为 Arrange 步骤的一部分返回一个小于 10 的数字?我们会以某种方式放电吗?我们该怎么做?

BatteryMonitor 提供了一个与另一个对象协作的代码示例,其中无法强制从该协作者获得已知响应。我们无法更改 getBatteryPercentage() 将返回的值。我们实际上必须等到电池放电后,这个测试才能通过。这不是 TDD 的目的。

理解为何这些协作关系具有挑战性

在进行 TDD 时,我们希望测试快速且可重复。任何涉及不可预测行为或需要我们控制我们无法控制的情况的场景显然会给 TDD 带来问题。

在这些情况下编写测试的最佳方法是消除困难的根源。幸运的是,存在一个简单的解决方案。我们可以应用我们在前一章中学到的依赖注入原则,以及一个新概念——测试替身。我们将在下一节中回顾测试替身。