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

JUnit5测试时注入@Value Resource(JSON文件)遇空指针问题求助

问题:Spring静态Resource注入引发JUnit测试空指针异常

我在服务类中通过@Value注入静态Resource对象读取classpath下的multipleHearingLocations.json文件,代码如下:

@Value("classpath:multipleHearingLocations.json")
public static Resource resource;

public void someMethod(){
   ConcurrentHashMap<String,List<String>> multipleHearingLocations = new ObjectMapper().readValue(
       Files.newInputStream(Paths.get(resource.getURI())), 
       new TypeReference<ConcurrentHashMap<String, List<String>>>(){}
   );
}

JSON文件存放在main/resources目录下。JUnit5测试时,我在测试类中尝试注入该Resource,但运行测试时服务类抛出空指针异常,提示resource为null。

测试方法代码:

@Mock
public static Resource resource;

@Test
void getMultipleHearingLocations_shouldReturnCorrespondingMultipleEpimsIdForVenue() throws IOException {
    SscsCaseData caseData = SscsCaseData.builder()
        .appeal(Appeal.builder()
                    .hearingOptions(HearingOptions.builder().build())
                    .build())
        .processingVenue(PROCESSING_VENUE_1)
        .build();

    given(venueService.getEpimsIdForVenue(caseData.getProcessingVenue())).willReturn(Optional.of("443014"));
    given(referenceDataServiceHolder.getVenueService()).willReturn(venueService);
    given(resource.getURI()).willReturn(new ClassPathResource("multipleHearingLocationsTest.json").getURI());
    List<HearingLocation> result = HearingsDetailsMapping.getHearingLocations(
        caseData,
        referenceDataServiceHolder
    );
}

尝试过以下测试配置仍未解决:

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = HearingsDetailsMapping.class)
@TestPropertySource(locations = "classpath:multipleHearingLocationsTest.json")

错误原因分析

  1. 静态字段无法被Spring直接注入:Spring的@Value注解仅支持实例字段注入,不会处理静态字段,因此服务类中的static Resource resource始终为null。
  2. Mock静态字段方式无效:Mockito的@Mock注解无法直接初始化静态字段,且即使初始化了Mock对象,也无法替换服务类中属于类级别的静态字段。
  3. @TestPropertySource使用错误:该注解用于加载属性文件(.properties/.yml),不能加载JSON文件,配置完全无效。

解决方案

方案1:修改服务类,遵循Spring依赖注入最佳实践

将静态字段改为实例字段,通过构造方法注入,并将类交给Spring管理(添加@Component):

@Component
public class HearingsDetailsMapping {

    private final Resource hearingLocationsResource;
    private final ObjectMapper objectMapper;

    // 构造方法注入(推荐方式)
    public HearingsDetailsMapping(
        @Value("classpath:multipleHearingLocations.json") Resource hearingLocationsResource,
        ObjectMapper objectMapper
    ) {
        this.hearingLocationsResource = hearingLocationsResource;
        this.objectMapper = objectMapper;
    }

    public void someMethod() throws IOException {
        ConcurrentHashMap<String, List<String>> multipleHearingLocations = objectMapper.readValue(
            hearingLocationsResource.getInputStream(), 
            new TypeReference<ConcurrentHashMap<String, List<String>>>(){}
        );
        // 业务逻辑实现
    }

    // 业务方法getHearingLocations...
}

对应的测试类写法:

@SpringBootTest
class HearingsDetailsMappingTest {

    @Autowired
    private HearingsDetailsMapping hearingsDetailsMapping;

    @MockBean
    private VenueService venueService;

    @MockBean
    private ReferenceDataServiceHolder referenceDataServiceHolder;

    @Test
    void getMultipleHearingLocations_shouldReturnCorrespondingMultipleEpimsIdForVenue() throws IOException {
        SscsCaseData caseData = SscsCaseData.builder()
            .appeal(Appeal.builder()
                .hearingOptions(HearingOptions.builder().build())
                .build())
            .processingVenue(PROCESSING_VENUE_1)
            .build();

        given(venueService.getEpimsIdForVenue(caseData.getProcessingVenue())).willReturn(Optional.of("443014"));
        given(referenceDataServiceHolder.getVenueService()).willReturn(venueService);

        List<HearingLocation> result = hearingsDetailsMapping.getHearingLocations(caseData, referenceDataServiceHolder);

        // 断言逻辑
    }
}

方案2:保留静态字段,通过反射强制赋值(不推荐,仅用于无法修改服务类的场景)

在测试方法开头通过反射修改服务类的静态字段值:

@Test
void getMultipleHearingLocations_shouldReturnCorrespondingMultipleEpimsIdForVenue() throws IOException, NoSuchFieldException, IllegalAccessException {
    // 手动设置静态resource字段的值
    Resource testResource = new ClassPathResource("multipleHearingLocationsTest.json");
    Field resourceField = HearingsDetailsMapping.class.getDeclaredField("resource");
    resourceField.setAccessible(true);
    resourceField.set(null, testResource);

    // 原有测试逻辑
    SscsCaseData caseData = SscsCaseData.builder()
        .appeal(Appeal.builder()
                    .hearingOptions(HearingOptions.builder().build())
                    .build())
        .processingVenue(PROCESSING_VENUE_1)
        .build();

    given(venueService.getEpimsIdForVenue(caseData.getProcessingVenue())).willReturn(Optional.of("443014"));
    given(referenceDataServiceHolder.getVenueService()).willReturn(venueService);

    List<HearingLocation> result = HearingsDetailsMapping.getHearingLocations(caseData, referenceDataServiceHolder);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 04:45:44