【问题标题】:How can a unit test "test the contract" on a method that returns void?单元测试如何在返回 void 的方法上“测试合同”?
【发布时间】:2016-03-31 08:55:00
【问题描述】:

这里是 Java 8,但这是一个(可能)与语言无关的通用单元测试问题。

编写 JUnit 测试的语法很简单,但决定要编写什么测试以及如何测试主要/生产代码是我发现最大的问题挑战。在阅读单元测试最佳实践时,我一遍又一遍地听到同样的话:

测试合约

相信这样的想法是,单元测试不应该是脆弱的,并且如果方法的实现发生变化也不一定会中断。该方法应定义输入合同 -> 结果/结果,并且测试应旨在验证合同是否得到履行。我想。

假设我有以下方法:

public void doFizzOnBuzz(Buzz buzz, boolean isFoobaz) {
    // wsClient is a REST client for a microservice
    Widget widget = wsClient.getWidgetByBuzzId(buzz.getId());

    if(widget.needsFile()) {
        File file = readFileFromFileSystem(buzz.getFile());

        if(isFoobaz) {
            // Do something with the file (doesn't matter what)
        }
    }

    return;
}

private File readFileFromFileSystem(String filename) {
    // Private helper method; implementation doesn't matter here EXCEPT...
    // Any checked exceptions that Java might throw (as a result of working)
    // with the file system are wrapped in a RuntimeException (hence are now
    // unchecked.

    // Reads a file from the file system based on the filename/URI you specify
}

所以在这里,我们有一个我们希望为 (doFizzOnBuzz) 编写单元测试的方法。这个方法:

  • 有两个参数,buzzisFoobaz
  • 使用类属性wsClient 进行网络/REST 调用
  • 调用一个私有辅助方法,该方法不仅适用于外部文件系统,而且“吞下”已检查异常;因此readFileFromFileSystem 可以抛出RuntimeExceptions

我们可以为此编写什么样的单元测试来“测试合同”?

验证输入(buzzisFoobaz)是显而易见的;合同应定义每一个的有效值/状态,以及如果它们无效应发生哪些异常/结果。

但除此之外,我不确定这里的“合同”是什么,这使得为其编写测试非常困难。所以我想这个问题真的应该是“我如何确定单元测试的合同是什么,然后你如何编写针对合同而不是实现的测试?” p>

但是对于 SO 问题来说,这个标题太长了。

【问题讨论】:

  • test the contract 只是意味着给定您输入的先决条件,测试您的程序的后置条件是否成立,您的示例是不完整的,因为您没有给出先决条件和doFizzOnBuzz的后置条件
  • 该方法设计得不好,因此很难以合理的方式进行测试。您在这里承担了多项职责——业务逻辑、“小部件查找”(即依赖关系解析)、文件读取等等。难怪很难测试。
  • 合约测试是通过将方法视为黑盒并将各种参数发送给方法来完成的。预计该方法不会遇到任何未记录的错误。因此,在您的情况下,如果您向该方法发送 null Buzz,您将面临可能违反合同的NullPointerException。如果方法声明了任何抛出异常,你也需要测试这些。
  • 我将“定义合同”放在“测试合同”之前。这实际上是 TDD 如何导致更好的设计的一个很好的例子。您需要先为您的方法定义精确的要求,然后再从那里着手。

标签: java unit-testing design-by-contract


【解决方案1】:

使用doFizzOnBuzz(Buzz buzz, boolean isFoobaz)private File readFileFromFileSystem(String filename) 方法的代码不容易测试,因为第一种方法会尝试读取文件,而这不是您想要在测试中做的事情。

在这里,doFizzOnBuzz 需要一些东西来提供一个 File 以供它使用。这个FileProvider(我会这么称呼它)可能是一个接口,类似于:

public interface FileProvider {
  File getFile(String filename);
}

在生产中运行时,会使用从磁盘实际读取文件的实现,但在单元测试doFizzOnBuzz 时,可以使用FileProvider 的模拟实现。这将返回一个模拟 File

要记住的关键点是,在测试doFizzOnBuzz 时,我们测试提供文件的任何内容或其他任何内容。我们假设它可以正常工作。这些其他代码有自己的单元测试。

可以使用诸如Mockito 之类的模拟框架来创建FileProviderFile 的模拟实现,并将模拟FileProvider 注入到被测类中,可能使用setter:

public void setFileProvider(FileProvider f) {
  this.fileProvider = f;
}

另外,我不知道wsClient 是什么,但我知道它有一个getWidgetByBuzzId() 方法。这个类也可以是一个接口,出于测试目的,该接口将被模拟,并返回一个模拟Widget,类似于上面的 FileProvider。

使用 mockito,您不仅可以设置接口的模拟实现,还可以定义在该接口上调用方法时返回的值:例如

//setup mock FileProvider
FileProvider fp = Mockito.mock(FileProvider.class);

//Setup mock File for FileProvider to return
File mockFile = Mockito.mock(File.class);
Mockito.when(mockFile.getName()).thenReturn("mockfilename");
//other methods...

//Make mock FileProvider return mock File
Mockito.when(fp.getFile("filename")).thenReturn(mockFile);

ClassUnderTest test = new ClassUnderTest();
test.setFileProvider(fp); //inject mock file provider

//Also set up mocks for Buzz,, Widget, and anything else

//run test
test.doFizzOnBuzz(...)

//verify that FileProvider.getFile() was actually called:
Mockito.verify(fp).getFile("filenane"); 

如果 getFile() 未使用参数 'filename' 调用,则上述测试失败

结论 如果您不能直接观察方法的结果,例如它是无效的,您可以使用 Mocking 来验证它与其他类和方法的交互。

【讨论】:

  • 在我看来,你应该更喜欢实现接口的存根而不是模拟。它们使每个测试更脆弱,更难阅读。
  • 我的意思更多的是你应该小心测试中的模拟。您的示例使用两个模拟,但仅验证一个。为什么不返回一个存根文件,这样您就不需要对文件执行其他设置来通过不相关的测试。
  • 您的验证中还有一个错字,您正在寻找"filenane"。我不能建议这么小的编辑
  • 谢谢@NickJ,那么根据单元测试最佳实践,什么时候可以使用private 方法?因为听起来您(可能?)提倡删除私有readFileFromFileSystem 方法来代替FileProvider 接口和相应的子类。如果一个私有方法被同一个类中的多个方法使用怎么办?这个可重用的代码如何影响这些公共方法的契约?
  • 或者@LukeW 可能知道...?
【解决方案2】:

问题是你的合约方法并没有说明你可以从外部观察到什么效果。它基本上是一个 BiConsumer,所以除了确保是否有异常之外,没有太多的单元测试可能。

您可以做的测试是确保调用 (Mocked) REST 服务,或者文件(Buzz 参数的一部分,可能指向临时文件)在某些情况下会受到该方法的影响.

如果您想对方法的输出进行单元测试,您可能需要重构以将确定应该做什么(文件需要更新)与实际执行分开。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-05-10
    • 1970-01-01
    • 2014-11-08
    • 2022-01-09
    • 1970-01-01
    • 2012-12-15
    • 2021-10-02
    • 2019-10-27
    相关资源
    最近更新 更多