引言
对于软件开发人员来说单元测试是一项必不可少的工作它既可以验证程序的有效性又可以在程序出现 BUG 的时候帮助开发人员快速的定位问题所在但是在写单元测试的过程中开发人员经常要访问类的一些非公有的成员变量或方法这给测试工作带来了很大的困扰本文总结了访问类的非公有成员变量或方法的四种途径以方便测试人员在需要访问类非公有成员变量或方法时进行选择
尽管有很多经验丰富的程序员认为不应该提倡访问类的私有成员变量或方法因为这样做违反了 Java 语言封装性的基本规则然而在实际测试中被测试的对象千奇百怪为了有效快速的进行单元测试有时我们不得不违反一些这样或那样的规则本文只讨论如何访问类的非公有成员变量或方法至于是否应该在开发测试中这样做则留给读者自己根据实际情况去判断和选择
方法一修改访问权限修饰符
先介绍最简单也是最直接的方法就是利用 Java 语言自身的特性达到访问非公有成员的目的说白了就是直接将 private 和 protected 关键字改为 public 或者直接删除我们建议直接删除因为在 Java 语言定义中缺省访问修饰符是包可见的这样做之后我们可以另建一个源码目录 —— test 目录(多数 IDE 支持这么做如 Eclipse 和 JBuilder)然后将测试类放到 test 目录相同包下从而达到访问待测类的成员变量和方法的目的此时在其它包的代码依然不能访问这些变量或方法在一定程度上保障了程序的封装性
下面的代码示例展示了这一方法
清单 原始待测类 A 代码
public class A { private String name = null; private void calculate() { }}
清单 针对单元测试修改后的待测类 A 的代码
public class A { String name = null; private void calculate() { }}
这种方法虽然看起来简单粗暴但经验告诉我们这个方法在测试过程中是非常有效的当然由于改变了源代码虽然只是包可见也已经破坏了对象的封装性对于多数对代码安全性要求严格的系统此方法并不可取
方法二利用安全管理器
安全性管理器与反射机制相结合也可以达到我们的目的Java 运行时依靠一种安全性管理器来检验调用代码对某一特定的访问而言是否有足够的权限具体来说安全性管理器是 javalangSecurityManager 类或扩展自该类的一个类且它在运行时检查某些应用程序操作的权限换句话说所有的对象访问在执行自身逻辑之前都必须委派给安全管理器当访问受到安全性管理器的控制应用程序就只能执行那些由相关安全策略特别准许的操作因此安全管理器一旦启动可以为代码提供足够的保护默认情况下安全性管理器是没有被设置的除非代码明确地安装一个默认的或定制的安全管理器否则运行时的访问控制检查并不起作用我们可以通过这一点在运行时避开 Java 的访问控制检查达到我们访问非公有成员变量或方法的目的为能访问我们需要的非公有成员我们还需要使用 Java 反射技术Java 反射是一种强大的工具它使我们可以在运行时装配代码而无需在对象之间进行源代码链接从而使代码更具灵活性在编译时Java 编译程序保证了私有成员的私有特性从而一个类的私有方法和私有成员变量不能被其他类静态引用然而通过 Java 反射机制使得我们可以在运行时查询以及访问变量和方法由于反射是动态的因此编译时的检查就不再起作用了
下面的代码演示了如何利用安全性管理器与反射机制访问私有变量
清单 利用反射机制访问类的成员变量
//获得指定变量的值
public static Object getValue(Object instance String fieldName)
throws IllegalAccessException NoSuchFieldException {
Field field = getField(instancegetClass() fieldName);
// 参数值为true禁用访问控制检查
fieldsetAccessible(true);
return fieldget(instance);
}
//该方法实现根据变量名获得该变量的值
public static Field getField(Class thisClass String fieldName)
throws NoSuchFieldException {
if (thisClass == null) {
throw new NoSuchFieldException(Error field !);
}
}
其中 getField(instancegetClass() fieldName) 通过反射机制获得对象属性如果存在安全管理器方法首先使用 this 和 MemberDECLARED 作为参数调用安全管理器的 checkMemberAccess 方法这里的 this 是 this 类或者成员被确定的父类 如果该类在包中那么方法还使用包名作为参数调用安全管理器的 checkPackageAccess 方法 每一次调用都可能导致 SecurityException当访问被拒绝时这两种调用方式都会产生 securityexception 异常
setAccessible(true) 方法通过指定参数值为 true 来禁用访问控制检查从而使得该变量可以被其他类调用我们可以在我们所写的类中扩展一个普通的基本类 javalangreflectAccessibleObject 类这个类定义了一种 setAccessible 方法使我们能够启动或关闭对这些类中其中一个类的实例的接入检测这种方法的问题在于如果使用了安全性管理器它将检测正在关闭接入检测的代码是否允许这样做如果未经允许安全性管理器抛出一个例外
除访问私有变量我们也可以通过这个方法访问私有方法
清单 利用反射机制访问类的成员方法
public static Method getMethod(Object instance String methodName Class[] classTypes)
throws NoSuchMethodException {
Method accessMethod = getMethod(instancegetClass() methodName classTypes);
//参数值为true禁用访问控制检查
accessMethodsetAccessible(true);
return accessMethod;
}
private static Method getMethod(Class thisClass String methodName Class[] classTypes)
throws NoSuchMethodException {
if (thisClass == null) {
throw new NoSuchMethodException(Error method !);
} try {
return thisClassgetDeclaredMethod(methodName classTypes);
} catch (NoSuchMethodException e) {
return getMethod(thisClassgetSuperclass() methodName classTypes);
}
}
获得私有方法的原理与获得私有变量的方法相同当我们得到了函数后需要对它进行调用这时我们需要通过 invoke() 方法来执行对该函数的调用代码示例如下
//调用含单个参数的方法
public static Object invokeMethod(Object instance String methodName Object arg)
throws NoSuchMethodException
IllegalAccessException InvocationTargetException {
Object[] args = new Object[];
args[] = arg;
return invokeMethod(instance methodName args);
}
//调用含多个参数的方法
public static Object invokeMethod(Object instance String methodName Object[] args)
throws NoSuchMethodException
IllegalAccessException InvocationTargetException {
Class[] classTypes = null;
if (args != null) {
classTypes = new Class[argslength];
for (int i = ; i < argslength; i++) {
if (args[i] != null) {
classTypes[i] = args[i]getClass();
}
}
}
return getMethod(instance methodName classTypes)invoke(instance args);
}
利用安全管理器及反射可以在不修改源码的基础上访问私有成员为测试带来了极大的方便尤其是在编译期间该方法可以顺利地通过编译但同时该方法也有一些缺点第一个是性能问题用于字段和方法接入时反射要远慢于直接代码第二个是权限问题有些涉及 Java 安全的程序代码并没有修改安全管理器的权限此时本方法失效
另一种方法
package test;
import javalangreflectField;
import modelDept;
public class TypeTest {
public static void main(String args[])
{
Dept d=new Dept();
dsetDeptNo();
dsetDName(v);
dsetLoc(mopish);
delete(d Deptclass);
}
public static void delete(Object obj Class<?> clazz)
{
try
{
Systemoutprintln(?+(obj instanceof Dept));
Systemoutprintln(clazzgetName());
Systemoutprintln(clazzgetDeclaredFields()length);
for(Field f: clazzgetDeclaredFields())
{
fsetAccessible(true);
Systemoutprintln(fgetName());
Systemoutprintln(+fget(obj));
}
}catch(Exception e)
{
eprintStackTrace();
}
}
}
package model;
public class Dept {
private long deptNo;
private String DName;
private String Loc;
public long getDeptNo() {
return deptNo;
}
public void setDeptNo(long deptNo) {
thisdeptNo = deptNo;
}
public String getDName() {
return DName;
}
public void setDName(String dName) {
DName = dName;
}
public String getLoc() {
return Loc;
}
public void setLoc(String loc) {
Loc = loc;
}
}