I have a problem to write the JUnit Test with the usage of resttemplate.
When I run the testCalculateRate
, I got this error message shown below
java.lang.NullPointerException: Cannot invoke "org.springframework.http.ResponseEntity.getBody()" because "responseEntity" is null
I noticed that ResponseEntity<RateResponse> responseEntity = restTemplate.exchange(url, HttpMethod.GET, headersEntity, RateResponse.class);
returns null
.
Next, I debug the code
Here is saveRatesFromApi method of RateService
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); ---> ERROR LINE
RateResponse rates = responseEntity.getBody();
RateEntity entity = convert(rates);
return rateRepository.save(entity);
}
Here RateServiceTest shown below
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() {
// Initialize mocks
MockitoAnnotations.openMocks(this);
// Mocked data
EnumCurrency base = EnumCurrency.EUR;
List<EnumCurrency> targets = Arrays.asList(EnumCurrency.USD, EnumCurrency.GBP);
LocalDate date = LocalDate.of(2023, 5, 22);
// Mocked rate entity
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);
// Mock repository behavior
when(rateRepository.findOneByDate(date)).thenReturn(Optional.of(mockedRateEntity));
// Mock API response
RateResponse mockedRateResponse = RateResponse.builder()
.base(base)
.rates(rates)
.date(date)
.build();
// Create a HttpHeaders object and set the "apikey" header
HttpHeaders headers = new HttpHeaders();
headers.add("apikey", EXCHANGE_API_API_KEY);
// Create a mock response entity with the expected headers and body
ResponseEntity<RateResponse> mockedResponseEntity = ResponseEntity.ok()
.headers(headers)
.body(mockedRateResponse);
// Mock RestTemplate behavior
when(restTemplate.exchange(
anyString(),
eq(HttpMethod.GET),
any(HttpEntity.class),
eq(RateResponse.class)
)).thenReturn(mockedResponseEntity);
// Call the method
RateDto result = rateService.calculateRate(base, targets, date);
// Verify repository method was called
verify(rateRepository, times(1)).findOneByDate(date);
// Verify API call was made
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)
);
// Verify the result
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;
}
}
How can I fix the issue?
Here is the repo : Link
I missed the fact that BaseServiceTest is a SpringBootTest. I think you got problems because your original code tries to use, at the same time, Mockito injection and Spring injection.
A clean solution if you want a SpringBootTest is to :
@Profile("test")
@Configuration
public class TestConfiguration {
@Bean
@Primary
public RateRepository mockRateRepository() {
return mock(RateRepository.class);
}
}
And just let Spring inject the services and repository that you use into your test :
class RateServiceTest extends BaseServiceTest {
@Autowired
private RateRepository rateRepository;
@Autowired
private RateService rateService;
@Test
void testCalculateRate() {
// Mocked data
EnumCurrency base = EnumCurrency.EUR;
List<EnumCurrency> targets = Arrays.asList(EnumCurrency.USD, EnumCurrency.GBP);
LocalDate date = LocalDate.of(2023, 5, 22);
// Mocked rate entity
RateEntity mockedRateEntity = new RateEntity();
mockedRateEntity.setBase(base);
mockedRateEntity.setDate(date);
Map<EnumCurrency, Double> rates = new EnumMap<>(EnumCurrency.class);
rates.put(EnumCurrency.USD, 1.2);
rates.put(EnumCurrency.GBP, 0.9);
mockedRateEntity.setRates(rates);
// Mock repository behavior
when(rateRepository.findOneByDate(date)).thenReturn(Optional.of(mockedRateEntity));
// Mock API response
RateResponse mockedRateResponse = RateResponse.builder()
.base(base)
.rates(rates)
.date(date)
.build();
// Create a mock response entity with the expected headers and body
ResponseEntity<RateResponse> mockedResponseEntity = ResponseEntity.ok()
.body(mockedRateResponse);
// Call the method
RateDto result = rateService.calculateRate(base, targets, date);
// Verify the result
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)
);
// Verify repository method was called
verify(rateRepository, times(1)).findOneByDate(date);
}
}
You now have multiple solutions for your problem.
Old answer
I don't have a complete solution for you but I hope my analysis will help you.
Your first problem is not that restTemplate.exchange return null but that rateRepository.findOneByDate(date) does not return your mockedRateEntity. If your test was working fine, restTemplate would not be called, as it is called only when the rateRepository does not return an object. But you mocked it so as it returns one.
I don't know why yet but, when debugging, we can see that the mocked restTemplate (@13398) and rateRepository(@13396) in your test class:
are not the same mocked objects than the ones injected in the RestService class (restTemplate@17879 and rateRepository@17878) :
I found a solution that is not totally satisfying (sorry) but that works :
@InjectMocks
private RateService rateService;
rateService = new RateService(rateRepository, restTemplate);
You'll also need to remove this part :
when(restTemplate.exchange(
anyString(),
eq(HttpMethod.GET),
any(HttpEntity.class),
eq(RateResponse.class)
)).thenReturn(mockedResponseEntity);
because restTemplate won't be called if rateRepository returns a result.
Complete test class that works with the solution I found is :
package com.exchangeapi.currencyexchange.service;
import com.exchangeapi.currencyexchange.base.BaseServiceTest;
import com.exchangeapi.currencyexchange.dto.RateDto;
import com.exchangeapi.currencyexchange.dto.RateInfoDto;
import com.exchangeapi.currencyexchange.entity.RateEntity;
import com.exchangeapi.currencyexchange.entity.enums.EnumCurrency;
import com.exchangeapi.currencyexchange.payload.response.RateResponse;
import com.exchangeapi.currencyexchange.repository.RateRepository;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.time.LocalDate;
import java.util.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
class RateServiceTest extends BaseServiceTest {
@Mock
private RateRepository rateRepository;
@Mock
private RestTemplate restTemplate;
@Test
void testCalculateRate() {
// Mocked data
EnumCurrency base = EnumCurrency.EUR;
List<EnumCurrency> targets = Arrays.asList(EnumCurrency.USD, EnumCurrency.GBP);
LocalDate date = LocalDate.of(2023, 5, 22);
// Mocked rate entity
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);
// Mock repository behavior
when(rateRepository.findOneByDate(date)).thenReturn(Optional.of(mockedRateEntity));
// Mock API response
RateResponse mockedRateResponse = RateResponse.builder()
.base(base)
.rates(rates)
.date(date)
.build();
// Create a mock response entity with the expected headers and body
ResponseEntity<RateResponse> mockedResponseEntity = ResponseEntity.ok()
.body(mockedRateResponse);
// Mock RestTemplate behavior
// when(restTemplate.exchange(
// anyString(),
// eq(HttpMethod.GET),
// any(HttpEntity.class),
// eq(RateResponse.class)
// )).thenReturn(mockedResponseEntity);
RateService rateService = new RateService(rateRepository, restTemplate);
// Call the method
RateDto result = rateService.calculateRate(base, targets, date);
// Verify API call was made
// String expectedUrl = getExchangeUrl(date, base, targets);
// HttpHeaders expectedHeaders = new HttpHeaders();
// expectedHeaders.add("apikey", EXCHANGE_API_API_KEY);
// HttpEntity<String> expectedHttpEntity = new HttpEntity<>(expectedHeaders);
// Verify the result
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)
);
// Verify repository method was called
verify(rateRepository, times(1)).findOneByDate(date);
// verify(restTemplate, times(1)).exchange(
// eq(expectedUrl),
// eq(HttpMethod.GET),
// eq(expectedHttpEntity),
// eq(RateResponse.class)
// );
}
// 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;
// }
}
I let the useless code commented to let you easily see what I changed / removed.
Good luck !