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

2

1

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

Scrannel answered 22/5, 2023 at 18:10 Comment(4)
For a reason I don't yet understand, the mocked objects in the RateService are not the ones you configure in your test.Glyconeogenesis
@SebPerp I have no idea why resttemplate returns null. I hope you can help me.Burnett
Did you try to stub like this? when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(RateResponse.class))) .thenReturn(responseEntity); (Could be that you are stubbing the wrong method)Milagro
@Feelfree I tried to change the matchers of the method, the issue is not the stubbing. Stubbing done in the test class is not "passed" to the RateService as the objects are different. If you inject "manually" the mocks in the service, stubbing works.Glyconeogenesis
G
0

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 !

Glyconeogenesis answered 22/5, 2023 at 23:42 Comment(0)
S
0

Here is my answer shown below

Here is the BaseServiceTest shown below

@ExtendWith(MockitoExtension.class)
@ActiveProfiles(value = "test")
public abstract class BaseServiceTest {

}

Here is the RateServiceTest shown below

class RateServiceTest extends BaseServiceTest {


    @Mock
    private RateRepository rateRepository;

    @Mock
    private RestTemplate restTemplate;

    @InjectMocks
    private RateService rateService;


    @Test
    void whenCalculateRate_butRateRepositoryFoundOne() {

        // 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);

        List<RateInfoDto> rateInfoList = targets.stream()
                .map(currency -> new RateInfoDto(currency, rates.get(currency)))
                .collect(Collectors.toList());

        RateDto expected = RateDto.builder()
                .id(mockedRateEntity.getId())
                .base(mockedRateEntity.getBase())
                .date(mockedRateEntity.getDate())
                .rates(rateInfoList)
                .build();

        // Mock repository behavior
        when(rateRepository.findOneByDate(date)).thenReturn(Optional.of(mockedRateEntity));


        // Call the method
        RateDto result = rateService.calculateRate(base, targets, date);

        // Verify the result
        assertThat(result.getBase()).isEqualTo(expected.getBase());
        assertThat(result.getDate()).isEqualTo(expected.getDate());
        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);

        // The saveRatesFromApi method won't be run because the rateRepository.findOneByDate return the mockedRateEntity
        verify(restTemplate, times(0)).exchange(
                anyString(),
                eq(HttpMethod.GET),
                any(HttpEntity.class),
                eq(RateResponse.class)
        );
    }

    @Test
    void whenCalculateRate_andRateRepositoryNotFound() {
        // 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);

        List<RateInfoDto> rateInfoList = targets.stream()
                .map(currency -> new RateInfoDto(currency, rates.get(currency)))
                .collect(Collectors.toList());

        RateDto expected = RateDto.builder()
                .id(mockedRateEntity.getId())
                .base(mockedRateEntity.getBase())
                .date(mockedRateEntity.getDate())
                .rates(rateInfoList)
                .build();

        // Mock API response
        RateResponse mockedRateResponse = RateResponse.builder()
                .base(base)
                .rates(rates)
                .date(date)
                .build();

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

        // Mock repository behavior
        when(rateRepository.findOneByDate(date)).thenReturn(Optional.empty()); // Return null to simulate repository not finding the entity

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

        // Mock saveRatesFromApi behavior
        when(rateRepository.save(any(RateEntity.class))).thenReturn(mockedRateEntity);

        // Call the method
        RateDto result = rateService.calculateRate(base, targets, date);

        // Verify the result
        assertThat(result.getBase()).isEqualTo(expected.getBase());
        assertThat(result.getDate()).isEqualTo(expected.getDate());
        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(rateRepository, times(1)).save(any(RateEntity.class));

        // Verify restTemplate.exchange was called
        verify(restTemplate, times(1)).exchange(
                anyString(),
                eq(HttpMethod.GET),
                any(HttpEntity.class),
                eq(RateResponse.class)
        );
    }

}
Scrannel answered 16/6, 2023 at 22:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.