测试 Java 类的非公有成员变量和方法

本文涉及的产品
访问控制,不限时长
简介:
对 于软件开发人员来说,单元测试是一项必不可少的工作。它既可以验证程序的有效性,又可以在程序出现 BUG 的时候,帮助开发人员快速的定位问题所在。但是,在写单元测试的过程中,开发人员经常要访问类的一些非公有的成员变量或方法,这给测试工作带来了很大的困 扰。本文总结了访问类的非公有成员变量或方法的四种途径,以方便测试人员在需要访问类非公有成员变量或方法时进行选择。
尽 管有很多经验丰富的程序员认为不应该提倡访问类的私有成员变量或方法,因为这样做违反了 Java 语言封装性的基本规则。然而,在实际测试中被测试的对象千奇百怪,为了有效快速的进行单元测试,有时我们不得不违反一些这样或那样的规则。本文只讨论如何 访问类的非公有成员变量或方法,至于是否应该在开发测试中这样做,则留给读者自己根据实际情况去判断和选择。





先 介绍最简单也是最直接的方法,就是利用 Java 语言自身的特性,达到访问非公有成员的目的。说白了就是直接将 private 和 protected 关键字改为 public 或者直接删除。我们建议直接删除,因为在 Java 语言定义中,缺省访问修饰符是包可见的。这样做之后,我们可以另建一个源码目录 —— test 目录(多数 IDE 支持这么做,如 Eclipse 和 JBuilder),然后将测试类放到 test 目录相同包下,从而达到访问待测类的成员变量和方法的目的。此时,在其它包的代码依然不能访问这些变量或方法,在一定程度上保障了程序的封装性。
下面的代码示例展示了这一方法。

清单 1. 原始待测类 A 代码
                
public class A {
private String name = null;
private void calculate() {
}
}


清单 2. 针对单元测试修改后的待测类 A 的代码
                
public class A {
String name = null;
private void calculate() {
}
}

这种方法虽然看起来简单粗暴,但经验告诉我们这个方法在测试过程中是非常有效的。当然,由于改变了源代码,虽然只是包可见,也已经破坏了对象的封装性,对于多数对代码安全性要求严格的系统此方法并不可取。





安 全性管理器与反射机制相结合,也可以达到我们的目的。Java 运行时依靠一种安全性管理器来检验调用代码对某一特定的访问而言是否有足够的权限。具体来说,安全性管理器是 java.lang.SecurityManager 类或扩展自该类的一个类,且它在运行时检查某些应用程序操作的权限。换句话说,所有的对象访问在执行自身逻辑之前都必须委派给安全管理器,当访问受到安全 性管理器的控制,应用程序就只能执行那些由相关安全策略特别准许的操作。因此安全管理器一旦启动可以为代码提供足够的保护。默认情况下,安全性管理器是没 有被设置的,除非代码明确地安装一个默认的或定制的安全管理器,否则运行时的访问控制检查并不起作用。我们可以通过这一点在运行时避开 Java 的访问控制检查,达到我们访问非公有成员变量或方法的目的。为能访问我们需要的非公有成员,我们还需要使用 Java 反射技术。Java 反射是一种强大的工具,它使我们可以在运行时装配代码,而无需在对象之间进行源代码链接,从而使代码更具灵活性。在编译时,Java 编译程序保证了私有成员的私有特性,从而一个类的私有方法和私有成员变量不能被其他类静态引用。然而,通过 Java 反射机制使得我们可以在运行时查询以及访问变量和方法。由于反射是动态的,因此编译时的检查就不再起作用了。
下面的代码演示了如何利用安全性管理器与反射机制访问私有变量。

清单 3. 利用反射机制访问类的成员变量
                
//获得指定变量的值
public static Object getValue(Object instance, String fieldName)
throws IllegalAccessException, NoSuchFieldException {

Field field = getField(instance.getClass(), fieldName);
// 参数值为true,禁用访问控制检查
field.setAccessible(true);
return field.get(instance);
}

//该方法实现根据变量名获得该变量的值
public static Field getField(Class thisClass, String fieldName)
throws NoSuchFieldException {

if (thisClass == null) {
throw new NoSuchFieldException("Error field !");
}
}

