本章主要内容:
- 单元测试
- EmbeddedChannel
一、前言
我们知道Netty的ChannelHandler总共有两大类,一类处理收到的数据,一类处理发送的数据,通过ChannelPipeline可以很容易的将这两大类ChannelHandler结合在一起。ChannelHandler算是使用了常用设计模式中的责任链模式,可以很方便的复用里面的实现。ChannelHandler只处理事件,逻辑很简单,也很清晰,并且方便进行测试。
测试ChannelHandler一般建议使用嵌入式传输方式,前面说过,这种传输方式不会真正走网络,但是很容易传输事件,从而测试你的ChannelHandler实现。这种传输方式提供了一个特殊的Channel实现,名字叫EmbeddedChannel。
但是它是如何工作的呢?很简单,可以向EmbeddedChannel写数据模拟收到数据或发送数据,然后检查是否经过ChannelPipeline。这样就可以检查数据是否被编码解码或者触发了什么事件。
下表列出了AbstractEmbeddedChannel提供的常用方法。
名称 | 描述 |
writeInbound(…) | 向Channel写入数据,模拟Channel收到数据,也就 |
readInbound(…) | 从EmbeddedChannel中读数据,返回经过ChannelPipeline中的 |
writeOutbound(…) | 向Channel写入数据,模拟Channel发送数据,也就 |
readOutbound(…) | 从EmbeddedChannel中读数据,返回经过ChannelPipeline中的 |
finish() | 结束EmbeddedChannel,如果里面有任何类型的可读数据都会返回true,它也会调用Channel的close方法 |
为了更加清楚了解这些方法读写的数据在EmbeddedChannel中的流程,请看下面的结构图。
从上图中可以很明显看出来,writeOutbound(…)会将数据写到Channel并经过OutboundHandler,然后通过readOutbound(…)方法就能读取到处理后的数据。模拟收到数据也是类似的,通过writeInbound(…)和readInbound(…)方法。收到的数据和发送的数据的逻辑基本是一样的,经过ChannelPipeline后到达终点后会存储在EmbeddedChannel中。
了解了EmbeddedChannel大致结构,下面我们来学习下如何使用它来测试你的ChannelHandler。
二、测试ChannelHandler
为了测试ChannelHandler,最好还是使用EmbeddedChannel。下面会简单介绍几个例子,讲述如何使用EmbeddedChannel测试我们编写的ChannelHandler。
2.1、测试InboundHandler
我们先实现一个简单的ByteToMessageDecoder,主要逻辑就是每次收到数据后将数据分成数量固定的组,如果数据不够就不分然后等到下次收到数据的时候再检查是否数据足够,如下图所示。
从上图可以看出来,主要逻辑很简单,就是读取指定数量数据然后分组,完整代码如下。
import java.util.List;
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);
}
}
}
一般来说,我们实现了一段主要的逻辑,最好就使用单元测试验证一下。即使你能保证你现在写的代码没什么问题,但你后面可能会重构你的代码,所以写一段单元测试,重构之后只要再跑一下单元测试,就能检查重构是否出现问题。单元测试可以帮助开发者发现很多问题,预防在生产环境出现重大问题。
下面我们来测试一下刚才我们实现的解码器。
package com.nan.netty.test;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel;
import org.junit.Assert;
import org.junit.Test;
public class FixedLengthFrameDecoderTest {
@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 FixedLengthFrameDecoder(3));
//验证写数据返回True
Assert.assertTrue(channel.writeInbound(input.readBytes(9)));
Assert.assertTrue(channel.finish());
//每次读3个数据
Assert.assertEquals(buf.readBytes(3), channel.readInbound());
Assert.assertEquals(buf.readBytes(3), channel.readInbound());
Assert.assertEquals(buf.readBytes(3), channel.readInbound());
Assert.assertNull(channel.readInbound());
}
@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());
Assert.assertEquals(buf.readBytes(3), channel.readInbound());
Assert.assertEquals(buf.readBytes(3), channel.readInbound());
Assert.assertEquals(buf.readBytes(3), channel.readInbound());
Assert.assertNull(channel.readInbound());
}
}
我们看看上边的单元测试的主要逻辑。
testFramesDecoded()方法主要逻辑是一个包含9个字节的ByteBuf,警告我们实现的解码器,变成了3个ByteBuf,每个ByteBuf包含3个字节。通过writeInbound(…)方法将9个字节写到EmbeddedByteChannel中,调用finish()方法标记EmbeddedByteChannel已经结束,然后使用readInbound()方法读出EmbeddedByteChannel中已经解码完成的数据。
testFramesDecoded2()方法的大致逻辑和前一个方法一样,唯一的区别就是写入数据的时候先写2个字节,因此导致我们实现的FixedLengthFrameDecoder没有解码输出,所以返回结果为false。
2.2、测试OutboundHandler
上面我们学习了如何测试InboundHandler,接下来我们学习如何单元测试OutboundHandler。测试OutboundHandler和测试InboundHandler的结构大致是一样的,我们还是通过实际例子学习。
我们实现一个AbsIntegerEncoder,将负数变成正数,然后传递给下一个ChannelHandler,主要步骤如下:
- 收到ByteBuf后,调用Math.abs(..)将它们都转成正数
- 转换完成之后将数据传递给下一个ChannelHandler
package com.nan.netty.test;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;
import java.util.List;
public class AbsIntegerEncoder extends MessageToMessageEncoder<ByteBuf> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext,
ByteBuf in, List<Object> out) throws Exception {
while (in.readableBytes() >= 4) {
int value = Math.abs(in.readInt());
out.add(value);
}
}
}
可以看出,实现很简单,有整数就读出来,取得绝对值,放入到结果列表中。这么简单的逻辑大家可能会觉得肯定不会出错,不过有没有错误最好还是通过单元测试来证明。
package com.nan.netty.test;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel;
import org.junit.Assert;
import org.junit.Test;
public class AbsIntegerEncoderTest {
@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());
//模拟发送数据
Assert.assertTrue(channel.writeOutbound(buf));
Assert.assertTrue(channel.finish());
//检查是否是正数
for (int i = 1; i < 10; i++) {
Assert.assertEquals(i, (int) channel.readOutbound());
}
//没有数据返回null
Assert.assertNull(channel.readOutbound());
}
}
三、测试捕获异常
package com.nan.netty.test;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.TooLongFrameException;
import java.util.List;
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) {
in.clear();
throw new TooLongFrameException();
}
ByteBuf buf = in.readBytes(readableBytes);
out.add(buf);
}
}
单元测试还是选择
EmbeddedChannel最好,代码如下。
package com.nan.netty.test;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.TooLongFrameException;
import org.junit.Assert;
import org.junit.Test;
public class FrameChunkDecoderTest {
@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));
Assert.assertTrue(channel.writeInbound(input.readBytes(2)));
try {
channel.writeInbound(input.readBytes(4));
Assert.fail();
} catch (TooLongFrameException e) {
System.out.println("Catch TooLongFrameException");
}
Assert.assertTrue(channel.writeInbound(input.readBytes(3)));
Assert.assertTrue(channel.finish());
Assert.assertEquals(buf.readBytes(2), channel.readInbound());
Assert.assertEquals(buf.skipBytes(4).readBytes(3), channel.readInbound());
}
}
这个单元测试可能和前面的看着很像,但是有很明显的区别,就是这里使用了
try / catch捕获了异常,可以看到,使用
EmbeddedChannel也可以测试这种特殊的需求。例子中虽然测试的是ByteToMessageDecoder,但是很明显,任何ChannelHandler抛出异常的情况都可以使用EmbeddedChannel进行测试。
四、总结
这一章我们主要学习了单元测试我们实现的ChannelHandler,单元测试框架使用的还是经典的JUnit。测试中使用的Channel类型是EmbeddedChannel,虽然它的实现很简单,但功能是很完善的,可以帮助我们测试自己的ChannelHandler。