CH10-单元测试

本章介绍

  • 单元测试
  • EmbeddedChannel

学会了使用一个或多个 ChannelHandler 处理接收/发送数据消息,但是如何测试它们呢?Netty 提供了2个额外的类使得测试 ChannelHandler变得很容易,本章讲解如何测试 Netty 程序。测试使用 JUnit4,如果不会用可以慢慢了解。JUnit4 很简单,但是功能很强大。

本章将重点讲解测试已实现的 ChannelHandler 和编解码器

总览

我们已经知道,ChannelHandler 实现可以串联在一起,以构建ChannelPipeline 的处理逻辑。我们先前解释说,这个设计方法 支持潜在的复杂的分解处理成小和可重用的组件,其中每个一个定义良好的处理任务或步骤。在这一章里,我们将展示它简化了测试。

Netty 的促进 ChannelHandler 的测试通过的所谓“嵌入式”传输。这是由一个特殊 Channel 实现,EmbeddedChannel,它提供了一个简单的方法通过管道传递事件。

想法很简单:你入站或出站数据写入一个E mbeddedChannel 然后检查是否达到 ChannelPipeline 的结束。这样你可以确定消息编码或解码和ChannelHandler 是否操作被触发。

在表10.1中列出了相关方法。

名称职责
writeInbound写一个入站消息到 EmbeddedChannel。 如果数据能从 EmbeddedChannel 通过 readInbound() 读到,则返回 true
readInbound从 EmbeddedChannel 读到入站消息。任何返回遍历整个ChannelPipeline。如果读取还没有准备,则此方法返回 null
writeOutbound写一个出站消息到 EmbeddedChannel。 如果数据能从 EmbeddedChannel 通过 readOutbound() 读到,则返回 true
readOutbound从 EmbeddedChannel 读到出站消息。任何返回遍历整个ChannelPipeline。如果读取还没有准备,则此方法返回 null
Finish如果从入站或者出站中能读到数据,标记 EmbeddedChannel 完成并且返回。这同时会调用 EmbeddedChannel 的关闭方法

测试入站和出站数据

处理入站数据由 ChannelInboundHandler 处理并且表示数据从远端读取。出站数据由 ChannelOutboundHandler 处理并且表示数据写入远端。 根据 ChannelHandler 测试你会选择 writeInbound(),writeOutbound(), 或者两者都有。

图10.1显示了数据流如何通过 ChannelPipeline 使用 EmbeddedChannel 的方法。

NAME

Figure 10.1 EmbeddedChannel data flow

如上图所示,使用 writeOutbound() 写消息到 Channel,消息在出站方法通过 ChannelPipeline,之后就可以使用 readOutbound() 读取消息。着同样使用与入站,使用 writeInbound() 和 readInbound()。处在

每种情况下,消息是通过 ChannelPipeline 并被有关ChannelInboundHandler 或 ChannelOutboundHandler 进行处理。如果消息是不消耗您可以使用 readInbound() 或 readOutbound() 适当的读到 Channel 处理后的消息。

让我们仔细看一下这两个场景,看看他们如何适用于测试您的应用程序逻辑。

测试 ChannelHandler

本节,将使用 EmbeddedChannel 来测试 ChannelHandler

测试入站消息

我们来编写一个简单的 ByteToMessageDecoder 实现,有足够的数据可以读取时将产生固定大小的包,如果没有足够的数据可以读取,则会等待下一个数据块并再次检查是否可以产生一个完整包。

如图所示,它可能会占用一个以上的“event”以获取足够的字节产生一个数据包,并将它传递到 ChannelPipeline 中的下一个 ChannelHandler,

NAME

Figure 10.2 Decoding via FixedLengthFrameDecoder

实现如下:

Listing 10.1 FixedLengthFrameDecoder implementation

public class FixedLengthFrameDecoder extends ByteToMessageDecoder { //1

    private final int frameLength;

    public FixedLengthFrameDecoder(int frameLength) { //2
        if (frameLength <= 0) {
            throw new IllegalArgumentException(
                    "frameLength must be a positive integer: " + frameLength);
        }
        this.frameLength = frameLength;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (in.readableBytes() >= frameLength) { //3
            ByteBuf buf = in.readBytes(frameLength);//4
            out.add(buf); //5
        }
    }
}
  1. 继承 ByteToMessageDecoder 用来处理入站的字节并将他们解码为消息
  2. 指定产出的帧的长度
  3. 检查是否有足够的字节用于读到下个帧
  4. 从 ByteBuf 读取新帧
  5. 添加帧到解码好的消息 List

下面是单元测试的例子,使用 EmbeddedChannel

Listing 10.2 Test the FixedLengthFrameDecoder

public class FixedLengthFrameDecoderTest {

