You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java Eclipse中JUnit测试when...thenReturn失效致空指针问题

JUnit测试中@InjectMocks注入KeycloakService失败导致空指针异常

在JUnit测试的setUp方法中执行when(kc.getToken()).thenReturn(token);后,调用AasServiceImpl的update方法时触发空指针错误:

java.lang.NullPointerException: Cannot invoke "services.KeycloakService.getToken()" because "this.kc" is null

使用的依赖

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mockito:mockito-core:5.14.2'
testImplementation 'org.mockito:mockito-junit-jupiter:5.14.2'

已尝试的操作

  • 执行testMockInjection测试,所有断言全部通过;
  • 执行testUpdate测试时,代码在AasServiceImpl的String zaToken = kc.getToken();行抛出空指针;
  • setUp方法中的日志正常打印,确认该方法已执行;
  • 尝试用Spy替代Mock,问题未解决;
  • 已确认导入正确注解,使用@ExtendWith(MockitoExtension.class)、@BeforeEach、@InjectMocks,也试过MockitoAnnotations.initMocks(this),错误依旧。

服务实现代码

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
...
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
...
import services.AasService;
import services.KeycloakService;
 
@Service
public class AasServiceImpl implements AasService{
    private final RestTemplate restTemplate;
    
    @Autowired
    private KeycloakService kc;

    @Value("${ass.server}")
    private String aasUri;
    @Value("${ass.id}")
    private String aasIdShort;
    
    public AasServiceImpl (RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
    
    @Override
    public void update(String applicationUri, String nodeId, JSONObject value) {
        System.out.println(applicationUri);
        System.out.println(nodeId);
        System.out.println(value);

        String submodelElementValueId = applicationUri.replace(":", "_") + nodeId.replace("i=", "");
        
        System.out.println(submodelElementValueId);
        
        if (kc instanceof KeycloakServiceImpl) {
            System.out.println("This is original service.");
        } else {
            System.out.println("This is  not the original service.");
// 此处打印的是这个分支
        }
// 报错行
        String zaToken = kc.getToken();
        
// 该行日志未打印
        System.out.println(zaToken);
        try {
            updateValue("DefectDetectionSkill", "Inputs", submodelElementValueId, value, zaToken);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
    }
}

测试代码

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
...
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
...
import org.junit.jupiter.api.BeforeEach;
import services.KeycloakService;  // 这是一个接口

@ExtendWith(MockitoExtension.class)
public class AasServiceImplTest {

    @Mock
    private KeycloakService kc;
    
    @Mock
    private RestTemplate restTemplate;
        
    @InjectMocks
    private AasServiceImpl aasService;

    @BeforeEach
    void setUp() {
       
        String token = "mockToken";
        when(kc.getToken()).thenReturn(token);

        ResponseEntity<String> response= new ResponseEntity<>("", HttpStatus.OK);
        when(restTemplate.exchange(anyString(), any(), any(), eq(String.class))).thenReturn(response);
 
        System.out.println("set up runs");
        // 该日志正常打印
    }

    /*
    @Test
    void testMockInjection() {
        
        assertNotNull(kc, "KeycloakService mock is not injected!");
        assertNotNull(restTemplate, "RestTemplate mock is not injected!");
        assertNotNull(aasService, "AasServiceImpl is not initialized!");
    }
    
    */
    @Test
    void testUpdate() {
        
        // 准备测试数据
        String applicationUri = "http://mock_uri";
        String nodeId="i=ioanna";
        JSONObject value = new JSONObject();
        value.put("Value", 1234);

        // 执行测试方法
        aasService.update(applicationUri,  nodeId , value);
        System.out.println("Value: " + value.get("Value"));

        // 断言
        assertNotNull(kc, "KeycloakService mock is not injected!");

        // verify(kc, times(1)).getToken();
        // verify(restTemplate, atLeastOnce()).exchange(anyString(), any(), any(), eq(String.class));
    }
}

问题原因与解决方案

原因

Mockito的@InjectMocks优先使用构造函数注入,但AasServiceImpl的构造函数仅接收RestTemplate,未包含KeycloakService,导致字段kc无法被自动注入。

解决方案

方法1:修改服务类使用全构造函数注入(推荐)

将所有依赖通过构造函数注入,这也是Spring官方推荐的依赖注入方式:

@Service
public class AasServiceImpl implements AasService{
    private final RestTemplate restTemplate;
    private final KeycloakService kc;

    @Value("${ass.server}")
    private String aasUri;
    @Value("${ass.id}")
    private String aasIdShort;
    
    // 构造函数注入所有依赖
    public AasServiceImpl (RestTemplate restTemplate, KeycloakService kc) {
        this.restTemplate = restTemplate;
        this.kc = kc;
    }
    
    // 其余代码不变
}

修改后,@InjectMocks会自动将@Mock注解的kc和restTemplate通过构造函数注入到aasService中。

方法2:测试类中手动注入字段

如果无法修改服务类代码,可通过反射工具手动注入kc字段:

@BeforeEach
void setUp() {
    String token = "mockToken";
    when(kc.getToken()).thenReturn(token);

    ResponseEntity<String> response= new ResponseEntity<>("", HttpStatus.OK);
    when(restTemplate.exchange(anyString(), any(), any(), eq(String.class))).thenReturn(response);
 
    // 手动注入kc字段
    ReflectionTestUtils.setField(aasService, "kc", kc);
    
    System.out.println("set up runs");
}

需导入org.springframework.test.util.ReflectionTestUtils,该类已包含在spring-boot-starter-test依赖中。

内容的提问来源于stack exchange,提问作者joan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 01:53:16