1. 引言
InputStream
是一个常用的抽象类,用于处理来自不同源头的数据。无论数据来源于何处,使用这个类能让我们从源头抽象出来,独立于特定来源进行处理。
然而,在编写测试时,我们需要提供实际的实现。在这篇教程中,我们将学习在哪些情况下应该选择可用的实现,或者何时最好自己编写。
2. InputStream
接口基础
在开始编写自己的代码之前,理解 InputStream
接口的构建方式对我们很有帮助。幸运的是,它相当直观。要实现一个简单的 InputStream
,我们只需要考虑一个方法—— read()
。这个方法不需要参数,返回流中的下一个字节作为整数。如果 InputStream
已结束,它将返回 -1,提示我们停止处理。
2.1. 测试用例
在这个教程中,我们将测试一个处理以 InputStream
形式传输的文本消息的方法,并返回处理过的字节数。然后我们会断言读取了正确的字节数:
int bytesCount = processInputStream(someInputStream);
assertThat(bytesCount).isEqualTo(expectedNumberOfBytes);
processInputStream()
方法内部的实现在这里不太重要,所以我们使用了一个非常简单的版本:
public class MockingInputStreamUnitTest {
int processInputStream(InputStream inputStream) throws IOException {
int count = 0;
while(inputStream.read() != -1) {
count++;
}
return count;
}
}
2.2. 使用朴素实现
为了更好地理解 InputStream
的工作原理,我们将编写一个简单的实现,其中包含一个硬编码的消息。除了消息外,我们的实现还有一个指针,指向应读取下一个字节的位置。每次调用 read()
方法时,我们都会从消息中获取一个字节,然后递增索引。
在做这之前,我们还需要检查是否已经读取了消息的所有字节。如果是这样,我们需要返回 -1:
public class MockingInputStreamUnitTest {
@Test
public void givenSimpleImplementation_shouldProcessInputStream() throws IOException {
int byteCount = processInputStream(new InputStream() {
private final byte[] msg = "Hello World".getBytes();
private int index = 0;
@Override
public int read() {
if (index >= msg.length) {
return -1;
}
return msg[index++];
}
});
assertThat(byteCount).isEqualTo(11);
}
3. 使用 ByteArrayInputStream
如果我们完全确定整个数据负载可以完全装入内存,最简单的选择是 ByteArrayInputStream
。 我们向构造函数提供一个字节数组,然后流会逐字节遍历它,就像前一节示例中那样:
String msg = "Hello World";
int bytesCount = processInputStream(new ByteArrayInputStream(msg.getBytes()));
assertThat(bytesCount).isEqualTo(11);
4. 使用 FileInputStream
如果我们可以将数据保存为文件,也可以以 FileInputStream
的形式加载它。这种方法的优点是数据不会一次性全部加载到内存中,而是根据需要从磁盘读取。 如果我们将文件放在资源文件夹中,我们可以使用方便的 getResourceAsStream()
方法,一行代码就能直接从路径创建 InputStream
:
InputStream inputStream = MockingInputStreamUnitTest.class.getResourceAsStream("/mockinginputstreams/msg.txt");
int bytesCount = processInputStream(inputStream);
assertThat(bytesCount).isEqualTo(11);
注意,这个例子中的实际 InputStream
实现将是 BufferedFileInputStream
。顾名思义,它会读取更大的数据块并存储在缓冲区中,从而限制了对磁盘的读取次数。
5. 随机生成数据
有时我们希望测试系统在大量数据下是否正常工作。我们可以通过加载大文件来做到这一点,但这种方法存在一些严重问题。不仅可能浪费空间,而且像 git
这样的版本控制系统并不擅长处理大型二进制文件。幸运的是,我们不必预先拥有所有数据。相反,我们可以实时生成数据。
要做到这一点,我们需要实现我们的 InputStream
。让我们从定义字段和构造函数开始:
public class GeneratingInputStream extends InputStream {
private final int desiredSize;
private final byte[] seed;
private int actualSize = 0;
public GeneratingInputStream(int desiredSize, String seed) {
this.desiredSize = desiredSize;
this.seed = seed.getBytes();
}
}
desiredSize
变量将告诉我们何时停止生成数据。seed
变量将是一段重复的数据,而 actualSize
变量将帮助我们跟踪已返回的字节数。我们需要它,因为我们实际上并没有保存任何数据,只是返回当前的字节。
使用我们定义的变量,我们可以实现 read()
方法:
@Override
public int read() {
if (actualSize >= desiredSize) {
return -1;
}
return seed[actualSize++ % seed.length];
}
首先,我们检查是否达到了期望的大小。如果达到了,我们应该返回 -1,让流的消费者知道停止阅读。如果没有达到,我们应该从种子中返回一个字节。为了确定应该返回哪个字节,我们使用模运算符(/)获取生成数据的实际大小除以种子长度的余数。
6. 总结
在这篇教程中,我们探讨了如何在测试中处理 InputStream
。我们了解了类的构建方式以及在各种场景下可以使用的实现。最后,我们学习了如何编写自己的实现,以便实时生成数据。
如往常一样,代码示例可在 GitHub 上找到。