其中 getField(instance.getClass(), fieldName) 通过反射机制获得对象属性,如果存在安全管理器,方法首先使用 this 和 Member.DECLARED 作为参数调用安全管理器的 checkMemberAccess 方法,这里的 this 是 this 类或者成员被确定的父类。 如果该类在包中,那么方法还使用包名作为参数调用安全管理器的 checkPackageAccess 方法。 每一次调用都可能导致 SecurityException。当访问被拒绝时,这两种调用方式都会产生 securityexception 异常 。
setAccessible(true) 方法通过指定参数值为 true 来禁用访问控制检查,从而使得该变量可以被其他类调用。我们可以在我们所写的类中,扩展一个普通的基本类 java.lang.reflect.AccessibleObject 类。这个类定义了一种 setAccessible 方法,使我们能够启动或关闭对这些类中其中一个类的实例的接入检测。这种方法的问题在于如果使用了安全性管理器,它将检测正在关闭接入检测的代码是否允许 这样做。如果未经允许,安全性管理器抛出一个例外。
除访问私有变量,我们也可以通过这个方法访问私有方法。

清单 4. 利用反射机制访问类的成员方法
                
public static Method getMethod(Object instance, String methodName, Class[] classTypes)
throws NoSuchMethodException {

Method accessMethod = getMethod(instance.getClass(), methodName, classTypes);
//参数值为true,禁用访问控制检查
accessMethod.setAccessible(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 thisClass.getDeclaredMethod(methodName, classTypes);
} catch (NoSuchMethodException e) {
return getMethod(thisClass.getSuperclass(), methodName, classTypes);

}
}

获得私有方法的原理与获得私有变量的方法相同。当我们得到了函数后,需要对它进行调用,这时我们需要通过 invoke() 方法来执行对该函数的调用,代码示例如下:
//调用含单个参数的方法
public static Object invokeMethod(Object instance, String methodName, Object arg)
throws NoSuchMethodException,
IllegalAccessException, InvocationTargetException {

Object[] args = new Object[1];
args[0] = 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[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] != null) {
classTypes[i] = args[i].getClass();
}
}
}
return getMethod(instance, methodName, classTypes).invoke(instance, args);
}

利用安全管理器及反射,可以在不修改源码 的基础上访问私有成员,为测试带来了极大的方便。尤其是在编译期间,该方法可以顺利地通过编译。但同时该方法也有一些缺点。第一个是性能问题,用于字段和 方法接入时反射要远慢于直接代码。第二个是权限问题,有些涉及 Java 安全的程序代码并没有修改安全管理器的权限,此时本方法失效。




在 单元测试的过程中模仿对象被广泛使用。它从测试中分离了外部的不需要的因素,并且帮助开发人员专注于被测试的功能。模仿对象(Mock object)的核心是构造一个伪类,在测试中通常用这个构造的伪类替换原来的需要访问相关环境(如应用服务器,数据库等)的需要测试的待测类,这样单元 测试便可以运行在本地环境下(这也是对单元测试的基本要求之一,不依赖于任何特定的环境),并可以正确的执行。此外, 由于 Java 语言不能多继承的特性,使得该方法也可以被用来作为非公有成员变量及方法的访问方法(测试类不能同时继承 TestCase 和待测类),利用该方法,在模仿对象中改变类成员的访问控制权限,从而达到访问非公有类变量及方法的目的。
下面的代码示例演示了模仿对象方法。
本方法的应用场景在单元测试中非常常见,即在待测试的公有方法中,有一些受限制的成员变量是由其它私有方法来初始化的,在测试该方法的时候,需要给这个变量置初值才能完成测试。

清单 5. 待测类 A
                
public class A {
protected String s = null;
public A() {

}
private void method() {
s = "word";
System.out.println("this is mock test");
}
public void makeWord() {
String prefix = s;
System.out.println("prefix is:" + prefix);
}
}

在待测类 A 中,增加工厂方法。

清单 6. 包含工厂方法的待测类 A
                
// 增加工厂方法的类 A
public class A {
protected String s = null;

public A getA() {
return new A();
}

private void method() {
s = "word";
System.out.println("this is mock test");
}

public void makeWord() {
String prefix = s;
System.out.println("prefix is:" + prefix);
}
}
//伪类,在运行时替换类 A
public class MockA extends A{

public String s = null;

public MockA(){

}
}
//测试类
public class TestA extends TestCase{

public void setup(){

}
public void teardown(){

}
public void makeWordTest(){
A a = new MockA();
a.s = "test";
a.makeWord();
}
}

