其实认真写测试开始,主要面向的是 Python 这样没有访问修饰符的语言,并且考 虑到动态自由的特性,所以测试内部方法甚至动态的进行内容替换都是相对简单的 事情。以至于…笔者一直没留意到还有访问修饰符这样基础的问题会挡在测试面前。

从设计上来说,的确私有方法的目的是在类内部使用,外部不必知道细节。但既然写 自动测试是从 白盒测试 角度看待问题,那就表示需要看到内部,所以笔者倾向于 对测试内部方法也进行针对性测试;尤其是当逻辑复杂到一定规模时一定会多拆几个 方法时。

如何在 Java 中测试私有方法

不用 private 等访问修饰符

是的,看似很愚蠢但是…确实也是一种办法,但是在生产项目中这种办法是不实际 的,违背了封装原则。但如果没有掌握其他办法前,也不失为一种 临时方案

增加对应的 public 方法暴露测试接口

可以给类添加对应的 public 方法,一对一的暴露私有方法。测试的时候调用暴露 给测试的接口。优点看上去比较清晰,不过总体来说,带来的缺点更严重一些,私有 方法调整的时候还需要调整对应的 public 接口,并且多了一层额外的维护负担。

其他类似的套路还有:将过多的私有方法封装到单独的协助类里,这样就必须暴露公 用访问接口,这里不再赘述。

使用反射机制测试私有方法

反射机制很强大,可以拿到运行时的绝大多数信息,可以借此绕开访问权限的检查。

// src/java/main/MyExample.java
public class MyExample {
  private String internalThing() {
    return "some secret";
  }
}

// src/test/java/TestMyExample.java
public class TestMyExample {

  @Test

  public void testInternal()
    throws NoSuchMethodException,
      InvocationTargetException, IllegalAccessException{

    // 创建一个实例,用于调用
    MyExample inst = new MyExample();

    // 反射机制获取方法
    Class<MyExample> clazz = MyExample.class;
    // 注意这里的用法,翻一下 reflect 的文档
    Method method = clazz.getDeclaredMethod("internalThing");
    // 修改访问控制
    method.setAccessible(true);

    // 调用,同样注意参数用法
    Object rv = method.invoke(inst);

    // 用完记得改回去
    method.setAccessible(false);
  }
}

在测试时,用反射可以巧妙地绕开访问控制检查;既然测试代码与被测试方法本来 就是耦合的,并没有增加额外的维护负担;如果认为精确到参数耦合大的话,一个类 的内部使用面对的问题是一样的,得约定好,尽量不要经常大的改动。

getDeclaredMethod 限制

2019.04.03 补充,这个问题发生在笔者将需要测试的方法移到父级类后出现,针对子类测试时 发现抛异常。

特别需要注意一下该方法的使用情景,该方法只能获取 类本身 的方法,即如果方法是从 父级继承过来,会报 NoSuchMethodException 异常。例如以下例子:

class MyAbstract {
  private String internalThing() {
    return "some secret";
  }
}

class MyExample extends MyAbstract {

}

// src/test/java/TestMyExample.java
public class TestMyExample {

  @Test

  public void testInternal()
    throws NoSuchMethodException,
      InvocationTargetException, IllegalAccessException{

    // 创建一个实例,用于调用
    MyExample inst = new MyExample();

    // 反射机制获取方法
    Class<MyExample> clazz = MyExample.class;
    // 注意这里的用法,翻一下 reflect 的文档
    // 这里必须知道方法在类之间的关系,获取父级类后再反射方法
    Method method = clazz.getSuperClass().getDeclaredMethod("internalThing");
    // 修改访问控制
    method.setAccessible(true);

    // 调用,同样注意参数用法
    Object rv = method.invoke(inst);

    // 用完记得改回去
    method.setAccessible(false);
  }
}

getMethod 方法为何不能用,是因为后者仅返回 public 成员方法,与需求不符。

文档上专门提到了

See The Java™ Language Specification: 8.2 Class Members, 8.4 Method Declarations

(顺带一则) 如何在 JUnit 下运行指定测试类与测试方法

这个需求很明确,本地开发的时候针对个别测试用例编码过程中总会磕磕碰碰,需要 不断调试反复运行。如果每次都必须测试整个项目,想想那编译,启动,运行测试的 时间消耗,也是极其不科学的。如果有100个测试,而开发时只针对1个进行修改,那 每次都全量测试,时间、运算量成本一般会有超过9成浪费再不关注的点上。

很幸运 maven surefire 插件自带这样的功能

$ mvn test -Dtest='TestClass#testMethod'

甚至还有匹配功能,更多可以参考文档1

如果没有使用 maven ,而是其他构建工具,可以参考一下对应的工具是否有插件。

最后是 JUnit 本身的大招2,需要自己加一段 Java 代码来做,实际工程中 使用的时候,一般还是尽量考虑给构建工具加入插件或者流程,更便于使用与扩展。

问题拓展

  • Java 里是如何实现类的成员访问控制的?

参考

\_\_END\_\_