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

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();
    }
}
存在的问题
  • 生产代码逻辑缺陷
    1. 目录路径配置不合理:NodeHelper中硬编码staging目录为系统根路径下的/staging/,普通用户运行程序/测试时对根目录没有写权限,mkdirs()执行失败返回false,直接触发异常。
    2. 异常类型使用错误:创建目录失败属于IO操作异常,不属于参数非法场景,不应该抛出IllegalArgumentException。
    3. 耦合度太高:MyClass在构造方法中直接硬编码实例化NodeHelper,没有预留依赖注入入口,测试时无法直接替换依赖,导致单元测试会真实执行文件系统操作,违反单元测试隔离外部依赖的原则。
  • 测试代码写法缺陷
    1. 没有做依赖隔离:测试时使用真实的NodeHelper实例执行文件创建操作,依赖本地文件系统权限,测试结果受运行环境影响,不稳定。
    2. 静态Mock作用域不规范:MockedStatic实例放在try-with-resources块中,初始化完MyClass后静态Mock就会被关闭,如果后续测试用例需要依赖NodeUtils的Mock逻辑会直接失效。
修复方案
  • 生产代码调整
    1. 优化目录配置:不要硬编码无权限的根路径目录,可将目录路径改为可配置项,或者使用相对路径、临时目录方案。
    2. 修正异常类型:创建目录失败场景抛出IOException或对应业务异常,不要误用参数异常。
    3. 增加依赖注入入口,解耦MyClass和NodeHelper的硬编码依赖:
// 新增支持传入NodeHelper的构造方法
MyClass(NodeHelper nodeHelper) {
    ACTIVE_SERVICES_POST_RECONFIGURE.add("my-node-" + NodeUtils.getMyNode());
    this.nodeHelper = nodeHelper;
}
// 保留无参构造供生产环境默认使用
MyClass() {
    this(new NodeHelper());
}
  • 测试代码调整
    1. 单元测试中MockNodeHelper依赖,不真实执行文件系统操作,消除测试对本地环境权限的依赖。
    2. 如果需要测试真实的文件创建逻辑,使用JUnit 5提供的@TempDir注解创建临时测试目录传入,避免权限问题。
    3. 按需调整静态Mock的作用域,如果测试全程需要Mock静态方法,不要提前关闭MockedStatic实例。

修复后的测试代码示例:

@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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 04:09:16