在基于 TDD 的单元测试中处理 I/O

Handle I/O in TDD based unit tests

我正在练习 Java.based TDD 并编写了一些代码,用于读取和写入文件。现在,我对每个场景(读取和写入)进行了大约 100 个测试来测试我的代码,我每次都在其中创建文件或读取给定文件。要写入的文件将在临时目录中创建,并在每次测试后删除 运行。 但是这种策略会产生很多 I/O 并且恐怕例如固态硬盘寿命。模拟不是一种选择。

一种可能性是 reade/write 一次文件然后 运行 我针对(静态)数据结构(伪代码)的测试:

private static Object resultData = null;

@BeforeClass
// Read/Write my stuff here
resultData = ....

@Test
// Check my requirements
assertTrue(resultData....);

问题是,我可以在测试方法中改变预期的行为,所以我的测试不再是自主的。

你会怎么处理?

But this strategy produces a lot of I/O and I'm afraid in case of e.g. SSD lifetime.

你想多了:SSD Disk(今天已经足够标准)的 DWPD(驱动器每天写入次数)适合许多用例。
DWPD 测量您可以在一天的生命周期内覆盖驱动器的整个大小的次数。
对于 500 GO 磁盘,DWPD 到 1 意味着理论上您可以每天写入 500 GO 而未达到产品保修政策期限。
这通常在 5 到 10 年之间。
因此,如果您每天执行数百万次构建,则每次构建(100 个或更多)时创建的一些临时文件几乎什么都不是。

此外,您不应该因为这种考虑而使代码或测试变得更复杂。
SSD 的存在是为了让开发人员的事情变得更简单,而不是更复杂。

话虽这么说,但在单元测试中避免写入如此多的文件仍然有意义,因为必须快速执行测试。
您可以更改测试 class 的 API 以使其也接受 ByteArrayInputStreamByteArrayOutputStream。这样读写将在内存中进行,而不是在文件系统中进行。

How would you deal with it?

我会重构代码,以便使用测试替身(又名模拟)一个选项。

具体来说,我希望创建这样的接缝

  1. 实现I/O的代码太简单了,不会失败
  2. 所有复杂的代码都不关心 I/O 是如何实现的。

在像 java 这样的语言中,接缝就是一个接口。我的 I/O 代码会实现接口,但它本身不会在测试中成为 运行。

相反,测试将使用合适的测试替身 -- 实现相同接口但实际上不执行磁盘写入的东西。

注意:如果我想要模拟各种写入错误的测试,以确保复杂的逻辑正确处理这些失败,可能会有多种类型的测试替身。

生产代码的 composition root 当然需要创建 "real" 实现的实例才能使用。但是测试的组合根可以代替 select 测试替身,或内存文件系统,或任何确保测试结果是确定性的东西(顺便说一句,不要咀嚼你的 SSD 寿命).

我们从 Hoare

了解到幕后代码的启发式方法

One way [of constructing a software design] is to make it so simple that there are obviously no deficiencies