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")
错误原因分析
- 静态字段无法被Spring直接注入:Spring的
@Value注解仅支持实例字段注入,不会处理静态字段,因此服务类中的static Resource resource始终为null。 - Mock静态字段方式无效:Mockito的
@Mock注解无法直接初始化静态字段,且即使初始化了Mock对象,也无法替换服务类中属于类级别的静态字段。 - @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
相关产品推荐
相关产品推荐

