1. 简介

本文将介绍如何使用 EmbeddedChannel 测试入站和出站通道处理器的功能。

Netty 是一个用于构建高性能异步应用的强大框架。但如果没有合适的工具,单元测试这类应用可能会很棘手。

幸运的是,框架提供了 EmbeddedChannel 类——专门用于简化 ChannelHandlers 的测试

2. 环境准备

EmbeddedChannel 是 Netty 框架的一部分,所以只需要添加 Netty 的依赖即可。

该依赖可在 Maven Central 获取:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.24.Final</version>
</dependency>

3. EmbeddedChannel 核心机制

EmbeddedChannel 本质上是 AbstractChannel 的另一种实现——它无需真实网络连接即可传输数据。

这很有用,因为我们可以通过向入站通道写入数据来模拟入站消息,同时检查出站通道生成的响应。这样既能单独测试每个 ChannelHandler,也能测试整个通道管道。

要测试一个或多个 ChannelHandler,首先需要通过构造函数创建 EmbeddedChannel 实例。

最常用的初始化方式是直接传入 ChannelHandlers 列表:

EmbeddedChannel channel = new EmbeddedChannel(
  new HttpMessageHandler(), new CalculatorOperationHandler());

如果想更灵活地控制处理器添加顺序,可以使用默认构造函数创建后手动添加:

channel.pipeline()
  .addFirst(new HttpMessageHandler())
  .addLast(new CalculatorOperationHandler());

另外,创建 EmbeddedChannel 时会使用 DefaultChannelConfig 提供的默认配置

当需要自定义配置(比如降低连接超时时间)时,可通过 config() 方法获取配置对象:

DefaultChannelConfig channelConfig = (DefaultChannelConfig) channel
  .config();
channelConfig.setConnectTimeoutMillis(500);

EmbeddedChannel 提供了读写 ChannelPipeline 数据的核心方法:

  • readInbound()
  • readOutbound()
  • writeInbound(Object… msgs)
  • writeOutbound(Object… msgs)

read 方法会移除并返回入站/出站队列的首个元素。当需要访问完整消息队列而不移除元素时,可使用 outboundMessages() 方法:

Object lastOutboundMessage = channel.readOutbound();
Queue<Object> allOutboundMessages = channel.outboundMessages();

*write 方法在消息成功添加到通道的入站/出站管道时返回 true*

channel.writeInbound(httpRequest)

核心思路是:向入站管道写入消息,由 ChannelHandlers 处理后,从出站管道读取预期结果。

4. 测试 ChannelHandlers

来看一个简单示例:测试由两个处理器组成的管道,它们接收 HTTP 请求并返回包含计算结果的 HTTP 响应:

EmbeddedChannel channel = new EmbeddedChannel(
  new HttpMessageHandler(), new CalculatorOperationHandler());

第一个处理器 HttpMessageHandler 从 HTTP 请求中提取数据,传递给管道中的第二个处理器 CalculatorOperationHandler 进行处理。

现在写入 HTTP 请求,验证入站管道的处理逻辑:

FullHttpRequest httpRequest = new DefaultFullHttpRequest(
  HttpVersion.HTTP_1_1, HttpMethod.GET, "/calculate?a=10&b=5");
httpRequest.headers().add("Operator", "Add");

assertThat(channel.writeInbound(httpRequest)).isTrue();
long inboundChannelResponse = channel.readInbound();
assertThat(inboundChannelResponse).isEqualTo(15);

我们通过 writeInbound() 发送请求,用 readInbound() 读取结果。inboundChannelResponse 是数据经过所有入站处理器处理后的消息。

接下来验证服务器是否返回了正确的 HTTP 响应。检查出站管道是否存在消息:

assertThat(channel.outboundMessages().size()).isEqualTo(1);

出站消息是 HTTP 响应,验证其内容是否正确。读取出站管道的最后一条消息:

FullHttpResponse httpResponse = channel.readOutbound();
String httpResponseContent = httpResponse.content()
  .toString(Charset.defaultCharset());
assertThat(httpResponseContent).isEqualTo("15");

5. 测试异常处理

另一个常见测试场景是异常处理。

ChannelInboundHandlers 中可通过实现 exceptionCaught() 方法处理异常,但有时我们希望将异常传递给管道中的下一个处理器。

使用 EmbeddedChannelcheckException() 方法可以检查管道中是否存在 Throwable 对象并重新抛出。

这样就能捕获异常并验证处理器是否按预期抛出:

assertThatThrownBy(() -> {
    channel.pipeline().fireChannelRead(wrongHttpRequest);
    channel.checkException();
}).isInstanceOf(UnsupportedOperationException.class)
  .hasMessage("HTTP method not supported");

示例中我们发送了一个预期会触发异常的 HTTP 请求。通过 checkException() 重新抛出管道中的最后一个异常,即可进行断言验证。

6. 总结

EmbeddedChannel 是 Netty 框架提供的强大工具,用于验证 ChannelHandler 管道的正确性。它既能单独测试每个处理器,更重要的是能测试整个管道。

本文源码可在 GitHub 获取。


原始标题:Testing Netty with EmbeddedChannel