此方法中有几个值得注意的地方,首先是将 创建代码抽取到工厂方法中,在测试子类中覆盖该工厂方法,然后令被覆盖的方法返回模仿对象。如果可以的话,添加需要原始对象的工厂方法的单元测试,以返回 正确类型的对象。模仿对象方法在处理许多对象依赖基础结构的其它对象或层时, 可以起到很好的效果。模仿对象符合实际对象的接口,但只要有足够的代码来“欺骗”测试对象并跟踪其行为。例如, 在单元测试中需要测试一个使用数据库的对象,或者需要测试连接 J2EE 应用服务器的对象,通常的测试用例需要安装、配置和发送本地数据库副本、运行测试然后再卸装本地数据库或者需要安装、配置应用服务器、运行测试然后再卸装 应用服务器,操作可能很麻烦,。模仿对象提供了解决这一困难的途径。对于既需要访问相关环境又要访问非公有变量或方法的类来说,模仿对象非常适合,但是, 如果只是访问非公有变量或方法,那么传统的模仿对象法显得有些笨重,可以对该法进行简化,不使用工厂方法,达到同样的效果。
下面的代码示例演示了经过简化的模仿对象方法:

清单 7. 简化的待测类 A 的模仿对象
                
//伪类,在运行时替换类A
public class MockA extends A{

public MockA(){
super();
s = "test";
}
}
//测试类
public class TestA extends TestCase{

public void setup(){

}
public void teardown(){

}
public void makeWordTest(){
A a = new MockA();
a.makeWord();
}
}

模仿对象方法既能消除运行环境的影响,又能解决多继承的难题,但是由于该方法使用子类的实例来替代父类的实例,对于私有成员变量及方法来说,仍然不能进行访问。




Java 编译器把 Java 源代码编译成字节码 bytecode(字节码),既然在测试中尽量要避免改变原来的代码,那么最直接的改造 Java 类的方法莫过于直接改写 class 文件。通过修改字节码中的关键字,将私有的成员变量及方法改成公有的成员变量及方法,可以做到在不改变源码的情况下访问到需要的成员变量及方法。Java 规范有 class 文件的格式的详细说明,直接编辑字节码确实可以改变 Java 类的行为,但是这也要求使用者对 Java class 文件有较深的理解。目前,比较流行的字节码处理工具有 Javassist,BCEL 和 ASM 等。这几种工具各有特点,适合于不同的应用场景,如果读者对字节码技术感兴趣,可以阅读后面的参考文献。本文选择利用字节码工具 ASM。
ASM 能被用来动态生成类或者修改既有类的功能。它可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类(.class)。ASM 作为 Java 字节码操控框架,是所有同类工具中效率最高的一个,并且由于其采用了基于 Vistor 模式的框架设计,它也是同类工具中最轻巧灵活的,尽管它的学习台阶相对要高一些,它仍然是达到本文目的的首选。
利 用 ASM 访问私有变量及方法,需要了解的比较重要的几个类:ClassReader、ClassVistor、MethodVisitor、 FieldVisitor 和 ClassAdaptor 等。ClassReader 类可以直接由字节数组或由 class 文件间接的获得字节码数据,它能正确的分析字节码,通过调用 accept 方法接受一个 ClassVisitor 接口的实现类实例作为参数,然后依次调用 ClassVisitor 接口的各个方法;ClassVisitor 接口中定义了对应 Java 类各个成员的访问函数,比如 visitMethod 会返回一个实现 MethordVisitor 接口的实例,visitField 会返回一个实现 FieldVisitor 接口的实例。不同 Visitor 的组合,可以非常简单的封装对字节码的各种修改;ClassAdaptor 类为 ClassVisitor 接口提供了一个默认实现。创建一个 ClassAdaptor 对象实例时,需要传入一个 ClassVisitor 接口的实现类实例来访问字节吗。因此当我们需要对字节码进行调整时,只需从 ClassAdaptor 类派生出一个子类,覆写需要修改的方法,完成相应功能后再把调用传递到下一个需要修改的 visitor 即可。
本例的应用场景为,要对公有方法 method() 进行单元测试,但是,该方法中有一个私有变量 number 是由另一个私有方法 makePaper() 付值,所以,需要在测试中为该私有变量置初值。

清单 8. 待测类 A
                
class A{
private String number = “”;
public void method() {
if(number.eaquals(“prefix”))
System.out.println("method..."+number);
else
System.out.println(number +”is null”);
}

private void makePaper() {
number=”prefix”;
System.out.println("makePaper...");
}
}


清单 9. 使用字节码访问类 A
                
