ChannelHandler是Netty应用程序的关键元素,因此完全地测试他们应该是你的开发过程的一个标准部分。最佳实践要求你的测试不只要可以证实你的实现是正确的,并且还要可以很容易地隔离那些因修改代码而忽然出现的问题。这种类型的测试叫作单元测试。ide
其基本思想是,以尽量小的区块测试你的代码,而且尽量地和其余的代码模块以及运行时的依赖相隔离。单元测试
一、EmbeddedChannel概述测试
你已经知道,能够将ChannelPipeline中的ChannelHandler实现链接在一块儿,以构建你的应用程序的业务逻辑。以前已经解释过,这种设计支持将任何潜在的复杂处理过程分解为小的可重用的组件,每一个组件都将处理一个明肯定义的任务或者步骤。this
Netty提供了它所谓的Embedded传输,用于测试ChannelHandler。这个传输是一种特殊的Channel实现——EmbeddedChannel——的功能。这是实现提供了经过ChannelPipeline传播事件的简便方法。编码
这个想法是直截了当的:将入站数据或者出站数据写入到EmbeddedChannel中,而后检查是否有任何东西到达了ChannelPipeline的尾端。以这种方式,你即可以肯定消息是否被编码或者被解码过了,以及是否触发了任何的ChannelHandler动做。设计
下图展现了使用EmbeddedChannel的方法,数据是如何流经ChannelPipeline的。你可使用writeOutbound()方法将消息写到Channel中,并经过ChannelPipeline沿着出站的方向传递。随后,你可使用readOutbound()方法来读取已被处理过的消息,已肯定结果是否和预期同样。相似地,对于入站数据,你须要使用writeInbound()和readInbound()方法。netty
在每种状况下,消息都将会传递过ChannelPipeline,而且被相关的ChannelInboundHandler或者ChannelOutboundHandler处理。若是消息没有被消费,那么你可使用readInbound()或者readOutbound()方法来在处理过了这些消息以后,酌情把它们从Channel中读出来。code
二、使用EmbeddedChannel测试ChannelHandlerblog
JUnit断言事件
org.junit.Assert 类提供了不少用于测试的静态方法。失败的断言将致使一个异常被抛出,并将终止当前正在执行中的测试。导入这些断言的最高效的方式是经过一个import static语句来实现:
import static org.junit.Assert.*;
一旦这样作了,就能够直接调用Assert方法了:
assertEquals(buf.readSlice(3),read);
三、测试入站消息
下图展现了一个简单的ByteToMessageDecoder实现。给定足够的数据,这个实现将产生固定大小的帧。若是没有足够的数据可供读取,它将等待下一个数据块的到来,并将再次检查是否产生一个新的帧。
能够从图中右侧的帧看到的那样,这个特定的解码器将产生固定为3字节大小的帧。所以,它可能会须要多个事件来提供足够的字节数以产生一个帧。
最终,每一个帧都会被传递给ChannelPipeline中的下一个ChannelHandler,该解码器的实现以下代码所示。
//扩展ByteToMessageDecoder以处理入站字节,并将它们解码为消息public class FixedLengthFrameDecoder extends ByteToMessageDecoder{ private final int frameLength; //指定要生成的帧的长度 public FixedLengthFrameDecoder(int frameLength) throws IllegalAccessException { if (frameLength <= 0){ throw new IllegalAccessException("frameLength must be a positive integer: " + frameLength); } this.frameLength = frameLength; } @Override protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception { //检查是否有足够的字节能够被读取,以生成下一个帧 while (byteBuf.readableBytes() >= frameLength){ //从ByteBuf中读取一个新帧 ByteBuf buf = byteBuf.readBytes(frameLength); //将该帧添加到已被解码的消息列表中 list.add(buf); } } }
如下代码展现了一个使用EmbeddedChannel的对于前面代码的测试
public class FixedLengthFrameDecoderTest { @Test public void decode() throws Exception { //建立一个ByteBuf,并存储9个字节 ByteBuf buf = Unpooled.buffer(); for (int i = 0; i < 9; i++){ buf.writeByte(i); } ByteBuf input = buf.duplicate(); //建立一个EmbeddedChannel,并添加一个FixedLengthFrameDecoder,其将以3字节的帧长度被测试 EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3)); //write bytes //将数据写入EmbeddedChannel assertTrue(channel.writeInbound(input.retain())); //标记Channel为已完成状态 assertTrue(channel.finish()); //read messages //读取所生成的消息,而且验证是否有3帧,其中每帧都为3字节 ByteBuf read = (ByteBuf)channel.readInbound(); assertEquals(buf.readSlice(3),read); read.release(); assertNull(channel.readInbound()); buf.release(); } }
四、测试出站消息
简单地说起咱们正在测试的处理器——AbsIntegerEncoder,它是netty的MessageToMessageEncoder的一个特殊化的实现,用于将负值整数转换为绝对值。
该示例将会按照下列方式工做:
——持有AbsIntegerEncoder的EmbeddedChannel将会以4字节的负整数的形式写出站数据。
——编码器将从传入的ByteBuf中读取每一个负整数,并将会调用Math.abs()方法来获取其绝对值
——编码器将会把每一个负整数的绝对值写到ChannelPipeline中。
如下代码实现了这个逻辑。
public class AbsIntegerEncoder extends MessageToMessageEncoder<ByteBuf>{ @Override protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception { //检查是否有足够的字节用来编码 while (byteBuf.readableBytes() >= 4){ //从输入的ByteBuf中读取下一个整数,而且计算其绝对值 int value = Math.abs(byteBuf.readInt()); //将该整数写入到编码消息的List中 list.add(value); } } }
如下代码使用了EmbeddedChannel来测试代码
public class AbsIntegerEncoderTest { @Test public void encode() throws Exception { //建立一个ByteBuf,而且写入9个负整数 ByteBuf buf = Unpooled.buffer(); for (int i = 1; i < 10; i++){ buf.writeInt(i * -1); } //建立一个EmbeddedChannel,并安装一个要测试的AbsIntegerEncoder EmbeddedChannel channel = new EmbeddedChannel( new AbsIntegerEncoder()); //写入ByteBuf,并断言调用readOutbound()方法将会产生数据 assertTrue(channel.writeOutbound(buf)); assertTrue(channel.finish()); //读取所产生的消息,并断言它们包含了对应的绝对值 //read bytes for (int i=1; i < 10; i++){ assertEquals(i , channel.readOutbound()); } assertNull(channel.readOutbound()); } }
五、测试异常处理
应用程序一般须要执行比转换数据更加复杂的任务。例如,你可能须要处理格式不正确的输入或者过量的数据。下一个示例中,若是所读取的字节数超出了特定的限制,咱们将会抛出一个TooLongFrameException。这是一种常常用来防范资源被耗尽的方法。
以下图,最大的帧大小已经被设置为3字节,若是一个帧的大小超过了该限制,那么程序将会丢弃它的字节,并抛出一个TooLongFrameException。位于ChannelPipeline中的其它ChannelHandler能够选择在exceptionCaught()方法中处理该异常或者忽略它。
其实现以下代码所示。
public class FrameChunkDecoder extends ByteToMessageDecoder{ private final int maxFrameSize; public FrameChunkDecoder(int maxFrameSize) { this.maxFrameSize = maxFrameSize; } @Override protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> out) throws Exception { int readableBytes = in.readableBytes(); //若是该帧太大,则丢弃它并抛出异常 if (readableBytes > maxFrameSize){ //discard the bytes in.clear(); throw new TooLongFrameException(); } //从ByteBuf中读取一个新的帧 ByteBuf buf = in.readBytes(readableBytes); //将该帧添加到解码消息的List中 out.add(buf); } }
使用的Try/Catch块是EmbeddedChannel的一个特殊功能。若是其中一个write*方法产生了一个受检查的Exception,那么它将会被包装在一个RuntimeException中并抛出,这使得能够容易地测试出一个Exception是否在处理数据的过程当中已经被处理了。