使用JUnit5和Mockito模拟Reactor Netty HttpClient报错及优化方案
问题描述
我在用JUnit5和Mockito测试Spring Boot应用中的类,其中某个方法用了Reactor Netty HttpClient。参照WebClient的模拟方式逐段mock HttpClient时,出现以下错误:
org.mockito.exceptions.misusing.MissingMethodInvocationException:
when() requires an argument which has to be 'a method call on a mock'.
For example:
when(mock.getArticles()).thenReturn(articles);Also, this error might show up because:
- you stub either of: final/private/equals()/hashCode() methods.
Those methods cannot be stubbed/verified.
Mocking methods declared on non-public parent classes is not supported.- inside when() you don't call method on mock but on some other object.
我的测试代码:
import io.netty.handler.codec.http.HttpMethod; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.Spy; import reactor.core.publisher.Mono; import reactor.netty.ByteBufFlux; import reactor.netty.ByteBufMono; import reactor.netty.http.client.HttpClient; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; public class ServiceTest { @Spy private HttpClient httpClient; @Mock HttpClient.RequestSender requestSender; @Mock ByteBufFlux byteBufFlux; @Mock ByteBufMono byteBufMono; @BeforeEach void setup() { httpClient = HttpClient.create(); } @Test void testFetchImageFromURLBase64Success() { mockStatic( HttpClient.class ); mockHttpClient(); var host = "example.com"; var port = 80; var imageUrl = "http://example.com/image.jpg"; when( httpClient.request( HttpMethod.GET ) ) .thenReturn( requestSender ); var res = Service.fetchImageFromURLBase64( host, port, imageUrl ); assertNotNull( res ); } private void mockHttpClient() { var mockImageBytes = new byte[]{ (byte)0x89, (byte)0x50 }; when( httpClient.request( HttpMethod.GET ) ) .thenReturn( requestSender ); when( requestSender.uri( anyString() ) ) .thenReturn( requestSender ); when( requestSender.responseContent() ) .thenReturn( byteBufFlux ); when( byteBufFlux.aggregate() ) .thenReturn( byteBufMono ); when( byteBufMono.asByteArray() ) .thenReturn( Mono.just( mockImageBytes ) ); } }
待测试的目标方法:
public static String fetchImageFromURLBase64( String hostName, int port, String imageUrl ) { try { var httpClient = HttpClient.create() .proxy( p -> p.type( ProxyProvider.Proxy.HTTP ).host( hostName ).port( port ) ); var response = httpClient .request( HttpMethod.GET ) .uri( imageUrl ) .responseContent() .aggregate() .asByteArray() .block(); if( response == null || response.length == 0 ) { log.error( "Could not retrieve image bytes from url: {}", imageUrl ); return null; } return Base64.getEncoder().encodeToString( response ); } catch( Exception e ) { // In case the service returns a 500 we don't want the application to crash log.error( "Error retrieving image from URL: {} with error message: ", imageUrl, e ); return null; } }
请问出现该错误的原因是什么?有没有更简便的模拟HttpClient调用的方案?
错误原因
- Mock对象未正确使用:你用
@Spy标注了httpClient,但在setup方法里直接把它赋值为HttpClient.create()返回的真实实例,后续when(httpClient.request(...))是在真实对象上调用方法,Mockito的when()只允许在mock/spy对象上进行stub,这直接触发了错误。 - 静态方法导致依赖无法注入:目标方法是静态方法,且内部直接创建
HttpClient实例,完全和测试类中的httpClientmock对象隔离,就算你正确mock了测试类里的对象,目标方法也不会用到。 - 静态mock未生效:你调用了
mockStatic(HttpClient.class)但没有stubHttpClient.create()方法,导致目标方法里的HttpClient.create()还是返回真实实例,而非mock对象。
更简便的模拟方案
方案一:重构代码(推荐)
把静态方法改成实例方法,并通过构造注入HttpClient,这样测试时可以直接传入mock对象,完全不用处理静态mock:
重构后的Service类:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.netty.http.client.HttpClient; import reactor.netty.http.client.ProxyProvider; import io.netty.handler.codec.http.HttpMethod; import java.util.Base64; public class Service { private static final Logger log = LoggerFactory.getLogger(Service.class); private final HttpClient httpClient; // 构造注入HttpClient public Service(HttpClient httpClient) { this.httpClient = httpClient; } public String fetchImageFromURLBase64(String hostName, int port, String imageUrl) { try { // 使用注入的HttpClient,添加代理配置 var proxyHttpClient = httpClient.proxy(p -> p.type(ProxyProvider.Proxy.HTTP).host(hostName).port(port)); var response = proxyHttpClient .request(HttpMethod.GET) .uri(imageUrl) .responseContent() .aggregate() .asByteArray() .block(); if (response == null || response.length == 0) { log.error("Could not retrieve image bytes from url: {}", imageUrl); return null; } return Base64.getEncoder().encodeToString(response); } catch (Exception e) { log.error("Error retrieving image from URL: {} with error message: ", imageUrl, e); return null; } } }
对应的测试代码:
import io.netty.handler.codec.http.HttpMethod; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Mono; import reactor.netty.ByteBufFlux; import reactor.netty.ByteBufMono; import reactor.netty.http.client.HttpClient; import reactor.netty.http.client.HttpClient.RequestSender; import reactor.netty.http.client.ProxyProvider; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class ServiceTest { @Mock private HttpClient httpClient; @Mock private HttpClient proxyHttpClient; // 对应添加代理后的HttpClient @Mock private RequestSender requestSender; @Mock private ByteBufFlux byteBufFlux; @Mock private ByteBufMono byteBufMono; @InjectMocks private Service service; @Test void testFetchImageFromURLBase64Success() { var mockImageBytes = new byte[]{(byte) 0x89, (byte) 0x50}; var host = "example.com"; var port = 80; var imageUrl = "http://example.com/image.jpg"; // 链式mock调用流程 when(httpClient.proxy(any())) .thenReturn(proxyHttpClient); when(proxyHttpClient.request(HttpMethod.GET)) .thenReturn(requestSender); when(requestSender.uri(anyString())) .thenReturn(requestSender); when(requestSender.responseContent()) .thenReturn(byteBufFlux); when(byteBufFlux.aggregate()) .thenReturn(byteBufMono); when(byteBufMono.asByteArray()) .thenReturn(Mono.just(mockImageBytes)); var res = service.fetchImageFromURLBase64(host, port, imageUrl); assertNotNull(res); } }
方案二:不重构,用Mockito静态mock
如果不想修改原有代码,可以正确mockHttpClient.create()方法,让它返回mock对象,再链式mock后续调用:
测试代码修改后:
import io.netty.handler.codec.http.HttpMethod; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Mono; import reactor.netty.ByteBufFlux; import reactor.netty.ByteBufMono; import reactor.netty.http.client.HttpClient; import reactor.netty.http.client.HttpClient.RequestSender; import reactor.netty.http.client.ProxyProvider; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class ServiceTest { @Mock private HttpClient httpClient; @Mock private HttpClient proxyHttpClient; @Mock private RequestSender requestSender; @Mock private ByteBufFlux byteBufFlux; @Mock private ByteBufMono byteBufMono; @Test void testFetchImageFromURLBase64Success() { var mockImageBytes = new byte[]{(byte) 0x89, (byte) 0x50}; var host = "example.com"; var port = 80; var imageUrl = "http://example.com/image.jpg"; // mock静态方法HttpClient.create(),返回我们的mock对象 try (var mockedStatic = mockStatic(HttpClient.class)) { mockedStatic.when(HttpClient::create).thenReturn(httpClient); // 链式mock代理配置和后续调用 when(httpClient.proxy(any())) .thenReturn(proxyHttpClient); when(proxyHttpClient.request(HttpMethod.GET)) .thenReturn(requestSender); when(requestSender.uri(anyString())) .thenReturn(requestSender); when(requestSender.responseContent()) .thenReturn(byteBufFlux); when(byteBufFlux.aggregate()) .thenReturn(byteBufMono); when(byteBufMono.asByteArray()) .thenReturn(Mono.just(mockImageBytes)); var res = Service.fetchImageFromURLBase64(host, port, imageUrl); assertNotNull(res); } } }
内容的提问来源于stack exchange,提问作者tdog
相关产品推荐
相关产品推荐

