Netty in action—单元测试
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实现一起使用。
更多推荐
所有评论(0)