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:
- HttpEntity不匹配:业务代码中
headersEntity设置了Accept: application/json头,但测试里构造的expectedHttpEntity未添加该头,HttpEntity的equals方法会严格校验所有属性,导致匹配失败。 - 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
相关产品推荐
相关产品推荐

