单元测试问题:RestTemplate Mock始终返回Null
问题
我在为实现BankId认证功能的BankIdClient类编写单元测试时遇到了问题:测试中customRestTemplate.postForEntity(...)返回的authResponse始终为null,Mockito的模拟配置没有生效,已经排查3小时仍未解决。
测试代码如下:
package se.kt.client; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.client.RestTemplate; import org.springframework.http.HttpEntity; import se.kt.common.vo.PublicApplicationForm; import se.kt.models.BankIdAuthRequest; import se.kt.models.BankIdAuthResponse; import se.kt.models.BankIdCollectResponse; import static org.mockito.ArgumentMatchers.any; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @RunWith(MockitoJUnitRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class BankIdClientTest { private final RestTemplate restTemplate = Mockito.mock(RestTemplate.class); @InjectMocks private BankIdClient bankIdClient; @BeforeEach public void setUp() { } @Test public void testBankIdAuthentication_success() throws InterruptedException { PublicApplicationForm form = new PublicApplicationForm(); form.setSsn("123456-7890"); form.setIp_address("123.123.123.123"); BankIdAuthRequest authRequest = bankIdClient.authRequestFromApplicationForm(form, "123"); BankIdAuthResponse authResponse = new BankIdAuthResponse(); authResponse.setOrderRef("123456"); BankIdCollectResponse collectResponse = new BankIdCollectResponse(); collectResponse.setStatus("completed"); Mockito.when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), any())) .thenReturn(ResponseEntity.ok(authResponse)); Mockito.when(restTemplate.getForEntity(anyString(), any())) .thenReturn(ResponseEntity.ok(collectResponse)); assertTrue(bankIdClient.bankIdAuthentication(authRequest)); } }
待测试的BankIdClient类代码:
package se.kt.client; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import se.kt.common.domain.AbstractApplicationForm; import se.kt.common.vo.PublicApplicationForm; import se.kt.models.BankIdAuthRequest; import se.kt.models.BankIdAuthResponse; import se.kt.models.BankIdCollectResponse; import javax.validation.constraints.AssertFalse; import javax.validation.constraints.AssertTrue; import java.util.Objects; @Component public class BankIdClient { private static final Logger log = LoggerFactory.getLogger(BankIdClient.class); private final RestTemplate customRestTemplate; private static final String CONTENT_TYPE = "Content-Type"; @Value("${BankId.AuthUrl}") private String bankIdAuthUrl; @Value("${BankId.CollectUrl}") private String bankIdCollectUrl; @Value("${BankId.SecretKey}") private String bankIdSecretKey; public BankIdClient(RestTemplate customRestTemplate) { this.customRestTemplate = customRestTemplate; } public BankIdAuthRequest authRequestFromApplicationForm(PublicApplicationForm form, String jobId) { BankIdAuthRequest bankIdAuthRequest = new BankIdAuthRequest(); bankIdAuthRequest.setPno(form.getSsn()); bankIdAuthRequest.setIpAddress(form.getIp_address()); bankIdAuthRequest.setRefID(jobId); bankIdAuthRequest.setSecretKey(bankIdSecretKey); bankIdAuthRequest.setAvsikt("Kt application"); return bankIdAuthRequest; } public boolean bankIdAuthentication(BankIdAuthRequest bankIdAuthRequest) throws InterruptedException { //Setup header and body for request. HttpHeaders headers = new HttpHeaders(); headers.add(CONTENT_TYPE, MediaType.APPLICATION_JSON.toString()); ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); try { String bankIdAuthFormJson = ow.writeValueAsString(bankIdAuthRequest); HttpEntity<String> httpEntity = new HttpEntity<>(bankIdAuthFormJson, headers); ResponseEntity<BankIdAuthResponse> authResponse = customRestTemplate.postForEntity(bankIdAuthUrl, httpEntity, BankIdAuthResponse.class); bankIdCollectUrl += Objects.requireNonNull(authResponse.getBody()).getOrderRef(); ResponseEntity<BankIdCollectResponse> collectResponse; do { collectResponse = customRestTemplate.getForEntity(bankIdCollectUrl, BankIdCollectResponse.class); Thread.sleep(1500); if (Objects.requireNonNull(collectResponse.getBody()).getStatus().equals("completed")) return true; if (Objects.requireNonNull(collectResponse.getBody()).getStatus().equals("failed")) return false; } while (Objects.requireNonNull(collectResponse.getBody()).getStatus().equals("progress")); } catch (JsonProcessingException e) { log.info(e.getMessage()); } catch (NullPointerException e) { log.info(e.toString()); log.info("BankId API not responding correctly. Check server connection"); } return false; } public void cancelBankIdAuthentication(@Value("${BankId.CancelUrl}") String bankIdCancelUrl) { customRestTemplate.postForEntity(bankIdCancelUrl, null, String.class); } }
问题分析与修复方案
1. Spring容器干扰Mockito注入
测试类同时使用了@SpringBootTest和Mockito的@InjectMocks,Spring会自动创建BankIdClient的实例,覆盖Mockito的注入逻辑,导致你手动创建的restTemplate Mock对象根本没被用到。
修复:移除@SpringBootTest和JUnit4的@RunWith注解,改用JUnit5的MockitoExtension,让Mockito完全控制对象生命周期:
@ExtendWith(MockitoExtension.class) class BankIdClientTest { // ... 其他代码 }
2. Mock对象未正确关联到目标类
测试中手动创建RestTemplate Mock对象,但没有通过@Mock注解标记,@InjectMocks无法识别并注入到BankIdClient的customRestTemplate字段。
修复:用@Mock注解创建Mock对象:
@Mock private RestTemplate restTemplate; @InjectMocks private BankIdClient bankIdClient;
3. @Value属性未初始化
BankIdClient中的bankIdAuthUrl、bankIdSecretKey等属性依赖配置文件,单元测试中没有加载配置,这些属性为null,会导致postForEntity的URL参数为null,Mockito匹配器无法匹配,进而返回null。
修复:用ReflectionTestUtils手动设置私有属性值:
// 在测试方法开头添加 ReflectionTestUtils.setField(bankIdClient, "bankIdAuthUrl", "https://mock-auth-url.com"); ReflectionTestUtils.setField(bankIdClient, "bankIdCollectUrl", "https://mock-collect-url.com/"); ReflectionTestUtils.setField(bankIdClient, "bankIdSecretKey", "mock-secret-key");
4. Mock匹配器不够精确
原测试中使用any()匹配返回类型,可能导致匹配模糊。建议使用精确的类型匹配:
when(restTemplate.postForEntity(anyString(), any(), eq(BankIdAuthResponse.class))) .thenReturn(ResponseEntity.ok(authResponse)); when(restTemplate.getForEntity(anyString(), eq(BankIdCollectResponse.class))) .thenReturn(ResponseEntity.ok(collectResponse));
修改后的完整测试代码
package se.kt.client; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.ResponseEntity; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.RestTemplate; import se.kt.common.vo.PublicApplicationForm; import se.kt.models.BankIdAuthRequest; import se.kt.models.BankIdAuthResponse; import se.kt.models.BankIdCollectResponse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class BankIdClientTest { @Mock private RestTemplate restTemplate; @InjectMocks private BankIdClient bankIdClient; @Test void testBankIdAuthentication_success() throws InterruptedException { // 初始化BankIdClient的配置属性 ReflectionTestUtils.setField(bankIdClient, "bankIdAuthUrl", "https://mock-auth-url.com"); ReflectionTestUtils.setField(bankIdClient, "bankIdCollectUrl", "https://mock-collect-url.com/"); ReflectionTestUtils.setField(bankIdClient, "bankIdSecretKey", "mock-secret-key"); PublicApplicationForm form = new PublicApplicationForm(); form.setSsn("123456-7890"); form.setIp_address("123.123.123.123"); BankIdAuthRequest authRequest = bankIdClient.authRequestFromApplicationForm(form, "123"); BankIdAuthResponse authResponse = new BankIdAuthResponse(); authResponse.setOrderRef("123456"); BankIdCollectResponse collectResponse = new BankIdCollectResponse(); collectResponse.setStatus("completed"); // 精确模拟post和get请求 when(restTemplate.postForEntity(anyString(), any(), eq(BankIdAuthResponse.class))) .thenReturn(ResponseEntity.ok(authResponse)); when(restTemplate.getForEntity(anyString(), eq(BankIdCollectResponse.class))) .thenReturn(ResponseEntity.ok(collectResponse)); assertTrue(bankIdClient.bankIdAuthentication(authRequest)); } }
内容的提问来源于stack exchange,提问作者Mr.Gomer

