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 上找到。