//修改变量的修饰符
public class AccessClassAdapter extends ClassAdapter {
public AccessClassAdapter(ClassVisitor cv) {
super(cv);
}
public FieldVisitor visitField(final int access, String name,
final String desc, final String signature,
final Object value) {
int privateAccess = access;

//找到名字为number的变量
if (name.equals("number"))
privateAccess = Opcodes.ACC_PUBLIC;

//修字段的修饰符为public:在职责链传递过程中替换调用参数
return cv.visitField(privateAccess, name, desc,
signature, value);
}
public static void main(String[] args) throws Exception {
ClassReader cr = new ClassReader("A");
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassAdapter classAdapter = new AccessClassAdapter(cw);
cr.accept(classAdapter, ClassReader.SKIP_DEBUG);
byte[] data = cw.toByteArray();
//生成新的字节码文件
File file = new File("A.class");
FileOutputStream fout = new FileOutputStream(file);
fout.write(data);
fout.close();
}
}

执行完该类,将产生一个新的 A.class 文件。
测试类测试 method 方法,先对变量进行置初值,然后就可以像其他单元测试一样,对 method 方法进行测试。




回页首


方法 修饰符 使用难度 缺陷
protected 缺省 private
方法一:修改访问权限修饰符 低,有java编程基础即可。 由于需要修改源代码,虽然是同包可见,也会带来一些封闭性的问题。
方法二:利用安全性管理器 中,需要了解java安全性管理器及反射机制。 一些对代码安全有要求的程序,程序员并没有修改security manager的权限,此时,安全管理器方法失效。
方法三:使用模仿对象 较高,需要了解设计模式和待测对象的内部实现细节。 由于模仿对象要求伪类必需和待测类是继承与被继承的关系,所以当源码以private关键字修饰时,此方法失效。
方法四:利用字节码技术 高,需要操作和改写类部分的字节码。 学习成本高,需要了解Java字节码技术




在进行单元测试时,我们要尽可能的考虑代码的移植性和通用性,在不修改源程序的前提下达到测试的最佳效果。对于是否应该使用以及如何使用本文中提到的四种方法,需要开发人员根据具体场合谨慎选择。



