ChannelHandler是Netty中至关重要的元素,因此完全地测试它们应该是你开发过程中的重要部分。

在这篇文章中我们会学习一个特殊的Channel实现-EmbeddedChannel,它是用来测试ChannelHandler的。

因为正在测试的代码模块或单元将在正常运行时环境之外执行,您需要一个框架或工具来运行它。在我们的示例中,我们将使用JUnit 4作为测试框架。

EmbeddedChannel概述

Netty提供了一个embedded传输服务来支持测试handler。这个传输服务是EmbeddedChannel的特性,它提供了通过pipeline传播事件的简单方法。这个想法很简单:将输入或输出数据写入EmbeddedChannel,然后检查是否有任何内容到达ChannelPipeline的末尾。这样,您可以决定消息是否被编码或解码,以及是否触发任何ChannelHandler操作。

相关的方法如下表所示:

名称功能
writeInbound(Object… msgs)写入输入消息到EmbeddedChannel,如果数据能通过readInbound()方法从EmbeddedChannel中读,返回true
readInbound()从EmbeddedChannel中读取输入消息。返回的数据会经过整个pipeline
writeOutbound(Object… msgs)写入输出消息到EmbeddedChannel,如果现在可以通过readOutbound()从EmbeddedChannel中读取某些内容,则返回true
readOutbound()EmbeddedChannel中读取输出消息。
finish()将EmbeddedChannel标记为完成,这会调用EmbeddedChannel的close()方法

这里写图片描述

输入数据由ChannelInboundHandlers处理,表示从远程peer读取到的数据。输出数据由ChannelOutboundHandlers处理,并表示要写入远程peer的数据。上图显示在数据通过EmbeddedChannel的方法如何流过pipeline。你可以使用writeOutbound()向Channel中写入消息然后以输出的方向在pipeline中传递。随后你可以用readOutbound()读取已处理的消息,以确定结果是否符合你的预期。同样,对于输入数据你可以使用writeInbound()和readInbound()。

消息从pipeline中传递,然后被相关的ChannelInboundHandler或ChannelOutboundHandler处理,如果这条消息没有被使用,你可以通过readInbound()或readOutbound()来读取这条消息。

通过EmbeddedChannel测试ChannelHandler

测试输入消息

下图代表了一个简单的ByteToMessageDecoder(字节到消息)的实现,提供足够(3字节)的数据,这将产生固定大小的frame。如果没有足够的可读数据,它会等待下一个数据块,再次检查是否可以生成一个frame。

这里写图片描述

你可以从上图中右侧的frame可以看到,这个特定的解码器产生固定大小为3字节的frame。 因此,它可能需要不只一个事件来提供足够的字节以产生frame。最后,每个frame将被传递给ChannelPipeline中的下一个ChannelHandler。

这个解码器的实现如下所示:

public class FixedLengthFrameDecoder extends ByteToMessageDecoder {
            private final int frameLength;
            public FixedLengthFrameDecoder(int frameLength) {
                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 {
                while (in.readableBytes() >= frameLength) {
                    ByteBuf buf = in.readBytes(frameLength);
                    out.add(buf);
                }
            }
        }
    }

下面创建一个单元测试来检测它。

  @Test
    public void testFramesDecoded() {
        //创建一个ByteBuf然后保存9个字节
        ByteBuf buf = Unpooled.buffer();
        for (int i = 0; i < 9; i++) {
            buf.writeByte(i);
        }
        ByteBuf input = buf.duplicate();
        //创建一个EmbeddedChannel然后添加FixedLengthFrameDecoder
        EmbeddedChannel channel = new EmbeddedChannel(
                new FixedLengthFrameDecoder(3));
        // 将数据写入channel
        assertTrue(channel.writeInbound(input.retain()));
        //将channel标记为finish的
        assertTrue(channel.finish());
        // 读取产生的消息同时确定有3个包含3个字节的frame
        ByteBuf read = (ByteBuf) channel.readInbound();
        assertEquals(buf.readSlice(3), read);
        read.release();
        //再读一个frame
        read = (ByteBuf) channel.readInbound();
        assertEquals(buf.readSlice(3), read);
        read.release();
        //再读一个frame
        read = (ByteBuf) channel.readInbound();
        assertEquals(buf.readSlice(3), read);
        read.release();
        //验证已经没有可读的数据了
        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));
        assertFalse(channel.writeInbound(input.readBytes(2)));//返回false因为一个完整的frame还没准备好
        assertTrue(channel.writeInbound(input.readBytes(7)));
        assertTrue(channel.finish());
        ByteBuf read = (ByteBuf) channel.readInbound();
        assertEquals(buf.readSlice(3), read);
        read.release();
        read = (ByteBuf) channel.readInbound();
        assertEquals(buf.readSlice(3), read);
        read.release();
        read = (ByteBuf) channel.readInbound();
        assertEquals(buf.readSlice(3), read);
        read.release();
        assertNull(channel.readInbound());
        buf.release();
    }

