Search code examples
javaspring-bootjunit

Spring Boot JUnit Test - Resttemplate returns null in ServiceTest (How to mock restTemplate exchange)


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


Solution

  • 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 :

    1. Create a @Configuration class dedicated to your tests, that will create mocked spring beans :
        @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.

    1. 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.

    2. I don't know why yet but, when debugging, we can see that the mocked restTemplate (@13398) and rateRepository(@13396) in your test class:

    in test

    are not the same mocked objects than the ones injected in the RestService class (restTemplate@17879 and rateRepository@17878) :

    in service

    I found a solution that is not totally satisfying (sorry) but that works :

    • Remove @InjectMocks there:
        @InjectMocks
        private RateService rateService;
    
    • instantiate yourself the 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 !