本章将介绍:
- 单元测试
- EmbeddedChannel 浏览
- 测试 ChannelHandlers 和 EmbeddedChannel
ChannelHandlers 是 Netty 应用程序中的关键元素,所以彻底测试它们应该是开发过程的标准部分。最佳实践要求您测试的不仅是为了证明您的实现是正确的,而且还可以轻松地隔离在修改代码时出现的问题。 这种类型的测试称为单元测试。
虽然没有关于单元测试的通用定义,但大多数从业者都同意基础知识。 基本思想是以尽可能小的块来测试代码,尽可能地与其他代码模块以及运行时依赖(如数据库和网络)隔离。 如果您可以通过测试验证每个单元本身是否正常工作,那么当出现问题时,更容易找到罪魁祸首。
在本章中,我们将研究一个特殊的Channel实现EmbeddedChannel,Netty专门为ChannelHandlers提供单元测试。
由于要测试的代码模块或单元将在其正常运行时环境之外执行,因此您需要一个框架或工具来运行它。 在我们的示例中,我们将使用JUnit 4作为我们的测试框架,因此您需要对其使用有基本的了解。 如果它对你来说是新的,不要害怕; 虽然功能强大但很简单,您可以在JUnit网站(www.junit.org)上找到所需的所有信息。
9.1 Overview of EmbeddedChannel
您已经知道ChannelHandler实现可以在ChannelPipeline中链接在一起,以构建应用程序的业务逻辑。 我们之前解释过,这种设计支持将可能复杂的处理分解为小型和可重用的组件,每个组件都处理明确定义的任务或步骤。 在本章中,我们将向您展示它如何简化测试。
Netty提供了所谓的嵌入式传输来测试ChannelHandlers。 此传输是特殊Channel实现EmbeddedChannel的一个功能,它提供了一种通过管道传递事件的简单方法。这个想法非常简单:您将入站或出站数据写入EmbeddedChannel,然后检查是否有任何内容到达ChannelPipeline的末尾。
通过这种方式,您可以确定是否对消息进行了编码或解码,以及是否触发了任何ChannelHandler操作。
名称 | 职责 |
---|---|
writeInbound(Object … msgs) | 将入站消息(inbound message) 写入到 EmbeddedChannel. 如果能通过 EmbeddedChannel 的 readInbound() 方法读取这个数据,返回 true |
readInbound() | 从EmbeddedChannel 读取入站消息(inbound message). 返回的任何内容都遍历整个ChannelPipeline。如果没有准备好读取,则返回null。 |
writeOutbound(Object … msgs) | 将出站消息写入 EmbeddedChannel 。 如果现在可以通过readOutbound() 从 EmbeddedChannel 读取某些内容,则返回true。 |
readOutbound() | 从EmbeddedChannel读取出站消息。 返回的任何内容都遍历整个ChannelPipeline。 如果没有准备好读取,则返回null。 |
finish() | 将EmbeddedChannel标记为完成,如果可以读取入站或出站数据,则返回true。 这也将调用EmbeddedChannel上的close()。 |
入站数据由ChannelInboundHandlers处理,表示从远程对等方读取的数据。 出站数据由ChannelOutboundHandlers处理,表示要写入远程对等方的数据。 取决于您ChannelHandler 测试时,您将使用 *Inbound() 或 *Outbound() 方法对,或者两者兼而有之。
图9.1显示了使用EmbeddedChannel方法的数据如何流经 ChannelPipeline。 您可以使用writeOutbound() 向 Channel 写入消息,并在出站方向上通过 ChannelPipeline 传递消息。 随后你可以使用 readOutbound() 读取已处理的消息,以确定结果是否符合预期。 同样,对于入站数据,您使用 writeInbound() 和 readInbound()。
在每种情况下,消息都通过ChannelPipeline传递,并由相关的 ChannelInboundHandlers 或 ChannelOutboundHandlers 处理。 如果未使用该消息,则可以根据需要使用 readInbound() 或 readOutbound() 在处理完消息后从消息中读取消息。
让我们仔细研究两种场景,看看它们如何应用于测试应用程序逻辑。
9.2 Testing ChannelHandlers with EmbeddedChannel
在本节中,我们将解释如何使用EmbeddedChannel测试ChannelHandler。
JUnit assertions
类 org.junit.Assert 提供了许多静态方法(stati methods)用于测试. 一个失败的断言将导致一个异常抛出,并中断当前运行的测试程序。导入这些断言的最有效方法是通过import static语句:
import static org.junit.Assert.*;
一旦你这样做了,你就可以直接使用 Assert 方法了:
assertEquals(buf.readSlice(3), read);
9.2.1 Testing inbound messages
图9.2表示一个简单的ByteToMessageDecoder实现。 给定足够的数据,这将产生固定大小的帧。 如果没有足够的数据准备好读取,它将等待下一个数据块并再次检查是否可以生成帧。
从图右侧的帧中可以看出,这个特殊的解码器产生的帧大小固定为3个字节。 因此,可能需要多个事件来提供足够的字节来产生帧。
最后,每个帧将传递给ChannelPipeline中的下一个ChannelHandler。
解码器的实现如下表所示。
// Extends ByteToMessageDecoder to handle inbound bytes and decode them to messages
public class FixedLengthFrameDecoder extends ByteToMessageDecoder {
private final int frameLegth;
// Specifies the length of the frames to be produced
public FixedLengthFrameDecoder(int frameLenth) {
if(frameLength <= 0) {
throw new IllegalArgumentException("frameLength must be a positive integer: " + frameLength);
}
this.frameLenth = frameLength;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// Checks if enough bytes can be read to produce the next frame
while (in.readableBytes() >= frameLength) {
// Reads a new frame out of the ByteBuf
ByteBuf buf = in.readBytes(frameLength);
// Adds the frame to the List of decoded messages
out.add(buf);
}
}
现在让我们创建一个单元测试,以确保此代码按预期工作。 正如我们前面指出的那样,即使在简单的代码中,单元测试也有助于防止将来重构代码时可能出现的问题,并在有问题时对其进行诊断。
此列表显示使用EmbeddedChannel测试上述代码。
public class FixedLengthFrameDecoderTest {
// Annotated with @Test so JUnit will execute the method
@Test
// The first test method: testFramesDecoded()
public void testFramesDecoded() {
//Creates a ByteBuf and stores 9 bytes
ByteBuf buf = Unpooled.buffer();
for (int i = 0; i < 9; i++) {
buf.writeByte(i);
}
ByteBuf input = buf.duplicate();
// Creates an EmbeddedChannel and adds a FixedLengthFrameDecoder to be tested with a frame length of 3 bytes
EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3));
// writes data to the EmbeddedChannel
assertTrue(channel.writeInbound(input.retain()));
// Marks the Channel finished
assertTrue(channel.finish());
// reads the produced messages and verifies that there are 3 frames(slices) with 3 bytes each
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();
}
@Test
public void testFramesDecoded2() {
ByteBuf buf = Unplooed.buffer();
for(int i = 0; i < 9; i++) {
buf.writeByte(i);
}
ByteBuf input = buf.duplicate();
EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3));
// Returns false because a complete frame is not ready to be read.
assertFalse(channel.writeInbound(input.readBytes(2)));
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() 验证包含9个可读字节的 ByteBuf 被解码为3个 ByteBuf,每个包含3个字节。 请注意在一次 writeInbound() 调用中如何使用9个可读字节填充 ByteBuf。 在此之后,执行 finish() 以标记 EmbeddedChannel 完成。 最后,调用 readInbound()
从 EmbeddedChannel 中准确读取三个帧和一个空值。
方法 testFramesDecoded2() 类似,但有一点不同:入站 ByteBuf 分两步编写。 当调用writeInbound(input.readBytes(2)) 时,返回false。 为什么? 如表9.1所述,如果对 readInbound() 的后续调用将返回数据,则writeInbound() 将返回true。 但只有当三个或更多字节可读时,FixedLengthFrameDecoder 才会产生输出。 测试的其余部分与 testFramesDecoded() 相同。
9.2.2 Testing outbound messages
测试出站消息的处理与您刚看到的类似。 在下一个示例中,我们将展示如何使用EmbeddedChannel 以编码器的形式测试 ChannelOutboundHandler,编码器是将一种消息格式转换为另一种消息格式的组件。 您将在下一篇中详细研究编码器和解码器
章节,所以现在我们只提一下我们正在测试的处理程序 AbsIntegerEncoder 是Netty的 MessageToMessageEncoder 的一个特化,它将负值整数转换为绝对值。
该示例将如下工作:
- 包含 AbsIntegerEncoder 的 EmbeddedChannel 将以4字节负整数的形式写入出站数据。
- 解码器将从传入的 ByteBuf 中读取每个负整数,并调用 Math.abs() 来获取绝对值。
- 解码器将每个整数的绝对值写入ChannelHandlerPipeline。
图 9.3 显示了这个逻辑
下面的列表实现了这个逻辑,如图 9.3 阐述的那样, encode() 方法将生成的值写入List。
// AbsIntegerEncoder
// Extends MessageToMessageEncoder to encode a message to another format
public class AbsIntegerEncoder extends
MessageToMessageEncoder<ByteBuf> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext,
ByteBuf in, List<Object> out) throws Exception {
// Checks if there are enougth bytes to encode
while(in.readableBytes() >= 4) {
// Reads the next int out of the input ByteBuf and calculates the absolute value
int value = Math.abs(in.readInt());
// Writes the int to the List of encoded messages
out.add(value);
}
}
}
下一个清单使用EmbeddedChannel测试代码。
public class AbsIntegerEncoderTest {
@Test
public void testEncoded() {
// Creates a ByteBuf and writes 9 nagative ints
ByteBuf buf = Unpooled.buffer();
for(int i = 1; i < 10; i++) {
buf.writeInt(i * -1);
}
// Creates an EmbeddedChannel and installs an AbsIntegerEncoder to be tested.
EmbeddedChannel channel = new EmbeddedChannel(new AbsIntegerEncoder());
// Writes the ByteBuf and asserts that readOutbound() will produce data
assertTrue(channel.writeOutbound(buf));
// Marks the channel finished
assertTrue(channel.finish());
// Reads the produced messages and asserts that they contain absolute values
for (int i = 1; i < 10; i++) {
assertEquals(i, channel.readOutbound());
}
assertNull(channel.readOutbound());
}
}
这里代码执行了以下步骤:
- 写了 4哥字节的负整数到一个新的 ByteBuf 中。
- 创建了一个新的 EmbeddedChannel 并且将一个 AbsIntegerEncoder 指定给了它。
- 调用了 EmbeddedChannel 的 writeOutbound() 方法,将 ByteBuf 写入其中。
- 标记 channel 为已完成。
- 从 EmbeddedChannel 的出站侧读取所有的整数,并验证产生的值都是绝对值。
9.3 Testing exception handling
除了传输数据外,应用程序通常还有额外的任务需要执行。例如,您可能需要处理格式错误的输入或过多的数据。 在下一个示例中,如果读取的字节数超过指定的限制,我们将抛出TooLongFrameException。 这是一种常用于防止资源耗尽的方法。
在图9.4中,最大帧大小已设置为3个字节。 如果帧的大小超过该限制,则丢弃其字节并抛出TooLongFrameException。 管道中的其他ChannelHandler可以处理**exceptionCaught()**中的异常或忽略它。
实现如下所示:
// Extends ByteToMessageDecoder to decode inbound bytes to messages
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 {
// Specifies the maximum allowable size of the frames to be produced
int readableBytes = in.readableBytes();
if (readableBytes > maxFrameSize) {
// discard the bytes
in.clear();
throw new TooLongFrameException();
}
// ... otherwise, reads the new frame from the ByteBuf
ByteBuf buf = in.readBytes(readableBytes);
// Adds the frame to the List of decoded messages
out.add(buf);
}
}
同样,我们将使用EmbeddedChannel测试代码。
public class FrameChunkDecoderTest {
@Test
public void testFrameDecoded() {
// Creates a ByteBuf and writes 9 bytes to it.
ByteBuf buf = Unpooled.buffer();
for (int i = 0; i < 9; i++) {
buf.writeByte(i);
}
ByteBuf input = buf.duplicate();
// Creates an EmbeddedChannel and installs a FixedLengthFrameDecoder with a frame size of 3
EmbeddedChannel channel = new EmbeddedChannel(new FrameChunkDecoder(3));
// Writes 2 bytes to it and asserts that they produced a new frame
try {
//Writes a 4-byte frame and catches the expected TooLongFrameException
channel.writeInbound(input.readBytes(4));
// If the exception isn't thrown this assertion is reached and the test fails.
Assert.fail();
} catch(TooLongFrameException e) {
// expected exception
}
// Writes the remaining 2 bytes and asserts a valid frame
assertTrue(channel.writeInbound(input.readBytes(3)));
// Makes the channel finished
assertTrue(channel.finish());
// Read frames
// Reads the produced messages and verifies the values
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();
}
}
乍一看,这看起来非常类似于9.2的测试,但它有一个有趣的转折; 即TooLongFrameException的处理。 这里使用的 try / catch 块 EmbeddedChannel的一个特殊功能。 如果是其中一个 write* 方法生成一个已检查的Exception,它将被抛出包装在RuntimeException中。 这样可以轻松测试在处理数据期间是否处理了异常。
此处说明的测试方法可以与抛出异常的任何ChannelHandler实现一起使用。
9.4 Summary
使用JUnit等测试工具进行单元测试是保证代码正确性和增强其可维护性的极为有效的方法。 在本章中,您学习了如何使用Netty提供的测试工具来测试自定义ChannelHandler。
在接下来的章节中,我们将专注于使用Netty编写实际应用程序。 我们不会再提供任何测试代码示例,因此我们希望您能牢记我们在此演示的测试方法的重要性。