    @Test    //1
    public void testFramesDecoded() {
        ByteBuf buf = Unpooled.buffer(); //2
        for (int i = 0; i < 9; i++) {
            buf.writeByte(i);
        }
        ByteBuf input = buf.duplicate();

        EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3)); //3
        Assert.assertFalse(channel.writeInbound(input.readBytes(2))); //4
        Assert.assertTrue(channel.writeInbound(input.readBytes(7)));

        Assert.assertTrue(channel.finish());  //5
        ByteBuf read = (ByteBuf) channel.readInbound();
        Assert.assertEquals(buf.readSlice(3), read);
        read.release();

        read = (ByteBuf) channel.readInbound();
        Assert.assertEquals(buf.readSlice(3), read);
        read.release();

        read = (ByteBuf) channel.readInbound();
        Assert.assertEquals(buf.readSlice(3), read);
        read.release();

        Assert.assertNull(channel.readInbound());
        buf.release();
    }


    @Test
    public void testFramesDecoded2() {
        ByteBuf buf = Unpooled.buffer();
        for (int i = 0; i < 9; i++) {
            buf.writeByte(i);
        }
        ByteBuf input = buf.duplicate();

        EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3));
        Assert.assertFalse(channel.writeInbound(input.readBytes(2)));
        Assert.assertTrue(channel.writeInbound(input.readBytes(7)));

        Assert.assertTrue(channel.finish());
        ByteBuf read = (ByteBuf) channel.readInbound();
        Assert.assertEquals(buf.readSlice(3), read);
        read.release();

        read = (ByteBuf) channel.readInbound();
        Assert.assertEquals(buf.readSlice(3), read);
        read.release();

        read = (ByteBuf) channel.readInbound();
        Assert.assertEquals(buf.readSlice(3), read);
        read.release();

        Assert.assertNull(channel.readInbound());
        buf.release();
    }
}
  1. 测试增加 @Test 注解
  2. 新建 ByteBuf 并用字节填充它
  3. 新增 EmbeddedChannel 并添加 FixedLengthFrameDecoder 用于测试
  4. 写数据到 EmbeddedChannel
  5. 标记 channel 已经完成
  6. 读产生的消息并且校验

如上面代码,testFramesDecoded() 方法想测试一个 ByteBuf,这个ByteBuf 包含9个可读字节,被解码成包含了3个可读字节的 ByteBuf。你可能注意到,它写入9字节到通道是通过调用 writeInbound() 方法,之后再执行 finish() 来将 EmbeddedChannel 标记为已完成,最后调用readInbound() 方法来获取 EmbeddedChannel 中的数据,直到没有可读字节。testFramesDecoded2() 方法采取同样的方式,但有一个区别就是入站ByteBuf分两步写的,当调用 writeInbound(input.readBytes(2)) 后返回 false 时,FixedLengthFrameDecoder 值会产生输出,至少有3个字节是可读,testFramesDecoded2() 测试的工作相当于testFramesDecoded()。

Testing outbound messages

测试的处理出站消息类似于我们刚才看到的一切。这个例子将使用的实现MessageToMessageEncoder:AbsIntegerEncoder。

  • 当收到 flush() 它将从 ByteBuf 读取4字节整数并给每个执行Math.abs()。
  • 每个整数接着写入 ChannelHandlerPipeline

图10.3显示了逻辑。

Figure 10.3 Encoding via AbsIntegerEncoder

示例如下:

Listing 10.3 AbsIntegerEncoder

public class AbsIntegerEncoder extends MessageToMessageEncoder<ByteBuf> {  //1
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> out) throws Exception {
        while (in.readableBytes() >= 4) { //2
            int value = Math.abs(in.readInt());//3
            out.add(value);  //4
        }
    }
}
  1. 继承 MessageToMessageEncoder 用于编码消息到另外一种格式
  2. 检查是否有足够的字节用于编码
  3. 读取下一个输入 ByteBuf 产出的 int 值,并计算绝对值
  4. 写 int 到编码的消息 List

在前面的示例中,我们将使用 EmbeddedChannel 测试代码。清单10.4

Listing 10.4 Test the AbsIntegerEncoder

public class AbsIntegerEncoderTest {

    @Test   //1
    public void testEncoded() {
        ByteBuf buf = Unpooled.buffer();  //2
        for (int i = 1; i < 10; i++) {
            buf.writeInt(i * -1);
        }

        EmbeddedChannel channel = new EmbeddedChannel(new AbsIntegerEncoder());  //3
        Assert.assertTrue(channel.writeOutbound(buf)); //4

        Assert.assertTrue(channel.finish()); //5
        for (int i = 1; i < 10; i++) {
            Assert.assertEquals(i, channel.readOutbound());  //6
        }
        Assert.assertNull(channel.readOutbound());
    }
}
  1. 用 @Test 标记
  2. 新建 ByteBuf 并写入负整数
  3. 新建 EmbeddedChannel 并安装 AbsIntegerEncoder 来测试
  4. 写 ByteBuf 并预测 readOutbound() 产生的数据
  5. 标记 channel 已经完成
  6. 读取产生到的消息,检查负值已经
  7. 编码为绝对值

