Netty 是一个高性能、异步事件驱动的 NIO 框架,它提供了对 TCP 、 UDP 和文件传输的支持,作为一个异步 NIO 框架,Netty 的所有 IO 操作都是异步非阻塞的,通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果。
Netty 在保持高性能的同时又提供了非常易用的 API,实际使用中只需要实现自定义的 Handler,那么快速完成开发后保证 Handler 中数据流的正确性就成为了重中之重。

好在 Netty 提供了专门用来测试 Handler 的类: EmbeddedChannel。通过 EmbeddedChannel 可以测试 inbound&outbound 两个方向的数据流来达到测试 Handler 的目的。
EmbeddedChannel 提供如下方法:

1
2
3
4
5
boolean writeInbound(Object... msgs) // 向 Inbound channel 写消息
<T> T readInbound() // 从 Inbound channel读消息
boolean writeOutbound(Object... msgs) // 向 Outbound channel 写消息
<T> T readOutbound() // 从 Outbound channel 读消息
boolean finish() // 将 channel 标记为 finished

下面就使用以上几个方法来完成 Handler 的测试。
首先将创建以下几个简单的 Handler:
NettyHttpRequestHandler(Inbound): 接收 Client 的 Request 并作简单处理
NettyHttpRelayHandler(Inbound): 将处理过的 Request 包装成 Response 并转交给下面的 Outbound
NettyHttpResponseHandler(Outbound): 接收 Response 并作简单处理
NettyHttpSenderHandler(Outbound): 将 Response 返回给 Client
其中 NettyHttpRelayHandler 和 NettyHttpSenderHandler 无需做单元测试,一个仅是将 request 包装成 response,另一个仅是将 response 返回给 cilent。

NettyHttpRequestHandler(Inbound)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class NettyHttpRequestHandler extends ChannelInboundHandlerAdapter {

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

// update http content to Base64 encode value
FullHttpRequest httpRequest = (FullHttpRequest) msg;
ByteBuf contentBuf = httpRequest.content();
String content = contentBuf.toString(CharsetUtil.UTF_8);
contentBuf.clear();
contentBuf.writeBytes(Base64.getEncoder().encode(content.getBytes(CharsetUtil.UTF_8)));
// update content length
HttpHeaders.setHeader(httpRequest, HttpHeaders.Names.CONTENT_LENGTH, contentBuf.readableBytes());

super.channelRead(ctx, msg);
}
}

NettyHttpRequestHandler 将接收到的 body 内容 base64 编码。

NettyHttpResponseHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class NettyHttpResponseHandler extends ChannelOutboundHandlerAdapter {

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {

// update http content to Base64 decode value
FullHttpResponse response = (FullHttpResponse) msg;
ByteBuf contentBuf = response.content();
String content = contentBuf.toString(CharsetUtil.UTF_8);
contentBuf.clear();
contentBuf.writeBytes(Base64.getDecoder().decode(content.getBytes(CharsetUtil.UTF_8)));
// update content length
HttpHeaders.setHeader(response, HttpHeaders.Names.CONTENT_LENGTH, contentBuf.readableBytes());

super.write(ctx, msg, promise);
}
}

NettyHttpResponseHandler 将接收到的 body 内容 base64 解码。

开始进入到如何对 NettyHttpRequestHandler 和 NettyHttpResponseHandler 进行单元测试的重头戏。
创建一个 EmbeddedChannel 对象:

1
2
3
4
5
6
EmbeddedChannel channel = new EmbeddedChannel(
new HttpRequestDecoder(),
new HttpObjectAggregator(10485760),
new NettyHttpResponseHandler(),
new NettyHttpRequestHandler()
);

构造 HttpRequest:

1
2
FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, POST, "/test");
setRequestHeaders(request);

向 channel 中发送 request:

1
2
3
4
5
6
7
8
HttpContent chunk = new DefaultHttpContent(Unpooled.wrappedBuffer(JSON_DATA.getBytes(UTF_8)));
HttpHeaders.setHeader(request, CONTENT_LENGTH, chunk.content().readableBytes());
// write request to Inbound
channel.writeInbound(request);
// write content to Inbound
channel.writeInbound(chunk);
// write last conent to Inbound
channel.writeInbound(LastHttpContent.EMPTY_LAST_CONTENT);

检查经过 NettyHttpRequestHandler 处理后的数据:

1
2
3
4
5
6
FullHttpRequest reqMsg = (FullHttpRequest) channel.readInbound();
assertNotNull(reqMsg);

String body = reqMsg.content().toString(UTF_8);
assertNotNull(body);
assertEquals(body, "eyJkYXRhIjogInRoaXMgaXMganNvbiBib2R5In0=");

构造 HttpResponse 并发送到 channel :

1
2
3
4
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1,
HttpResponseStatus.OK, Unpooled.copiedBuffer(body, UTF_8));
setResponseHeaders(response);
channel.writeOutbound(response);

检查经过 NettyHttpResponseHandler 处理后的数据:

1
2
3
4
5
6
7
8
FullHttpResponse resMsg = (FullHttpResponse) channel.readOutbound();
assertNotNull(resMsg);

body = resMsg.content().toString(UTF_8);
assertNotNull(body);
assertEquals(body, JSON_DATA);
// mark channel as finished
channel.finish();

以上内容展示了 Netty Handler 完整的单元测试流程,开发过程中可以编写充分的单元测试,尽量保证程序的正确性。
完整版源码下载地址:netty-handler-tester