@Test void testClientRun() throws Exception { try (MockedConstruction<Bootstrap> ignored = mockConstruction(Bootstrap.class, (mock, context) -> { when(mock.group(any())).thenReturn(mock); when(mock.channel(any())).thenReturn(mock); when(mock.option(any(), any())).thenReturn(mock); when(mock.handler(any())).thenReturn(mock); ChannelFuture mockBindFuture = mock(ChannelFuture.class); when(mock.bind(anyInt())).thenReturn(mockBindFuture); when(mockBindFuture.sync()).thenReturn(mockBindFuture); Channel mockChannel = mock(Channel.class); when(mockBindFuture.channel()).thenReturn(mockChannel); ChannelFuture mockCloseFuture = mock(ChannelFuture.class); when(mockChannel.closeFuture()).thenReturn(mockCloseFuture); when(mockCloseFuture.sync()).thenReturn(mockCloseFuture); })) { new NettyClient("127.0.0.1",9000).connect("greet","da"); Bootstrap bootstrap = ignored.constructed().get(0); verify(bootstrap).connect(anyString(), anyInt()); } } 运行上面的单元测试,报错如下: java.lang.NullPointerException at org.example.client.NettyClient.connect(NettyClient.java:65) at org.example.client.NettyClientTest.testClientRun(NettyClientTest.java:56) 原代码如下: @Slf4j @AllArgsConstructor public class NettyClient { private String host; private int port; public void connect(String api, String msg) throws InterruptedException, JsonProcessingException { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<Channel>() { @Override protected void initChannel(Channel channel) throws Exception { ChannelPipeline p = channel.pipeline(); p.addLast(new LengthFieldBasedFrameDecoder(1024 * 1024, 0, 4, 0, 4)); p.addLast(new StringDecoder(CharsetUtil.UTF_8)); p.addLast(new LengthFieldPrepender(4)); p.addLast(new StringEncoder(CharsetUtil.UTF_8)); p.addLast(new NettyClientHandler()); } }); ObjectMapper mapper = new ObjectMapper(); Channel channel = b.connect(host, port).sync().channel(); String json = JsonUtil.toJson(new GreetRequest(api, msg)); //System.out.println("request: " + json); log.info("request: " + json); channel.writeAndFlush(json).sync(); channel.closeFuture().sync(); } finally { group.shutdownGracefully(); } } public static void main(String[] args) throws InterruptedException, JsonProcessingException { new NettyClient("127.0.0.1", 9000).connect("greet", "alice"); } } 如何修改
时间: 2025-08-25 10:59:40 浏览: 5
<think>我们分析一下问题:在单元测试中模拟Bootstrap时,在NettyClient的connect方法中执行到`b.connect(host, port).sync().channel()`时出现了NullPointerException。
原因:在测试中,我们模拟了Bootstrap,但是当调用`b.connect(host, port)`时,我们返回了一个模拟的ChannelFuture(mockBindFuture),但是注意,在NettyClient中,我们调用了`b.connect(host, port).sync().channel()`,这意味着我们需要确保:
1. `b.connect(host, port)` 返回一个ChannelFuture(我们模拟为mockBindFuture)
2. 在这个ChannelFuture上调用`sync()`返回自身(我们做了:`when(mockBindFuture.sync()).thenReturn(mockBindFuture);`)
3. 然后调用`channel()`返回一个Channel(我们做了:`when(mockBindFuture.channel()).thenReturn(mockChannel);`)
但是,我们注意到在测试代码中,我们模拟的是`bind`方法,而不是`connect`方法!这是一个关键错误。
在NettyClient中,我们使用的是Bootstrap的`connect`方法,但在测试中我们模拟了`bind`方法:
`when(mock.bind(anyInt())).thenReturn(mockBindFuture);`
因此,我们需要将`bind`改为`connect`。
另外,在测试中,我们模拟了`b.connect(host, port)`返回mockBindFuture,但是我们在测试代码中并没有为`connect(String, int)`方法设置模拟行为,而是模拟了`bind(anyInt())`。所以,我们需要将`bind`替换为`connect`。
修改后的测试代码应该为:
`when(mock.connect(anyString(), anyInt())).thenReturn(mockBindFuture);`
同时,我们注意到在NettyClient中,`b.connect(host, port)`后面还调用了`sync()`和`channel()`,我们已经模拟了这些调用。
此外,我们还需要注意,在测试中,我们模拟了`b.group(any())`等链式调用,返回mock自身,这是正确的。
因此,修改测试代码中的错误:将`bind`改为`connect`。
另外,我们还需要注意,在测试中我们调用了`new NettyClient("127.0.0.1",9000).connect("greet","da")`,这行代码会执行实际的方法,而我们的模拟应该覆盖了Bootstrap的创建。但是,在模拟中,我们还需要确保`b.connect`返回的ChannelFuture的`sync`方法不会返回null,并且`channel`方法返回的Channel不为null,同时我们还需要模拟`channel.writeAndFlush`和`channel.closeFuture`,否则在后续执行中可能还会出现NPE。
因为NettyClient的connect方法中还有以下操作:
- 创建Channel后,发送消息:`channel.writeAndFlush(json).sync();`
- 等待关闭:`channel.closeFuture().sync();`
所以,我们需要在模拟的Channel上模拟这些方法。
因此,我们需要:
1. 模拟一个Channel,并设置它的行为,特别是`writeAndFlush`方法和`closeFuture`方法。
2. 模拟writeAndFlush返回的ChannelFuture,因为后面调用了sync()。
具体修改如下:
在测试代码中,我们模拟Bootstrap时,需要:
- 模拟`connect`方法返回一个ChannelFuture(我们命名为mockConnectFuture)
- 在mockConnectFuture上模拟`sync()`返回自身
- 在mockConnectFuture上模拟`channel()`返回一个模拟的Channel(mockChannel)
然后,对于这个mockChannel,我们需要:
- 模拟`writeAndFlush`方法,返回一个ChannelFuture(我们命名为mockWriteFuture),并模拟这个mockWriteFuture的`sync()`方法(返回自身)。
- 模拟`closeFuture`方法,返回一个ChannelFuture(我们命名为mockCloseFuture),并模拟这个mockCloseFuture的`sync()`方法(返回自身)。
另外,我们还需要模拟日志记录,因为代码中使用了log,如果不处理,可能会因为log为null而出现NPE(但原代码中已经使用了@Slf4j,在测试环境中需要确保log被模拟或者可以正常运行,但这里我们主要关注模拟Netty部分)。
但是,我们注意到在之前的测试中已经通过某种方式注入了模拟的Logger(在之前的回答中有提到),但在这个测试方法中并没有做。如果出现log为null的NPE,我们还需要处理log。
不过,根据错误堆栈,NPE发生在NettyClient.java的第65行,我们看一下原代码第65行是什么:
原代码:
Channel channel = b.connect(host, port).sync().channel();
所以,问题可能出现在这里。根据我们的分析,可能是因为`b.connect(host, port)`返回了null,或者`sync()`返回了null,或者`channel()`返回了null。
在测试中,我们确实模拟了`connect`返回mockBindFuture(但命名应该改为mockConnectFuture),然后模拟了`sync()`返回自身,再模拟了`channel()`返回mockChannel。所以,只要这些模拟都正确设置,就不会有NPE。
但是,我们之前错误地模拟了`bind`而不是`connect`,导致`connect`方法没有被模拟,返回了null(因为mock对象默认返回null)。所以,将`bind`改为`connect`后,应该可以解决。
另外,我们还需要模拟`writeAndFlush`和`closeFuture`,否则在后续步骤中也会出现NPE。
因此,修改后的测试方法如下:
```java
@Test
void testClientRun() throws Exception {
try (MockedConstruction<Bootstrap> ignored = mockConstruction(Bootstrap.class,
(mock, context) -> {
// 链式调用模拟
when(mock.group(any())).thenReturn(mock);
when(mock.channel(any())).thenReturn(mock);
when(mock.option(any(), any())).thenReturn(mock);
when(mock.handler(any())).thenReturn(mock);
// 创建模拟的ChannelFuture(用于connect)
ChannelFuture mockConnectFuture = mock(ChannelFuture.class);
// 模拟connect方法返回mockConnectFuture
when(mock.connect(anyString(), anyInt())).thenReturn(mockConnectFuture);
// 模拟sync返回自身
when(mockConnectFuture.sync()).thenReturn(mockConnectFuture);
// 创建模拟的Channel
Channel mockChannel = mock(Channel.class);
when(mockConnectFuture.channel()).thenReturn(mockChannel);
// 模拟writeAndFlush
ChannelFuture mockWriteFuture = mock(ChannelFuture.class);
when(mockChannel.writeAndFlush(any())).thenReturn(mockWriteFuture);
when(mockWriteFuture.sync()).thenReturn(mockWriteFuture);
// 模拟closeFuture
ChannelFuture mockCloseFuture = mock(ChannelFuture.class);
when(mockChannel.closeFuture()).thenReturn(mockCloseFuture);
when(mockCloseFuture.sync()).thenReturn(mockCloseFuture);
})) {
new NettyClient("127.0.0.1",9000).connect("greet","da");
Bootstrap bootstrap = ignored.constructed().get(0);
verify(bootstrap).connect(anyString(), anyInt());
}
}
```
这样修改后,应该可以避免NPE。
但是,我们还需要注意,在connect方法中,我们使用了`JsonUtil.toJson`,这可能会抛出JsonProcessingException。在测试中,我们传递了参数"greet"和"da",这应该不会导致异常。但为了安全,我们可以捕获这个异常或者确保它被处理。
另外,如果`JsonUtil.toJson`中使用了未模拟的静态方法,也可能导致NPE,但这里我们假设它是可以正常工作的。
最后,我们还需要注意,在测试中,我们模拟了EventLoopGroup的创建吗?实际上,在NettyClient中,我们创建了NioEventLoopGroup,但是在测试中,我们并没有模拟它。不过,由于我们模拟了Bootstrap,而Bootstrap的group方法被模拟了(它接受任何参数并返回自身),所以不会使用到真实的EventLoopGroup,因此不会导致问题。
但是,在finally块中,group.shutdownGracefully()会被调用。由于我们并没有模拟NioEventLoopGroup,所以这里会调用真实的方法。真实的方法会关闭线程池,但在测试中,我们并没有真正启动线程池,所以应该没有问题。但为了安全,我们也可以模拟NioEventLoopGroup。
不过,在这个测试中,我们主要关注的是Bootstrap的connect调用,所以暂时不处理EventLoopGroup的模拟。如果出现与shutdownGracefully相关的错误,我们可以再考虑模拟它。
因此,我们按照上述修改测试代码,应该可以解决NullPointerException。
如果仍然出现NPE,请检查堆栈信息,确定NPE发生的具体位置,然后检查对应的模拟设置。
另外,我们还可以考虑使用@Mock注解和MockitoAnnotations.openMocks(this)来简化模拟,但这里使用了MockedConstruction,所以不需要。
修改后的测试代码如上,运行测试看是否通过。</think>### 解决单元测试中的 NullPointerException 问题
#### 问题分析
```mermaid
graph TD
A[NullPointerException] --> B[关键模拟缺失]
B --> C[connect方法未模拟]
B --> D[channel()返回null]
B --> E[writeAndFlush未处理]
C --> F[未模拟b.connect方法]
D --> G[未设置channel返回值]
E --> H[未处理消息发送流程]
```
#### 完整修复方案
```java
@Test
void testClientRun() throws Exception {
try (MockedConstruction<Bootstrap> ignored = mockConstruction(Bootstrap.class,
(mock, context) -> {
// 链式调用模拟
when(mock.group(any())).thenReturn(mock);
when(mock.channel(any())).thenReturn(mock);
when(mock.option(any(), any())).thenReturn(mock);
when(mock.handler(any())).thenReturn(mock);
// 关键修复点1:模拟connect方法
ChannelFuture mockConnectFuture = mock(ChannelFuture.class);
when(mock.connect(anyString(), anyInt())).thenReturn(mockConnectFuture);
// 关键修复点2:模拟sync()和channel()
when(mockConnectFuture.sync()).thenReturn(mockConnectFuture);
Channel mockChannel = mock(Channel.class);
when(mockConnectFuture.channel()).thenReturn(mockChannel);
// 关键修复点3:模拟消息发送
ChannelFuture mockWriteFuture = mock(ChannelFuture.class);
when(mockChannel.writeAndFlush(any())).thenReturn(mockWriteFuture);
when(mockWriteFuture.sync()).thenReturn(mockWriteFuture);
// 关键修复点4:模拟关闭流程
ChannelFuture mockCloseFuture = mock(ChannelFuture.class);
when(mockChannel.closeFuture()).thenReturn(mockCloseFuture);
when(mockCloseFuture.sync()).thenReturn(mockCloseFuture);
// 关键修复点5:模拟日志记录
Logger mockLogger = mock(Logger.class);
try {
Field logField = NettyClient.class.getDeclaredField("log");
logField.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(logField, logField.getModifiers() & ~Modifier.FINAL);
logField.set(null, mockLogger);
} catch (Exception e) {
throw new RuntimeException(e);
}
})) {
new NettyClient("127.0.0.1",9000).connect("greet","da");
Bootstrap bootstrap = ignored.constructed().get(0);
verify(bootstrap).connect(eq("127.0.0.1"), eq(9000));
}
}
```
#### 关键修复点说明
1. **模拟connect方法**:
```java
ChannelFuture mockConnectFuture = mock(ChannelFuture.class);
when(mock.connect(anyString(), anyInt())).thenReturn(mockConnectFuture);
```
原测试中缺失了对`b.connect()`方法的模拟,导致实际调用时返回null
2. **完整通道模拟链**:
```java
// 模拟连接同步
when(mockConnectFuture.sync()).thenReturn(mockConnectFuture);
// 模拟通道对象
Channel mockChannel = mock(Channel.class);
when(mockConnectFuture.channel()).thenReturn(mockChannel);
```
3. **消息发送流程模拟**:
```java
// 模拟消息发送
ChannelFuture mockWriteFuture = mock(ChannelFuture.class);
when(mockChannel.writeAndFlush(any())).thenReturn(mockWriteFuture);
when(mockWriteFuture.sync()).thenReturn(mockWriteFuture);
```
4. **日志注入**:
```java
// 反射注入模拟日志
Field logField = NettyClient.class.getDeclaredField("log");
logField.setAccessible(true);
// ... 修改final修饰符
logField.set(null, mockLogger);
```
#### 增强版测试方案
```java
@Test
void shouldCompleteFullClientLifecycle() throws Exception {
// 创建消息捕获器
ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class);
try (MockedConstruction<Bootstrap> ignored = mockConstruction(Bootstrap.class,
(mock, context) -> {
// ... [同上模拟设置]
// 捕获发送的消息
when(mockChannel.writeAndFlush(any())).thenAnswer(inv -> {
String json = inv.getArgument(0);
messageCaptor.capture(json);
return mockWriteFuture;
});
})) {
// 执行测试
new NettyClient("127.0.0.1", 9000).connect("greet", "Alice");
// 验证消息内容
String sentJson = messageCaptor.getValue();
assertTrue(sentJson.contains("\"api\":\"greet\""));
assertTrue(sentJson.contains("\"msg\":\"Alice\""));
// 验证日志调用
verify(mockLogger).info(eq("request: {}"), anyString());
}
}
```
#### 常见错误排查表
| 错误位置 | 可能原因 | 解决方案 |
|----------|----------|----------|
| `b.connect()` | 未模拟connect方法 | 添加`when(mock.connect(...))` |
| `.sync().channel()` | sync()返回null | 模拟`when(mockConnectFuture.sync())` |
| `channel.writeAndFlush()` | channel为null | 设置`when(mockConnectFuture.channel())` |
| `log.info()` | @Slf4j未初始化 | 反射注入模拟Logger |
| `group.shutdownGracefully()` | EventLoopGroup未模拟 | 添加NioEventLoopGroup模拟 |
#### 高级测试技巧
**模拟超时场景**:
```java
@Test
void shouldHandleConnectTimeout() throws Exception {
try (MockedConstruction<Bootstrap> ignored = mockConstruction(Bootstrap.class,
(mock, context) -> {
ChannelFuture mockConnectFuture = mock(ChannelFuture.class);
when(mock.connect(anyString(), anyInt())).thenReturn(mockConnectFuture);
// 模拟连接超时
when(mockConnectFuture.sync()).thenThrow(new TimeoutException("Connection timed out"));
})) {
assertThrows(TimeoutException.class, () ->
new NettyClient("127.0.0.1",9000).connect("greet","da"));
}
}
```
**验证管道初始化**:
```java
@Test
void shouldInitializePipelineCorrectly() throws Exception {
try (MockedConstruction<Bootstrap> ignored = mockConstruction(Bootstrap.class,
(mock, context) -> {
// ... [其他模拟设置]
// 捕获管道初始化
ArgumentCaptor<ChannelInitializer> initializerCaptor =
ArgumentCaptor.forClass(ChannelInitializer.class);
when(mock.handler(initializerCaptor.capture())).thenReturn(mock);
// 模拟通道创建
EmbeddedChannel testChannel = new EmbeddedChannel();
initializerCaptor.getValue().initChannel(testChannel);
// 验证管道处理器
assertNotNull(testChannel.pipeline().get(LengthFieldBasedFrameDecoder.class));
assertNotNull(testChannel.pipeline().get(StringDecoder.class));
assertNotNull(testChannel.pipeline().get(NettyClientHandler.class));
})) {
new NettyClient("127.0.0.1",9000).connect("test","data");
}
}
```
阅读全文