测试异常处理

有时候传输的入站或出站数据不够,通常这种情况也需要处理,例如抛出一个异常。这可能是你错误的输入或处理大的资源或其他的异常导致。我们来写一个实现,如果输入字节超出限制长度就抛出TooLongFrameException,这样的功能一般用来防止资源耗尽。看下图:

在图10.4最大帧大小被设置为3个字节。

NAME

Figure 10.4 Decoding via FrameChunkDecoder

上图显示帧的大小被限制为3字节,若输入的字节超过3字节,则超过的字节被丢弃并抛出 TooLongFrameException。在 ChannelPipeline 中的其他ChannelHandler 实现可以处理 TooLongFrameException 或者忽略异常。处理异常在 ChannelHandler.exceptionCaught() 方法中完成,ChannelHandler 提供了一些具体的实现,看下面代码:

public class FrameChunkDecoder extends ByteToMessageDecoder {  //1

    private final int maxFrameSize;

    public FrameChunkDecoder(int maxFrameSize) {
        this.maxFrameSize = maxFrameSize;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        int readableBytes = in.readableBytes();  //2
        if (readableBytes > maxFrameSize)  {
            // discard the bytes   //3
            in.clear();
            throw new TooLongFrameException();
        }
        ByteBuf buf = in.readBytes(readableBytes); //4
        out.add(buf);  //5
    }
}
  1. 继承 ByteToMessageDecoder 用于解码入站字节到消息
  2. 指定最大需要的帧产生的体积
  3. 如果帧太大就丢弃并抛出一个 TooLongFrameException 异常
  4. 同时从 ByteBuf 读到新帧
  5. 添加帧到解码消息 List

示例如下:

Listing 10.6 Testing FixedLengthFrameDecoder

public class FrameChunkDecoderTest {

    @Test    //1
    public void testFramesDecoded() {
        ByteBuf buf = Unpooled.buffer();  //2
        for (int i = 0; i < 9; i++) {
            buf.writeByte(i);
        }
        ByteBuf input = buf.duplicate();

        EmbeddedChannel channel = new EmbeddedChannel(new FrameChunkDecoder(3));  //3
        Assert.assertTrue(channel.writeInbound(input.readBytes(2)));  //4
        try {
            channel.writeInbound(input.readBytes(4)); //5
            Assert.fail();  //6
        } catch (TooLongFrameException e) {
            // expected
        }
        Assert.assertTrue(channel.writeInbound(input.readBytes(3)));  //7


        Assert.assertTrue(channel.finish());  //8

        ByteBuf read = (ByteBuf) channel.readInbound();
        Assert.assertEquals(buf.readSlice(2), read); //9
        read.release();

        read = (ByteBuf) channel.readInbound();
        Assert.assertEquals(buf.skipBytes(4).readSlice(3), read);
        read.release();

        buf.release();
    }
}
  1. 使用 @Test 注解
  2. 新建 ByteBuf 写入 9 个字节
  3. 新建 EmbeddedChannel 并安装一个 FixedLengthFrameDecoder 用于测试
  4. 写入 2 个字节并预测生产的新帧(消息)
  5. 写一帧大于帧的最大容量 (3) 并检查一个 TooLongFrameException 异常
  6. 如果异常没有被捕获,测试将失败。注意如果类实现 exceptionCaught() 并且处理了异常 exception,那么这里就不会捕捉异常
  7. 写剩余的 2 个字节预测一个帧
  8. 标记 channel 完成
  9. 读到的产生的消息并且验证值。注意 assertEquals(Object,Object)测试使用 equals() 是否相当,不是对象的引用是否相当

即使我们使用 EmbeddedChannel 和 ByteToMessageDecoder。

应该指出的是,同样的可以做每个 ChannelHandler 的实现,将抛出一个异常。

乍一看,这看起来很类似于测试我们写在清单10.2中,但它有一个有趣的转折,即 TooLongFrameException 的处理。这里使用的 try/catch 块是 EmbeddedChannel 的一种特殊的特性。如果其中一个“write*“编写方法产生一个受控异常将被包装在一个 RuntimeException。这使得测试更加容易,如果异常处理的一部分处理。

总结

使用测试工具,如JUnit单元测试是一个非常有效的方式保证代码的正确性,提高其可维护性。在本章中,您了解了如何测试定制 ChannelHandler 来验证他们的工作。

在接下来的章节我们将专注于写 Netty “真实世界” 的应用程序。即使我们任何进一步的测试代码的例子,但希望你能记住我们的测试方法的探讨及其重要性。