本文转自 cping 51CTO博客,原文链接:http://blog.51cto.com/cping1982/129892
相关实践学习
消息队列+Serverless+Tablestore:实现高弹性的电商订单系统
基于消息队列以及函数计算,快速部署一个高弹性的商品订单系统,能够应对抢购场景下的高并发情况。
云安全基础课 - 访问控制概述
课程大纲 课程目标和内容介绍视频时长 访问控制概述视频时长 身份标识和认证技术视频时长 授权机制视频时长 访问控制的常见攻击视频时长
相关文章
|
11天前
|
Java 开发者
Java 中的 toString() 方法详解:为什么它如此重要?
在Java开发中,`toString()`方法至关重要,用于返回对象的字符串表示。默认实现仅输出类名和哈希码,信息有限且不直观。通过重写`toString()`,可展示对象字段值,提升调试效率与代码可读性。借助Lombok的`@Data`注解,能自动生成标准化的`toString()`方法,简化开发流程,尤其适合字段较多的场景。合理运用`toString()`,可显著提高开发效率与代码质量。
37 0
|
1月前
|
Java 开发者
重学Java基础篇—Java类加载顺序深度解析
本文全面解析Java类的生命周期与加载顺序,涵盖从加载到卸载的七个阶段,并深入探讨初始化阶段的执行规则。通过单类、继承体系的实例分析,明确静态与实例初始化的顺序。同时,列举六种触发初始化的场景及特殊场景处理(如接口初始化)。提供类加载完整流程图与记忆口诀,助于理解复杂初始化逻辑。此外,针对空指针异常等问题提出排查方案,并给出最佳实践建议,帮助开发者优化程序设计、定位BUG及理解框架机制。最后扩展讲解类加载器层次与双亲委派机制,为深入研究奠定基础。
62 0
|
5天前
|
Java 数据安全/隐私保护
Java 类和对象
本文介绍了Java编程中类和对象的基础知识,作为面向对象编程(OOP)的核心概念。类是对象的蓝图,定义实体类型;对象是具体实例,包含状态和行为。通过示例展示了如何创建表示汽车的类及其实例,并说明了构造函数、字段和方法的作用。同时,文章还探讨了访问修饰符的使用,强调封装的重要性,如通过getter和setter控制字段访问。最后总结了类与对象的关系及其在Java中的应用,并建议进一步学习继承等概念。
|
11天前
|
存储 Java 开发者
Java 中的 equals 方法:看似简单,实则深藏玄机
本文深入探讨了Java中`equals`方法的设计与实现。默认情况下,`equals`仅比较对象引用是否相同。以`String`类为例,其重写了`equals`方法,通过引用判断、类型检查、长度对比及字符逐一比对,确保内容相等的逻辑。文章还强调了`equals`方法需遵循的五大原则(自反性、对称性等),以及与`hashCode`的关系,避免集合操作中的潜在问题。最后,对比了`instanceof`和`getClass()`在类型判断中的优劣,并总结了正确重写`equals`方法的重要性,帮助开发者提升代码质量。
43 1
|
1月前
|
存储 JSON Java
《从头开始学java,一天一个知识点》之:方法定义与参数传递机制
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问&quot;`a==b`和`equals()`的区别&quot;,大脑突然空白 - 写出的代码总是莫名报NPE,却不知道问题出在哪个运算符 🚀 这个系列就是为你打造的Java「速效救心丸」!我们承诺:每天1分钟,地铁通勤、午休间隙即可完成学习;直击痛点,只讲高频考点和实际开发中的「坑位」;拒绝臃肿,没有冗长概念堆砌,每篇都有可运行的代码标本。上篇:《输入与输出:Scanner与System类》 | 下篇剧透:《方法重载与可变参数》。
52 25
|
1月前
|
缓存 安全 Java
《从头开始学java,一天一个知识点》之:输入与输出:Scanner与System类
你是否也经历过这些崩溃瞬间?三天教程连`i++`和`++i`都说不清,面试时`a==b`与`equals()`区别大脑空白,代码总是莫名报NPE。这个系列就是为你打造的Java「速效救心丸」!每天1分钟,地铁通勤、午休间隙即可学习。直击高频考点和实际开发中的“坑位”,拒绝冗长概念,每篇都有可运行代码示例。涵盖输入输出基础、猜数字游戏、企业编码规范、性能优化技巧、隐藏技能等。助你快速掌握Java核心知识,提升编程能力。点赞、收藏、转发,助力更多小伙伴一起成长!
47 19
|
1月前
|
存储 监控 安全
重学Java基础篇—类的生命周期深度解析
本文全面解析了Java类的生命周期,涵盖加载、验证、准备、解析、初始化、使用及卸载七个关键阶段。通过分阶段执行机制详解(如加载阶段的触发条件与技术实现),结合方法调用机制、内存回收保护等使用阶段特性,以及卸载条件和特殊场景处理,帮助开发者深入理解JVM运作原理。同时,文章探讨了性能优化建议、典型异常处理及新一代JVM特性(如元空间与模块化系统)。总结中强调安全优先、延迟加载与动态扩展的设计思想,并提供开发建议与进阶方向,助力解决性能调优、内存泄漏排查及框架设计等问题。
47 5
|
1月前
|
缓存 安全 Java
《从头开始学java,一天一个知识点》之:字符串处理:String类的核心API
🌱 **《字符串处理:String类的核心API》一分钟速通!** 本文快速介绍Java中String类的3个高频API:`substring`、`indexOf`和`split`,并通过代码示例展示其用法。重点提示:`substring`的结束索引不包含该位置,`split`支持正则表达式。进一步探讨了String不可变性的高效设计原理及企业级编码规范,如避免使用`new String()`、拼接时使用`StringBuilder`等。最后通过互动解密游戏帮助读者巩固知识。 (上一篇:《多维数组与常见操作》 | 下一篇预告:《输入与输出:Scanner与System类》)
64 11
|
11天前
|
Java
java中一个接口A,以及一个实现它的类B,一个A类型的引用对象作为一个方法的参数,这个参数的类型可以是B的类型吗?
本文探讨了面向对象编程中接口与实现类的关系,以及里氏替换原则(LSP)的应用。通过示例代码展示了如何利用多态性将实现类的对象传递给接口类型的参数,满足LSP的要求。LSP确保子类能无缝替换父类或接口,不改变程序行为。接口定义了行为规范,实现类遵循此规范,从而保证了多态性和代码的可维护性。总结来说,接口与实现类的关系天然符合LSP,体现了多态性的核心思想。
21 0
|
1月前
|
安全 IDE Java
重学Java基础篇—Java Object类常用方法深度解析
Java中,Object类作为所有类的超类,提供了多个核心方法以支持对象的基本行为。其中,`toString()`用于对象的字符串表示,重写时应包含关键信息;`equals()`与`hashCode()`需成对重写,确保对象等价判断的一致性;`getClass()`用于运行时类型识别;`clone()`实现对象复制,需区分浅拷贝与深拷贝;`wait()/notify()`支持线程协作。此外,`finalize()`已过时,建议使用更安全的资源管理方式。合理运用这些方法,并遵循最佳实践,可提升代码质量与健壮性。
48 1

热门文章

最新文章