方法testFramesDecoded()验证一个ByteBuf包含9可读的字节被解码为3个ByteBufs,每个包含3个字节。注意ByteBuf是如何在一次writeInbound()调用中填充9个可读字节。 之后,finish()被执行以标记EmbeddedChannel完成。 最后,readInbound()被调用并从EmbeddedChannel中精确地读取三个frame,一个null。
方法testFramesDecoded2()与第一个方法类似,但有一个区别:输入ByteBufs分为两个步骤。 当writeInbound(input.readBytes(2))被调用时,返回false。 为什么? 如上面的表所示,如果后续调用readInbound()会返回数据writeInbound()才会返回true。 但是FixedLengthFrameDecoder只有当三个或更多字节可读时才会产生输出。 其余的测试是与testFramesDecoded()相同。

测试输出消息

测试输出消息的处理与前面类似。下一个例子中,我们将展示如何使用EmbeddedChannel以编码器(将一种消息格式转换为另一种消息格式的组件)的形式测试ChannelOutboundHandler。

我们将测试一个将负数转换为绝对值的编码器-AbsIntegerEncoder。
这个例子将按如下的步骤运行:

  • 持有AbsIntegerEncoder的EmbeddedChannel将以4字节负整数的形式写入输入数据
  • 解码器将从传入的ByteBuf读取每个负整数然后调用Math.abs()获取绝对值。
  • 解码器将把每个整数的绝对值写入ChannelHandlerPipeline。

它的逻辑如下图所示:
这里写图片描述

首先给出AbsIntegerEncoder的实现:

//继承了MessageToMessageEncoder将消息从一种格式转换为另一种
public class AbsIntegerEncoder extends MessageToMessageEncoder<ByteBuf> {
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext,
                          ByteBuf in, List<Object> out) throws Exception {
        while (in.readableBytes() >= 4) {//检测是否有足够的数据来转换(因为一个int占用4字节)
            int value = Math.abs(in.readInt());
            out.add(value);
        }
    }
}

下面是测试代码:

@Test
    public void testEncoded() {
        ByteBuf buf = Unpooled.buffer();
        for (int i = 1; i < 10; i++) {
            buf.writeInt(i * -1);
        }
        EmbeddedChannel channel = new EmbeddedChannel(
                new AbsIntegerEncoder());
        assertTrue(channel.writeOutbound(buf));
        assertTrue(channel.finish());
        // read bytes
        for (int i = 1; i < 10; i++) {
            assertEquals(i, channel.readOutbound());
        }
        assertNull(channel.readOutbound());
    }

以下是代码中执行的步骤:

1.将负4字节整数写入新的ByteBuf。
2.创建一个EmbeddedChannel并为其分配一个AbsIntegerEncoder。
3.在EmbeddedChannel上调用writeOutbound()来写入ByteBuf。
4.标记通道完成。
5.从EmbeddedChannel的输出端读取所有整数,并验证是否只产生了绝对值(没有负数)。

测试异常处理

应用程序通常有额外的任务不仅仅是转换数据。例如,你可能需要处理格式不正确的输入或过大的数据量。
在下一个例子中,我们将抛出一个TooLongFrameException,如果读取的字节数量超过指定的限制。

在下图中,最大帧(frame)大小已设置为3字节。 如果一个帧超过该限制,它的字节被丢弃,并且TooLongFrameException被抛出。 pipeline中的其他ChannelHandler可以在exceptionCaught()中处理异常或忽略它。

这里写图片描述

public class FrameChunkDecoder extends ByteToMessageDecoder {
    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();
        if (readableBytes > maxFrameSize) {
            // discard the bytes
            in.clear();//丢弃这个frame中的字节
            throw new TooLongFrameException();
        }
        ByteBuf buf = in.readBytes(readableBytes);
        out.add(buf);
    }
}

下面是测试代码

@Test
    public void testFramesDecoded() {
        ByteBuf buf = Unpooled.buffer();
        for (int i = 0; i < 9; i++) {
            buf.writeByte(i);
        }
        ByteBuf input = buf.duplicate();
        EmbeddedChannel channel = new EmbeddedChannel(
                new FrameChunkDecoder(3));
        //写入2字节是OK的
        assertTrue(channel.writeInbound(input.readBytes(2)));
        try {
            //写入4字节
            channel.writeInbound(input.readBytes(4));
            //如果没有异常抛出,会执行下面的代码然后测试失败
            Assert.fail();
        } catch (TooLongFrameException e) {
            // expected exception
        }
        assertTrue(channel.writeInbound(input.readBytes(3)));
        assertTrue(channel.finish());
        // Read frames
        ByteBuf read = (ByteBuf) channel.readInbound();
        assertEquals(buf.readSlice(2), read);
        read.release();
        read = (ByteBuf) channel.readInbound();
        assertEquals(buf.skipBytes(4).readSlice(3), read);
        read.release();
        buf.release();
    }

在try/catch块这里使用的是EmbeddedChannel的一个特殊功能。 如果一个write*方法产生一个检查的异常,它将包裹在一个RuntimeException中被抛出。这样可以很容易地测试在处理过程中是否处理了异常数据。
这里的测试方法可以与任何引发异常的ChannelHandler实现一起使用。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