Java单元测试mock mkdirs()目录创建调用抛出异常问题排查
问题现象
运行对应单元测试时,代码调用mkdirs()方法抛出IllegalArgumentException,最终测试抛出外层包装的异常:
java.lang.RuntimeException: Cannot reconfigure node at com.javaeasily.demos.junit.MyClass.reconfigureNode(MyClass.java:22)
调试确认根因异常为:
java.lang.IllegalArgumentException: Could not create staging dir
关联代码
MyClass.java
package com.javaeasily.demos.junit; import java.util.ArrayList; public class MyClass { private final NodeHelper nodeHelper; private static final ArrayList<String> ACTIVE_SERVICES_POST_RECONFIGURE = new ArrayList<>(); // Only allow construction if number is greater than one MyClass() { ACTIVE_SERVICES_POST_RECONFIGURE.add("my-node-" + NodeUtils.getMyNode()); nodeHelper = new NodeHelper(); } public void reconfigureNode() { if (ACTIVE_SERVICES_POST_RECONFIGURE.isEmpty()) { return; } try { nodeHelper.createStagingDir(); } catch (Exception e) { throw new RuntimeException("Cannot reconfigure node"); } } }
NodeUtils.java
package com.javaeasily.demos.junit; import org.apache.commons.lang3.StringUtils; public class NodeUtils { private static final String HOSTNAME_PREFIX = "my-node-"; public static String hostnameToNode(String hostname) { if (!hostname.startsWith(HOSTNAME_PREFIX)) { throw new IllegalArgumentException(hostname + " is not recognized hostname"); } return StringUtils.removeStart(hostname, HOSTNAME_PREFIX); } public static String getHostname() { return System.getenv("HOSTNAME"); } public static String getMyNode() { return hostnameToNode(getHostname()); } }
NodeHelper.java
package com.javaeasily.demos.junit; import java.io.File; public class NodeHelper { private static final String STAGING_DIR = "/staging/"; public void createStagingDir() { File stagingDir = new File(STAGING_DIR); if (!stagingDir.exists() && !stagingDir.mkdirs()) { throw new IllegalArgumentException("Could not create staging dir"); } } }
MyClassTest.java
package com.javaeasily.demos.junit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) public class MyClassTest { private MyClass myclass; @BeforeEach public void SetUp() { try (MockedStatic<NodeUtils> nodeUtilsMockedStatic = Mockito.mockStatic(NodeUtils.class);) { nodeUtilsMockedStatic.when(NodeUtils::getMyNode).thenReturn("foo"); myclass = new MyClass(); } } @Test public void testReconfigureNode() { myclass.reconfigureNode(); } }
存在的问题
- 生产代码逻辑缺陷
- 目录路径配置不合理:
NodeHelper中硬编码staging目录为系统根路径下的/staging/,普通用户运行程序/测试时对根目录没有写权限,mkdirs()执行失败返回false,直接触发异常。 - 异常类型使用错误:创建目录失败属于IO操作异常,不属于参数非法场景,不应该抛出
IllegalArgumentException。 - 耦合度太高:
MyClass在构造方法中直接硬编码实例化NodeHelper,没有预留依赖注入入口,测试时无法直接替换依赖,导致单元测试会真实执行文件系统操作,违反单元测试隔离外部依赖的原则。
- 目录路径配置不合理:
- 测试代码写法缺陷
- 没有做依赖隔离:测试时使用真实的
NodeHelper实例执行文件创建操作,依赖本地文件系统权限,测试结果受运行环境影响,不稳定。 - 静态Mock作用域不规范:
MockedStatic实例放在try-with-resources块中,初始化完MyClass后静态Mock就会被关闭,如果后续测试用例需要依赖NodeUtils的Mock逻辑会直接失效。
- 没有做依赖隔离:测试时使用真实的
修复方案
- 生产代码调整
- 优化目录配置:不要硬编码无权限的根路径目录,可将目录路径改为可配置项,或者使用相对路径、临时目录方案。
- 修正异常类型:创建目录失败场景抛出
IOException或对应业务异常,不要误用参数异常。 - 增加依赖注入入口,解耦
MyClass和NodeHelper的硬编码依赖:
// 新增支持传入NodeHelper的构造方法 MyClass(NodeHelper nodeHelper) { ACTIVE_SERVICES_POST_RECONFIGURE.add("my-node-" + NodeUtils.getMyNode()); this.nodeHelper = nodeHelper; } // 保留无参构造供生产环境默认使用 MyClass() { this(new NodeHelper()); }
- 测试代码调整
- 单元测试中Mock
NodeHelper依赖,不真实执行文件系统操作,消除测试对本地环境权限的依赖。 - 如果需要测试真实的文件创建逻辑,使用JUnit 5提供的
@TempDir注解创建临时测试目录传入,避免权限问题。 - 按需调整静态Mock的作用域,如果测试全程需要Mock静态方法,不要提前关闭
MockedStatic实例。
- 单元测试中Mock
修复后的测试代码示例:
@ExtendWith(MockitoExtension.class) public class MyClassTest { private MyClass myclass; @Mock private NodeHelper nodeHelper; @BeforeEach public void SetUp() { try (MockedStatic<NodeUtils> nodeUtilsMockedStatic = Mockito.mockStatic(NodeUtils.class)) { nodeUtilsMockedStatic.when(NodeUtils::getMyNode).thenReturn("foo"); // 传入Mock的NodeHelper实例,隔离真实文件操作 myclass = new MyClass(nodeHelper); } } @Test public void testReconfigureNode() { myclass.reconfigureNode(); // 可增加校验确认目录创建逻辑被触发 Mockito.verify(nodeHelper).createStagingDir(); } }
内容的提问来源于stack exchange,提问作者Kundan Kumar
相关产品推荐
相关产品推荐

