Spring Boot测试中无法读取application-test.properties的文件上传目录配置导致测试失败
你遇到的问题核心很明确:纯Mockito测试模式下,Spring的@Value注解不会被自动处理。你当前的测试只用到了MockitoAnnotations.openMocks,这种方式不会启动Spring上下文,所以@Value("${file.upload-dir}")根本没机会从配置文件里读取值,uploadDir始终是null,自然触发了方法里的空值校验异常。而直接写死路径时,uploadDir有了有效值,方法就能正常执行。
下面给你几个可行的解决方案,你可以根据测试场景选择:
方案一:用@SpringBootTest启动完整Spring上下文(适合需要Spring环境的测试)
如果你的测试需要依赖Spring的其他特性,或者想让测试更贴近真实运行环境,可以用@SpringBootTest启动完整Spring上下文,同时用@MockBean来Mock掉不需要真实实例的依赖(比如ItemRepository和Converter)。这样Spring会自动处理@Value的配置注入。
修改后的测试类代码:
@SpringBootTest // 如果你的测试配置文件是application-test.properties,可指定路径;不指定的话默认会读取src/test/resources下的配置 @TestPropertySource(locations = "classpath:application-test.properties") public class ItemServiceImplTest { @Value("${file.upload-dir}") private String uploadDir; // 用@MockBean替代@Mock,让Spring管理这些Mock实例 @MockBean private ItemRepository itemRepository; @MockBean private Converter converter; // 直接从Spring容器注入ItemServiceImpl实例 @Autowired private ItemServiceImpl itemService; @Test void testSaveTempFile() throws IOException { // Arrange MultipartFile mockFile = mock(MultipartFile.class); String originalFileName = "test_image.png"; byte[] fileContent = "Dummy Content".getBytes(); when(mockFile.getOriginalFilename()).thenReturn(originalFileName); when(mockFile.getBytes()).thenReturn(fileContent); // Act String savedFileName = itemService.saveTempFile(mockFile); // Assert assertTrue(savedFileName.startsWith("TEMP_")); assertTrue(savedFileName.endsWith(".png")); Path savedFilePath = Paths.get(uploadDir, savedFileName); assertTrue(Files.exists(savedFilePath)); assertArrayEquals(fileContent, Files.readAllBytes(savedFilePath)); // 测试后清理文件 Files.deleteIfExists(savedFilePath); } }
方案二:轻量级Spring测试(@ExtendWith+@TestPropertySource)
如果你不想启动完整的应用上下文,只想让Spring处理@Value注解,可以用SpringExtension配合@TestPropertySource,这样既能读取配置,又能保持测试的轻量性:
// JUnit 5中@ExtendWith(SpringExtension.class)可以省略,因为Spring相关注解已经默认集成 @ExtendWith(SpringExtension.class) @TestPropertySource(locations = "classpath:application-test.properties") public class ItemServiceImplTest { @Value("${file.upload-dir}") private String uploadDir; @Mock private ItemRepository itemRepository; @Mock private Converter converter; @InjectMocks private ItemServiceImpl itemService; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); // 此时@Value已经被Spring处理,uploadDir已经有配置值了 } // 测试逻辑和你原来的代码一致... @Test void testSaveTempFile() throws IOException { // 你的原有测试代码 } }
方案三:纯Mockito测试(手动设置uploadDir)
如果你想完全脱离Spring容器,保持纯单元测试的速度,可以手动给itemService的uploadDir字段赋值。这里有两种方式:
方式A:给ItemServiceImpl添加测试用的setter
在你的ItemServiceImpl里加一个仅用于测试的setter方法(可以用@VisibleForTesting注解标记,表明这是测试专用):
import com.google.common.annotations.VisibleForTesting; public class ItemServiceImpl implements ItemService { // ... 原有代码 ... @VisibleForTesting void setUploadDir(String uploadDir) { this.uploadDir = uploadDir; } }
然后在测试类的setUp中调用这个方法:
@BeforeEach void setUp() throws IOException { MockitoAnnotations.openMocks(this); // 可以用系统临时目录,或者自定义测试目录 String testUploadDir = System.getProperty("java.io.tmpdir") + "/test-uploads"; // 确保目录存在 Files.createDirectories(Paths.get(testUploadDir)); // 手动设置上传目录 itemService.setUploadDir(testUploadDir); }
方式B:用反射设置私有字段
如果你不想修改业务代码,可以用反射直接给私有字段赋值:
@BeforeEach void setUp() throws NoSuchFieldException, IllegalAccessException, IOException { MockitoAnnotations.openMocks(this); String testUploadDir = "/tmp/test-uploads"; Files.createDirectories(Paths.get(testUploadDir)); // 用反射获取uploadDir字段并赋值 Field uploadDirField = ItemServiceImpl.class.getDeclaredField("uploadDir"); uploadDirField.setAccessible(true); uploadDirField.set(itemService, testUploadDir); }
最后注意事项
- 确保你的
application-test.properties放在src/test/resources目录下,并且配置正确:file.upload-dir=/tmp/test-uploads - 测试用的上传目录最好用临时目录,避免污染本地环境,测试完成后记得清理文件;
- 如果用JUnit 5,大部分Spring相关的扩展注解已经默认集成,不需要手动加
@ExtendWith。
备注:内容来源于stack exchange,提问作者Rockio

