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

Spring Boot JUnit测试:RestTemplate返回Null如何Mock exchange方法

问题:Spring Boot JUnit测试中RestTemplate.exchange返回NullPointerException

运行testCalculateRate方法时抛出异常:

java.lang.NullPointerException: Cannot invoke "org.springframework.http.ResponseEntity.getBody()" because "responseEntity" is null 

调试发现RateService的saveRatesFromApi方法中,以下代码返回null:

ResponseEntity<RateResponse> responseEntity = restTemplate.exchange(url, HttpMethod.GET, headersEntity, RateResponse.class);

相关代码片段

RateService的saveRatesFromApi方法

private RateEntity saveRatesFromApi(LocalDate rateDate, EnumCurrency base, List<EnumCurrency> targets) {
    log.info("ExchangeService | saveRatesFromApi is called");

    HttpHeaders headers = new HttpHeaders();
    headers.add("apikey", EXCHANGE_API_API_KEY);
    headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
    final HttpEntity<String> headersEntity = new HttpEntity<>(headers);
    String url = getExchangeUrl(rateDate, base, targets);

    ResponseEntity<RateResponse> responseEntity = restTemplate.exchange(url, HttpMethod.GET, headersEntity, RateResponse.class);

    RateResponse rates = responseEntity.getBody();
    RateEntity entity = convert(rates);
    return rateRepository.save(entity);
}

RateServiceTest测试代码

import static com.exchangeapi.currencyexchange.constants.Constants.EXCHANGE_API_API_KEY;
import static com.exchangeapi.currencyexchange.constants.Constants.EXCHANGE_API_BASE_URL;

class RateServiceTest extends BaseServiceTest {

    @Mock
    private RateRepository rateRepository;

    @Mock
    private RestTemplate restTemplate;

    @InjectMocks
    private RateService rateService;

    @Test
    void testCalculateRate() {
        MockitoAnnotations.openMocks(this);

        EnumCurrency base = EnumCurrency.EUR;
        List<EnumCurrency> targets = Arrays.asList(EnumCurrency.USD, EnumCurrency.GBP);
        LocalDate date = LocalDate.of(2023, 5, 22);

        RateEntity mockedRateEntity = new RateEntity();
        mockedRateEntity.setBase(base);
        mockedRateEntity.setDate(date);
        Map<EnumCurrency, Double> rates = new HashMap<>();
        rates.put(EnumCurrency.USD, 1.2);
        rates.put(EnumCurrency.GBP, 0.9);
        mockedRateEntity.setRates(rates);

        when(rateRepository.findOneByDate(date)).thenReturn(Optional.of(mockedRateEntity));

        RateResponse mockedRateResponse = RateResponse.builder()
                .base(base)
                .rates(rates)
                .date(date)
                .build();

        HttpHeaders headers = new HttpHeaders();
        headers.add("apikey", EXCHANGE_API_API_KEY);

        ResponseEntity<RateResponse> mockedResponseEntity = ResponseEntity.ok()
                .headers(headers)
                .body(mockedRateResponse);

        when(restTemplate.exchange(
                anyString(),
                eq(HttpMethod.GET),
                any(HttpEntity.class),
                eq(RateResponse.class)
        )).thenReturn(mockedResponseEntity);

        RateDto result = rateService.calculateRate(base, targets, date);

        verify(rateRepository, times(1)).findOneByDate(date);

        String expectedUrl = getExchangeUrl(date, base, targets);
        HttpHeaders expectedHeaders = new HttpHeaders();
        expectedHeaders.add("apikey", EXCHANGE_API_API_KEY);
        HttpEntity<String> expectedHttpEntity = new HttpEntity<>(expectedHeaders);
        verify(restTemplate, times(1)).exchange(
                eq(expectedUrl),
                eq(HttpMethod.GET),
                eq(expectedHttpEntity),
                eq(RateResponse.class)
        );

        assertThat(result.getBase()).isEqualTo(base);
        assertThat(result.getDate()).isEqualTo(date);
        assertThat(result.getRates()).hasSize(2);
        assertThat(result.getRates()).containsExactlyInAnyOrder(
                new RateInfoDto(EnumCurrency.USD, 1.2),
                new RateInfoDto(EnumCurrency.GBP, 0.9)
        );
    }

    private String getExchangeUrl(LocalDate rateDate, EnumCurrency base, List<EnumCurrency> targets) {
        String symbols = String.join("%2C", targets.stream().map(EnumCurrency::name).toArray(String[]::new));
        return EXCHANGE_API_BASE_URL + rateDate + "?symbols=" + symbols + "&base=" + base;
    }
}

问题原因

核心是RestTemplate的Mock匹配失败,导致实际调用时未命中预设的Mock行为,返回默认null:

  1. HttpEntity不匹配:业务代码中headersEntity设置了Accept: application/json头,但测试里构造的expectedHttpEntity未添加该头,HttpEntity的equals方法会严格校验所有属性,导致匹配失败。
  2. Mock设置与验证规则不一致:Mock设置时用any(HttpEntity.class)宽松匹配,但验证时用eq(expectedHttpEntity)精确匹配,逻辑偏差导致验证环节冲突。

解决方案

1. 修正HttpEntity的构造

确保测试中构造的expectedHttpEntity与业务代码的headersEntity完全一致:

HttpHeaders expectedHeaders = new HttpHeaders();
expectedHeaders.add("apikey", EXCHANGE_API_API_KEY);
expectedHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); // 新增这行
HttpEntity<String> expectedHttpEntity = new HttpEntity<>(expectedHeaders);

2. 统一Mock的匹配规则

方式一:用参数捕获器验证(推荐)

避免精确匹配的复杂性,用ArgumentCaptor捕获实际调用参数并验证:

// 替换原RestTemplate验证代码
ArgumentCaptor<String> urlCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<HttpEntity> entityCaptor = ArgumentCaptor.forClass(HttpEntity.class);
verify(restTemplate, times(1)).exchange(
        urlCaptor.capture(),
        eq(HttpMethod.GET),
        entityCaptor.capture(),
        eq(RateResponse.class)
);

// 验证URL正确性
assertThat(urlCaptor.getValue()).isEqualTo(expectedUrl);
// 验证Headers包含必要参数
HttpHeaders capturedHeaders = entityCaptor.getValue().getHeaders();
assertThat(capturedHeaders.getFirst("apikey")).isEqualTo(EXCHANGE_API_API_KEY);
assertThat(capturedHeaders.getAccept()).contains(MediaType.APPLICATION_JSON);

方式二:全精确匹配

如果必须严格验证所有参数,确保Mock设置和验证的参数完全一致:

// 构造与业务代码一致的HttpEntity
HttpHeaders mockHeaders = new HttpHeaders();
mockHeaders.add("apikey", EXCHANGE_API_API_KEY);
mockHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
HttpEntity<String> mockHttpEntity = new HttpEntity<>(mockHeaders);

// 设置Mock时用精确匹配
when(restTemplate.exchange(
        eq(expectedUrl),
        eq(HttpMethod.GET),
        eq(mockHttpEntity),
        eq(RateResponse.class)
)).thenReturn(mockedResponseEntity);

// 验证时用相同参数
verify(restTemplate, times(1)).exchange(
        eq(expectedUrl),
        eq(HttpMethod.GET),
        eq(mockHttpEntity),
        eq(RateResponse.class)
);

3. 额外优化

将getExchangeUrl方法提取到公共工具类(如UrlUtils或Constants),避免业务代码与测试代码重复实现,防止URL构造不一致导致匹配失败。

内容的提问来源于stack exchange,提问作者Sercan Noyan Germiyanoğlu

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 08:44:52