【问题标题】:How to test a void method before implementing it?如何在实现之前测试 void 方法?
【发布时间】:2018-06-02 22:20:21
【问题描述】:

我正在开发一个新项目。这是迄今为止所做的:

  1. 技术设计。
  2. 模型类(数据类)。
  3. 项目中的所有接口(但还没有实现)。

接下来我想做的是实现从骨架(高级方法)到嵌套对象的方法。不过,我想在编写实现之前为每个方法创建一个单元测试。首先实现高级方法不会有任何问题,因为我将使用接口并仅使用 DI 在外部 Java 配置文件中绑定具体实现。

我要实现的第一个方法称为lookForChanges(),它既接受又返回void。这个方法由 Spring 的调度程序 (@Scheduled) 调用,它管理整个过程:它从 DB 检索数据,从 Web 服务检索数据,比较它们,如果有任何变化,它会更新数据库并发送一个发送给客户端的 JMS 消息。当然,它不会自己做所有这些事情,而是调用相关的类和方法。

所以我遇到的第一个问题是如何为 void 方法创建单元测试。在所有教程中,测试的方法总是接受参数并返回结果。我找到了in this question 的答案。他说,即使没有结果要检查,至少可以确保被测方法中的方法被调用并且参数顺序正确。

我有点喜欢这个答案,但问题是我正在使用 TDD,所以与提出这个问题的人不同,我在实现测试方法之前编写测试,所以我还不知道它将使用哪些方法和顺序。我可以猜到,但只有在方法已经实现后,我才能确定这一点。

那么,如何在实现之前测试一个 void 骨架方法?

【问题讨论】:

  • 该方法应该做什么?这决定了你应该测试什么。 void 方法更难测试,因为您需要在测试代码中观察副作用,这在某种程度上被认为是一种反模式。那么,lookForChanges() 在被测类的状态下应该修改什么?
  • @MickMnemonic 我已经解释了它应该做什么:与 DB 层交互以检索数据,与 Web 服务层交互以检索数据,比较两个地方的数据,如果两者之间存在差异然后它与 DB 层交互以更新 DB,并与 JMS 层交互以向客户端发送通知。它不会更改对象状态(名为 MainService)中的任何内容。
  • 很公平。该方法基本上等同于main(),并且为它编写单元测试当然是可能的,但不会增加太多价值,因为您主要测试与模拟(DB,WS,JMS)的交互。也许可以将数据比较提取到一个单独的、可单元测试的方法/类中?
  • @MickMnemonic 是的,当然,此方法不会自己进行比较,而是会调用另一个方法来进行比较。但是我怎样才能测试那个“主要”方法呢?我的意思是如果它没有返回任何值并且我还没有编写实现,我可以测试什么?如果我确实编写了实现,我至少可以测试所有方法都被调用(使用间谍),以防有人删除任何行。
  • 我假设您的意思是您还没有编写依赖项的实现。这就是您使用模拟的目的;如果您的代码使用 DI 并且您针对接口进行编码,那么这应该没问题。我可以尝试在答案中对此进行更多解释。

标签: java unit-testing oop design-patterns tdd


【解决方案1】:

所以我遇到的第一个问题是如何为 void 方法创建单元测试。

void 方法意味着合作者。你验证这些。

示例。假设我们需要一个将System.in 复制到System.out 的任务。我们将如何为此编写自动化测试?

void copy() {
    // Does something clever with System.in and System.out
}

但如果你稍微眯起眼睛,你会发现你的代码确实看起来像

void copy() {
    InputStream in = System.in;
    PrintStream out = System.out;
    // Does something clever with `in` and `out`
}

如果我们对此执行并提取方法重构,那么我们最终可能会得到如下代码

void copy() {
    InputStream in = System.in;
    PrintStream out = System.out;
    copy(in, out);
}

void copy(InputStream in, PrintStream out) {
    // Does something clever with `in` and `out`
}

后者是我们可以测试的 API - 我们配置协作者,将它们传递给被测系统,然后验证更改。

目前,我们还没有对void copy() 进行测试,但没关系,因为那里的代码“非常简单,显然没有任何缺陷”。

请注意,从测试的角度来看,以下设计之间没有太大区别

{
    Task task = new Task();
    task.copy(in, out);
}

{
    Task task = new Task(in, out);
    task.copy();
}

{
    Task task = Task.createTask();
    task.copy(in, out)
}

{
    Task task = Task.createTask(in, out);
    task.copy();
}

一种思考方式是:我们不先写API,我们先写测试

// Arrange the test context to be in the correct initial state
// ???
// Verify that the test context arrived in final state consistent with the specification.

也就是说,在您开始考虑 API 之前,您首先需要弄清楚您将如何评估结果。

相同的想法,不同的拼写:如果函数调用的效果无法检测到,那么您还不如只发布一个无操作。如果无操作不符合您的要求,那么某处必须有可观察到的效果——您只需要确定是直接观察到该效果(检查返回值)还是通过代理(检查对某些解决方案中的其他元素,或扮演该元素角色的测试替身)。

好的,现在我可以将参数传递给测试它的方法,但是我可以测试什么?

你测试它应该做什么。

试试这个思想实验 - 假设你和我结对,而你提出了这个界面

interface Task {
    void lookForChanges();
}

然后,经过深思熟虑,我实现了这个:

class NoOpTask implements Task {
    @Override
    void lookForChanges() {}
}

您将如何证明我的实现不满足要求?

您在问题中写的是“它更新数据库并向客户端发送 JMS 消息”,因此需要考虑两个断言 - 数据库是否已更新,是否发送了 JMS 消息?

整个事情看起来像这样

Given:
    A database with data `A`
    A webservice with data `B`
    A JMS client with no messages

When:
    The task is connected to this database, webservice, and JMS client
    and the task is run

Then:
    The database is updated with data `B`
    The JMS client has a message.

看起来你建议的是端到端测试。

它看起来确实像一个。但是,如果您对这些协作者使用测试替身,而不是实时系统,那么您的测试将在一个独立且确定性的 shell 中运行。

这可能是一个社交测试——测试不知道也不关心 when 子句中被测系统的实现细节。我没有声称 SUT 是一个完全一致的“单元”。

我得先看看 foo 的实现。我错了吗?

是的 - 您需要了解 foo 的规范,而不是实现。

【讨论】:

  • 感谢您详细的回答。我喜欢这种方法,我会考虑改变我的设计以使其工作。但是,我担心它并不能解决主要问题,即我的 lookForChanges() 方法 returns 无效。好的,现在我可以将参数传递给测试它的方法,但是我可以测试什么?它不返回任何值,我还不知道它会调用哪些方法(我可以猜到,但在我编写实现之前还不能 100% 确定)。
  • 再次感谢您的详细说明。看起来您建议的是端到端测试。想象一下,我有两个方法:foo() 和 bar()。 foo 使用 bar。现在栏已损坏。我仍然希望 foo 通过测试。可以通过在 foo 的单元测试中提供 bar 的模拟来完成。那么这将是一个单元测试,但为此,我必须先查看 foo 的实现。我错了吗?
猜你喜欢
  • 1970-01-01
  • 2014-01-04
  • 2020-12-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-20
  • 2021-08-27
  • 1970-01-01
相关资源
最近更新 更多