You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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:

  1. 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.
  2. 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调用的方案?


错误原因
  1. Mock对象未正确使用:你用@Spy标注了httpClient,但在setup方法里直接把它赋值为HttpClient.create()返回的真实实例,后续when(httpClient.request(...))是在真实对象上调用方法,Mockito的when()只允许在mock/spy对象上进行stub,这直接触发了错误。
  2. 静态方法导致依赖无法注入:目标方法是静态方法,且内部直接创建HttpClient实例,完全和测试类中的httpClient mock对象隔离,就算你正确mock了测试类里的对象,目标方法也不会用到。
  3. 静态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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 03:39:50