在 Wordz 中测试错误条件

在本节中,我们将通过编写一个测试来应用我们所学到的知识,该测试将为一个类选择一个随机单词供玩家猜测,从存储的单词集中选择。我们将创建一个名为 WordRepository 的接口来访问存储的单词。我们将通过 fetchWordByNumber(wordNumber) 方法来实现这一点,其中 wordNumber 标识一个单词。这里的设计决策是每个单词都存储有一个从1开始的顺序号,以帮助我们随机选择一个。

我们将编写一个 WordSelection 类,它负责选择一个随机数并使用该数字从存储中获取标记有该数字的单词。我们将使用我们之前的 RandomNumbers 接口。对于这个例子,我们的测试将涵盖我们尝试从 WordRepository 接口获取单词,但由于某种原因,它不存在的情况。

我们可以编写如下测试:

@ExtendWith(MockitoExtension.class)
public class WordSelectionTest {
    @Mock
    private WordRepository repository;

    @Mock
    private RandomNumbers random;

    @Test
    public void reportsWordNotFound() {
        doThrow(new WordRepositoryException())
            .when(repository)
            .fetchWordByNumber(anyInt());

        var selection = new WordSelection(repository, random);

        assertThatExceptionOfType(WordSelectionException.class)
            .isThrownBy(() -> selection.getRandomWord());
    }
}

该测试捕获了更多关于我们如何打算让 WordRepositoryWordSelection 工作的设计决策。如果检索单词时出现任何问题,我们的 fetchWordByNumber(wordNumber) 存储库方法将抛出 WordRepositoryException。我们的意图是让 WordSelection 抛出其自己的自定义异常,以报告它无法完成 getRandomWord() 请求。

为了在测试中设置这种情况,我们首先安排存储库抛出异常。这是使用 MockitodoThrow() 功能完成的。每当调用 fetchWordByNumber() 方法时,无论我们传递什么参数,Mockito 都会抛出我们要求它抛出的异常,即 WordRepositoryException。这使我们能够驱动出处理此错误条件的代码。

我们的 Arrange 步骤通过创建 WordSelection SUT 类完成。我们向构造函数传递了两个协作者:WordRepository 实例和 RandomNumbers 实例。我们通过向测试替身 repositoryrandom 字段添加 @Mock 注解,要求 Mockito 为这两个接口创建存根。

现在 SUT 已正确构建,我们准备编写测试的 ActAssert 步骤。我们正在测试是否抛出异常,因此我们需要使用 AssertJassertThatExceptionOfType() 功能来做到这一点。我们可以传入我们期望抛出的异常类,即 WordSelectionException。我们链接 isThrownBy() 方法来执行 Act 步骤并使我们的 SUT 代码运行。这是作为 Java lambda 函数作为 isThrownBy() 方法的参数提供的。这将调用 getRandomWord() 方法,我们打算让它失败并抛出异常。断言将确认这已经发生,并且抛出了预期的异常类。我们将运行测试,看到它失败,然后添加必要的逻辑以使测试通过。

测试代码向我们展示了我们可以使用测试替身和错误条件的验证与测试优先的 TDD。它还表明测试可以很容易地与解决方案的特定实现耦合。在这个测试中有很多关于哪些异常发生以及它们在哪里使用的设计决策。这些决策甚至包括使用异常来报告错误的事实。尽管如此,这仍然是拆分职责和定义组件之间合同的合理方式。所有这些都捕获在测试中。