Spring Boot测试类中getLogo()多测试同跑失败、单独运行通过的问题
问题根因
你碰到的问题本质是测试用例之间没有做状态隔离,存在隐式的执行依赖:
- JUnit4 默认按测试方法名称的字典序执行,顺序为
deleteLogo()→getLogo()→uploadLogo()。没有任何前置状态准备的情况下,先执行的deleteLogo()会把环境里提前存在的logo文件、数据库配置记录清空。等轮到执行getLogo()时,因为之前的清理操作,已经找不到对应的logo文件了。 - 生产代码里的
getLogo方法有逻辑漏洞:getResourceAsStream()在找不到指定路径的资源时,不会抛出IOException,而是直接返回null。你把这个null直接传给IOUtils.toByteArray()时,就会抛出NullPointerException——这个异常属于运行时异常,根本触发不了原来写的catch块(catch仅捕获IOException),会直接抛到Web层形成你看到的嵌套异常,并不是你之前以为的走进了catch块。 - 单独运行
getLogo()测试时,不会触发前面的deleteLogo()的清理操作,类路径下提前放好的测试文件、数据库初始化的配置记录都还在,所以能正常运行通过。
调整步骤
1. 先修复生产代码的空指针隐患
把ConfigPropertiesService的getLogo方法加上非空判断,同时补全资源自动关闭逻辑:
public byte[] getLogo() { ConfigPropertiesEntity configPropertiesEntity = configPropertiesRepository.findByName("com.example.logoName"); if (configPropertiesEntity == null) { return null; } String logoRelativePath = "/static/photos/" + configPropertiesEntity.getValue(); // 用try-with-resources自动关闭输入流 try (InputStream in = getClass().getResourceAsStream(logoRelativePath)) { // 资源不存在时直接返回null,避免后续NPE if (in == null) { return null; } return IOUtils.toByteArray(in); } catch (IOException e) { e.printStackTrace(); } return null; }
2. 给测试类加状态隔离,避免用例互相影响
- 加
@Transactional注解,让测试过程中的数据库操作自动回滚,不会残留数据影响其他用例。 - 写统一的
@Before前置清理逻辑,每个测试方法执行前,清空数据库里的logo相关记录、删掉测试目录下的残留文件,把环境重置到已知的初始状态。 - 给每个测试方法准备自己需要的前置数据,不要依赖环境残留的状态。
调整后的测试类参考:
@WebAppConfiguration @EnableTransactionManagement @EnableJpaRepositories(basePackages = "com.example") @RunWith(SpringRunner.class) @SpringBootTest(classes = {ReceiptApplication.class}) @Transactional // 数据库操作自动回滚 public class PropertiesServiceTest { @Autowired private ConfigPropertiesService configPropertiesService; @Autowired private ConfigPropertiesRepository configPropertiesRepository; @Autowired private WebApplicationContext webApplicationContext; private MockMvc mockMvc; @Before public void setup() throws Exception { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build(); // 前置清理:删掉数据库里的logo配置 configPropertiesRepository.deleteByName("com.example.logoName"); // 前置清理:删掉照片目录下的残留测试文件 File photosDir = new File("target/classes/static/photos"); if (photosDir.exists() && photosDir.listFiles() != null) { for (File file : photosDir.listFiles()) { file.delete(); } } } @Test public void uploadLogo() throws Exception { MockMultipartFile file = new MockMultipartFile("logo", "originalMock.jpg", null, "bar".getBytes()); mockMvc.perform(multipart("/settings/uploadLogo").file(file)) .andExpect(status().isOk()); } @Test public void getLogo() throws Exception { // 自己准备前置数据:插入logo配置、生成测试文件 ConfigPropertiesEntity testConfig = new ConfigPropertiesEntity(); testConfig.setName("com.example.logoName"); testConfig.setValue("test-logo.jpg"); configPropertiesRepository.save(testConfig); // 生成对应测试文件 File testFile = new File("target/classes/static/photos/test-logo.jpg"); testFile.getParentFile().mkdirs(); FileUtils.writeByteArrayToFile(testFile, "test-content".getBytes()); mockMvc.perform(get("/settings/getLogo")) .andExpect(status().isOk()); } @Test public void deleteLogo() throws Exception { // 先准备前置数据:上传一个logo,再执行删除测试 MockMultipartFile file = new MockMultipartFile("logo", "delete-test.jpg", null, "bar".getBytes()); mockMvc.perform(multipart("/settings/uploadLogo").file(file)); // 执行删除接口 mockMvc.perform(delete("/settings/deleteLogo")) .andExpect(status().isOk()); } }
3. 注意事项
不要依赖默认的测试执行顺序来做测试,一旦顺序变动就很容易出现类似的环境残留问题。如果确实需要固定执行顺序,可以用JUnit的@FixMethodOrder(MethodSorters.JVM)注解指定,但优先推荐做每个用例的独立状态准备,从根源上避免用例耦合。
内容的提问来源于stack exchange,提问作者Greta
相关产品推荐
相关产品推荐

