- 浏览: 128072 次
- 性别:
- 来自: 佛山
文章分类
最新评论
-
jackyrong:
hi,你好,能否发一份代码到我邮箱?谢谢了,jackyrong ...
基于struts2+spring+hibernate+jquery的jmesa分页实现样例 -
qdongl:
不错,继续学习
Java 通用数据库连接类[支持存储过程 参数自动识别] -
ww_o_ww:
学习了··哈哈·· ··
JAVA EXCEL API 使用 -
Angel_Night:
@TestExecutionListeners( { Tran ...
使用 Spring 2.5 TestContext 测试框架 -
ray111:
缺少DBConnectionManager类?
Java 通用数据库连接类[支持存储过程 参数自动识别]
您的传统代码是不是要求使用匹配的类测试套件才能针对其源代码库运行?针对此类目的,jMock 堪称是一个优秀的测试框架。但是,并不是所有情况都能够适用,尤其是必须以 jMock 不期望的方式构造对象时。为避免生成自定义模拟对象套件才能支持应用程序中的单元测试的麻烦,可以调整 RMock,与 jMock 无缝地结合使用,从而解决这一问题。
模拟对象将模仿出于指导代码执行的惟一目的而编写的类的行为,以便它在测试时符合代码执行要求。最终,模拟对象数目可以随着应用程序类数目的增长而增长。使用 jMock、RMock 甚至 EasyMock 等框架有助于消除对物理的独立存在的模拟对象集的需求。
EasyMock 框架的一个主要缺点是不能模拟具体类 —— 而只能模拟接口。在本文中,我将向您展示怎样使用 jMock 框架来模拟具体类和 接口,以及如何用 RMock 测试某些模糊的情况。
注
Eclipse 平台为使用 jMock 和 RMock 测试框架提供了一种易于使用的机制。
在 Eclipse IDE 中配置 jMock 和 RMock
注:有关 JUnit、jMock 和 RMock 的最新二进制文件,请参阅 参考资料。
首先启动 Eclipse 集成开发环境 (IDE)。接下来,创建一个基本 Java™ 项目,稍后将把 JUnit、jMock 和 RMock Java Archive (JAR) 库导入到该项目中。将 Java 项目命名为 TestingExample。在 Java Perspective 内,选择 Project > Properties,然后单击 Libraries 选项卡,如下所示:
图 1. 在 Eclipse 中编辑 TestingExample 项目的属性
当 JAR 文件位于 Java 类路径(即,已在 Eclipse 内配置的 Java 运行时环境(Java Runtime Environment,JRE))中时,请使用 Add JARs 按钮。Add Variable 按钮适用于文件系统(本地或远程)中的资源(包括 JAR)所驻留的具体目录,并且通常可以引用此按钮。在必须引用 Eclipse 中默认的那些特定资源或为特定的 Eclipse 工作区环境配置的那些特定资源时,请使用 Add Library 按钮。单击 Add Class Folder,从已经配置为项目一部分的一个现有项目文件夹中添加资源。
对于本示例,请单击 Add External JARs 并浏览到已下载的 jMock 和 RMock JAR。将其添加到项目中。当显示图 2 中所示的属性窗口时,请单击 OK。
图 2. 已添加到 TestingExample 项目中的 jMock 和 RMock JAR
TestExample 源代码
对于 TestExample 项目,您将使用来自四个类的源代码:
ServiceClass.java
Collaborator.java
ICollaborator.java
ServiceClassTest.java
待测试的类将是 ServiceClass,该类包含了一个方法:runService()。服务方法将获取实现简单接口 ICollaborator 的 Collaborator 对象。具体的 Collaborator 类中实现了一个方法:executeJob()。Collaborator 是必须正确模拟的类。
第四个类是测试类:ServiceClassTest(实现的性质已经被尽可能地简化)。清单 1 将显示第四个类的代码。
清单 1. 服务类的样例代码
在 ServiceClass 类中,if...else 代码块是一个简单的逻辑分支,根据测试期望说明选取一条路经 —— 而不是另一条路经 —— 之后测试将失败(或通过)的原因。下面显示了 Collaborator 类的源代码。
清单 2. Collaborator 类的样例代码
Collaborator 类也十分简单,它配有无参数的构造函数以及从 executeJob() 方法返回的简单 String。下面的代码显示了 ICollaborator 类的代码。
接口 ICollaborator 有一个必须在 Collaborator 类中实现的方法。
以上代码就绪后,让我们继续检验怎样在各种场景中成功地运行 ServiceClass 类的测试。
场景 1:使用 jMock 模拟接口
测试 ServiceClass 类中的服务方法十分简单。假定测试要求为证明 runService() 方法并未运行 —— 换言之,返回的布尔结果是 false。在这种情况下,传递给 runService() 方法的 ICollaborator 对象被模拟 为期望调用 executeJob() 方法,并返回除了 “success” 以外的字符串。通过这种方法,确保把布尔字符串 false 返回给测试。
下面所示的是包含测试逻辑的 ServiceClassTest 类代码。
清单 3. 场景 1 的 ServiceClassTest 类样例代码
编写测试的时机
用测试模拟框架运行您自己的测试的最佳方法是利用 test-first 灵活方法。首先创建测试并设定期望。仅在测试失败后才编写实现以修正测试。当测试运行正常时,您将编写另一个测试以检查稍后添加到待测试的类中的功能。
如果将在各种测试用例中执行公共操作,则在测试中包括 setUp() 方法是一种很好的想法。包括 tearDown() 方法也很不错,但不作严格要求,除非要运行集成测试。
另请注意,使用 jMock 和 RMock,框架将在测试运行结束时或测试运行期间在所有模拟对象中检查所有期望。并不实际需要为每个模拟期望包括 verify() 方法。当作为 JUnit 测试运行时,测试将通过,如下所示:
图 3. 场景 1 测试通过
ServiceTestClass 类将扩展 jMock CGLIB 的 org.jmock.cglib.MockObjectTestCase 类。mockCollaborator 是一个十分简单的 org.jmock.JMock 类。通常,用 jMock 生成模拟对象有两种方法:
要模拟接口,则使用 new Mock(Class.class) 方法
要模拟具体类,则使用 mock(Class.class, "identifier") 方法
必须注意的是怎样将模拟代理 传递给 ServiceClass 类中的 runService() 方法。使用 jMock,您可以从已创建的模拟对象(其中期望已经被设定)中提取代理实现。这一点在本文稍后的场景中至关重要,尤其是在涉及 RMock 的场景中。
场景 2:使用 jMock 模拟带有默认构造函数的具体类
假定 ServiceClass 类中的 runService() 方法仅接受 Collaborator 类的具体实现。jMock 能够确保先前的测试通过而无需 更改期望吗?是的,只要您能够构造简单默认样式的 Collaborator 类。
更改 ServiceClass 类中的 runService() 方法使其反映以下代码。
清单 4. 经过编辑的场景 2 的 ServiceClass 类
ServiceClass 类的 if...else 逻辑分支保持不变(为了清晰起见)。同时,无参数构造函数仍然适用。注,并不总是需要有创造性逻辑,例如 while...do 子句或 for 循环来正确地测试类的方法。只要有针对类使用的对象的方法执行,简单的模拟期望就足以测试那些执行。
您还必须更改 ServiceClassTest 类以匹配场景,如下所示:
清单 5. 经过编辑的场景 2 的 ServiceClassTest 类
这里有几点需要注意。第一,runService() 方法签名已经不同于以往。它现在不接受 ICollaborator 接口,而接受具体类实现(Collaborator 类)。就测试框架而言,此更改非常重大(注,虽然在本质上反对多态,但是我们将使用传递具体类的示例(仅供举例之用)。在实际的面向对象的场景中绝对不能这样做)。
第二,模拟 Collaborator 类的方式已经更改。使用 jMock CGLIB 库可以模拟具体类实现。提供给 jMock CGLIB 的 mock() 方法的附加 String 参数被用作创建的模拟对象的标识符。使用 jMock(当然,还有 RMock)时,在单一测试用例内每个模拟对象设置都要求有惟一标识符。这对于在公共的 setUp() 方法中或在实际测试方法内定义的模拟对象来说是正确的。
第三,测试方法的原始期望并未更改。仍然要求有 false 证明才能使测试通过。这是十分重要的,因为通过展示使用的测试框架足够灵活、可以适应各种输入带来的更改、同时仍然允许获得不变的测试结果,使它们在无法调节输入生成同样的结果时展示了其实际限制。
现在,重新运行作为 JUnit 测试的测试。测试将通过,如下所示:
图 4. 场景 2 测试通过
在下一个场景中,情况会变得略微复杂一些。您将使用 RMock 框架来相对缓解一下这种困难的情形。
场景 3:使用 jMock 和 RMock 模拟带有非默认构造函数的具体类
首先像以前一样尝试使用 jMock 来模拟 Collaborator 对象 —— 只是这一次,Collaborator 没有默认的无参数构造函数。注,保留布尔 false 结果的测试期望。
同时假定 Collaborator 对象要求使用字符串和原始的 int 作为传递给构造函数的参数。清单 6 显示了对 Collaborator 对象所做的更改。
清单 6. 经过编辑的场景 3 的 Collaborator 类
Collaborator 类构造函数仍然十分简单。用传入参数设定类字段。这里不必使用任何其他逻辑,并且其 executeJob() 函数保持不变。
重新运行测试,并且示例的所有其他组件保持不变。结果是灾难性的测试失败,如下所示:
图 5. 场景 3 测试失败
以上测试是作为简单的 JUnit 测试运行的,没有代码覆盖。您可以用大多数代码覆盖工具(例如,Cobertura 或 EclEmma)来运行本文中列出的任何一个测试。但是,用 Eclipse 内的代码覆盖工具运行 RMock 测试时会带来一些问题(参见 表 1)。以下代码显示了实际堆栈跟踪的代码片段。
清单 7. 场景 3 中测试失败的堆栈跟踪
失败原因是 jMock 无法通过没有无参数构造函数的类定义创建可行的模拟对象。实例化 Collaborator 对象的惟一方法是提供两个简单参数。您现在必须找到一种方法把参数提供给模拟对象实例化过程以达到同样的效果,这就是使用 RMock 的原因。
用 RMock 测试框架更正失败的测试
要更正测试,必须执行一些修改。这些更改可能显得十分重要,但是实际上,它们是一种相对简单的解决方法,利用两种框架的强大功能来实现目的。
必需的第一项更改是使测试类成为 RMock TestCase,而不是成为 jMock CGLIB TestCase。目的是在测试本身内启用属于 RMock 的那些模拟对象的较容易的配置并且 —— 更重要的是 —— 在最初设置期间启用这些配置。经验证明,如果测试类扩展的整个 TestCase 对象属于 RMock,则通过两个框架构造和使用模拟对象将更容易。此外,乍看之下,快速确定模拟对象的流程将更容易一些(在这里,流程 用于描述使用模拟对象作为参数甚或作为其他模拟对象的返回类型的情况)。
必需的第二项更改是(至少)构造一个保存传递给 Collaborator 类的构造函数的参数实际值的对象数组。为了清晰起见,还可以包括构造函数接受的类类型的类型数组并可以传递该数组,以及刚刚描述为参数的对象数组以实例化模拟 Collaborator 对象。
第三项更改涉及用正确语法构造对 RMock 模拟对象的一个或多个期望。而第四项也是最后一项必需的更改是使 RMock 模拟对象脱离记录状态转入就绪状态。
实现 RMock 更改
清单 9 显示了对 ServiceClassTest 类的最终修改。它还显示了 RMock 及其相关功能的引入。
清单 9. 修正场景 3 的 ServiceClassTest 类
首先,需要注意测试的期望仍未改变。RMockTestCase 类的导入预示着引入 RMock 框架功能。接下来,测试类现在将扩展 RMockTestCase,而不是 MockObjectTestCase。稍后,我将向您展示在 TestClass 对象仍为 RMockTestCase 类型的对象的测试用例中重新引入 MockObjectTestCase。
使用 intercept() 方法的备选方法
通过 RMock,您可以使用 intercept() 方法仅模拟具体类。可以使用 RMock mock() 方法模拟具体类和接口。仅当需要模拟那几个重要方法时,使用 interface() 方法。此方法被视为经过改进的 mock() 方法。
在 setUp() 方法内,用 Collaborator 类的构造方法所需的实际 值实例化对象数组。该数组被立刻传递给 RMock 的 intercept() 方法来帮助实例化模拟对象。方法的签名类似于 jMock CGLIB mock() 方法的签名,因为这两种方法将接纳惟一模拟对象标识符作为参数。模拟对象到 Collaborator 类型的类强制转换十分有必要,因为 intercept() 方法将返回类型 Object。
在测试方法本身 testRunServiceAndReturnFalse() 之内,您可以看到更多更改。模拟 Collaborator 对象的 executeJob() 方法将被调用。在此阶段,模拟对象处于记录状态 —— 即简单地定义对象将一直期望的方法调用,因此,模拟将相应地记录期望。下一行是对模拟对象的通知,用于确保当它遇到 executeJob() 方法时,它应当返回字符串 failure。因此,使用 RMock,您只需通过调用方法而不调用模拟对象(并传递它可能需要的任何参数)来描述期望,然后修改该期望以相应地调整任何返回类型。
最后,调用 RMock 方法 startVerification() 把模拟 Collaborator 对象转为就绪状态。模拟对象现已准备好在 ServiceClass 类中作为实际对象使用。该方法非常重要并且必须调用它才能避免测试初始化失败。
测试更改
再次重新运行 ServiceClassTest 以达到最终的肯定结果:在模拟对象实例化期间提供的参数造成了所有差别。图 6 显示 JUnit 测试已经通过。
图 6. 使用 RMock 的场景 3 测试成功
assertFalse(result) 代码行表示与场景 1 相同的测试期望,而 RMock 像 jMock 以前那样维持测试成功。在许多方面,这都十分重要,但是这里更重要的特点可能是实践了修正失败测试的灵活 原则而不更改测试期望。惟一的差别是使用了另一个框架。
在下一个场景中,您将在一种特殊情况下使用 jMock 和 RMock。没有一个框架能够仅凭自身就实现正确结果,除非在测试内形成某种联合。
场景 4:jMock 和 RMock 之间的特定协作
如前所述,我希望检验两个框架必须协同工作才能实现某个结果的情况。否则,构建良好的测试每次都将失败。在某些情况下,使用 jMock 还是 RMock 并不重要,例如,当需要模拟的接口或类存在于已经签名的 JAR 中时。此类情况十分少见,但是当测试针对安全专有的产品(通常是这样或那样的一类现有软件)中的应用程序编程接口 (API) 编写代码时可能会出现此情况。
清单 10 显示了两个框架完成测试用例的示例。
清单 10. 场景 4 的测试示例
在 setUp() 方法内,根据为扩展 jMock-CGLIB MockObjectTestCase 对象而创建的私有内部类实例化了新 "testcase"。使用任何 jMock 功能时,这个小解决方法对于确保整个测试类为 RMock TestCase 对象十分有必要。例如,您将设定类似 testCase.once() 而不是类似 once() 的 jMock 期望,因为 TestClass 对象将扩展 RMockTestCase。
构建基于 ClassB 类的模拟对象并向其提供期望。然后您将使用它帮助实例化 RMock Collaborator 模拟对象。待测试的类是 MyNewClass 类(在这里显示为私有内部类)。同时,其 executeJob() 方法将接收 Collaborator 对象并运行 executeSomeImportantFunction() 方法。
清单 11 和 12 分别显示了 ClassA 和 ClassB 的代码。ClassA 是没有实现的简单类,而 ClassB 显示了阐明要点所需的最少细节。
清单 11. ClassA 类
此类只是我使用的一个虚构类,用于强化一个要点:要模拟构造函数接收对象参数的类,有必要使用 RMock。
清单 12. ClassB 类
ClassB 类的 wierdMethod 将返回 failed。这是十分重要的,因为该类必须简短地返回另一个字符串才能使测试通过。
清单 13 显示了测试示例的最重要部分:Collaborator 类。
清单 13. Collaborator 类
注,首要的是,使用 jMock 框架模拟了 ClassB 类。使用 RMock,没有一种实际方法从模拟对象中提取和使用代理,以便在测试 setUp() 方法中的其他位置使用该代理。使用 RMock,仅当调用 startVerification() 方法之后,才显示代理对象。本例中的优点是使用 jMock,因为在需要返回自我模拟对象的情况下,可以 获得设置其他模拟对象所需的信息。
反过来,需要注意的第二点是您不能使用 jMock 框架来模拟 Collaborator 类。原因是该类没有无参数构造函数。此外,在构造函数内有某种逻辑,这种逻辑将确定是否先获得实例。事实上,出于本次讨论的目的,ClassB 中的 wierdMethod() 方法必须返回 passed 才能使 Collaborator 对象被实例化。但是,另请注意,在默认情况下,方法总是返回 failed。测试成功明显需要模拟 ClassB。
此外,不同于先前的示例,此场景中的类数组作为附加参数被包含到了 intercept() 方法中。对此不作严格要求,但是用它作为密钥可以快速识别在实例化 RMock 测试对象时使用的对象类。
运行新测试用例。这一次,您将看到成功的结果。图 7 将显示令人愉快的结果。
图 7. RMock 与 jMock 协作使场景 4 测试成功
Collaborator 模拟对象已被正确设置,并且 mockClassB 对象将按预期执行。
快速查看测试工具差别
正如您已经在场景中看到的,jMock 和 RMock 都是用于测试 Java 代码的强大工具。但是,用于开发和测试的任何其他工具总是有限制。实际上,其他测试工具都是可用的,但是这些测试工具的运行情况都不如 RMock 和 jMock(在 Java 技术中)。个人经验告诉我 Microsoft® .NET 框架也附带了一些功能强大的工具(例如 TypeMock),但是那超出了本文(实际上还有平台)的范围。
表 1 显示了两个框架之间的一些不同之处以及随着时间的推移遇到的可能问题,尤其是在 Eclipse 环境中。
表 1. RMock 与 jMock 测试框架之间的不同之处
测试模拟样式 jMock RMock
可以模拟接口 是:新的 Mock() 方法 是:mock() 方法
可以模拟具体类 是:带有 CGLIB 的 mock() 方法 是:mock() 或 intercept() 方法
可以模拟任何具体类 否:无参数构造函数必须存在 是
可以随时获得代理 是 否:仅当 startVerification() 处于就绪状态后
使用其他Eclipse插件的问题无已知问题 是:与 Eclipse 的 CoverClipse 插件存在内存冲突
结束语
我鼓励您使用这些框架,利用它们的力量来生成单元测试的结果。许多 Java 开发人员不习惯于频繁编写测试。而且如果需要编写测试,通常都是十分简单、覆盖方法的主要功能目标的测试。要测试代码的某些 “难以达到的” 部分,jMock 和 RMock 都是优秀的选择。
使用 jMock 和 RMock 将极大地减少代码中的 bug,提高使用经过证明的方法测试编程逻辑的技巧。此外,阅读文档并用这些框架和其他框架的改进版本进行测试(并减少构造不好的代码)将对提高开发人员技巧有着额外的帮助。
模拟对象将模仿出于指导代码执行的惟一目的而编写的类的行为,以便它在测试时符合代码执行要求。最终,模拟对象数目可以随着应用程序类数目的增长而增长。使用 jMock、RMock 甚至 EasyMock 等框架有助于消除对物理的独立存在的模拟对象集的需求。
EasyMock 框架的一个主要缺点是不能模拟具体类 —— 而只能模拟接口。在本文中,我将向您展示怎样使用 jMock 框架来模拟具体类和 接口,以及如何用 RMock 测试某些模糊的情况。
注
Eclipse 平台为使用 jMock 和 RMock 测试框架提供了一种易于使用的机制。
在 Eclipse IDE 中配置 jMock 和 RMock
注:有关 JUnit、jMock 和 RMock 的最新二进制文件,请参阅 参考资料。
首先启动 Eclipse 集成开发环境 (IDE)。接下来,创建一个基本 Java™ 项目,稍后将把 JUnit、jMock 和 RMock Java Archive (JAR) 库导入到该项目中。将 Java 项目命名为 TestingExample。在 Java Perspective 内,选择 Project > Properties,然后单击 Libraries 选项卡,如下所示:
图 1. 在 Eclipse 中编辑 TestingExample 项目的属性
当 JAR 文件位于 Java 类路径(即,已在 Eclipse 内配置的 Java 运行时环境(Java Runtime Environment,JRE))中时,请使用 Add JARs 按钮。Add Variable 按钮适用于文件系统(本地或远程)中的资源(包括 JAR)所驻留的具体目录,并且通常可以引用此按钮。在必须引用 Eclipse 中默认的那些特定资源或为特定的 Eclipse 工作区环境配置的那些特定资源时,请使用 Add Library 按钮。单击 Add Class Folder,从已经配置为项目一部分的一个现有项目文件夹中添加资源。
对于本示例,请单击 Add External JARs 并浏览到已下载的 jMock 和 RMock JAR。将其添加到项目中。当显示图 2 中所示的属性窗口时,请单击 OK。
图 2. 已添加到 TestingExample 项目中的 jMock 和 RMock JAR
TestExample 源代码
对于 TestExample 项目,您将使用来自四个类的源代码:
ServiceClass.java
Collaborator.java
ICollaborator.java
ServiceClassTest.java
待测试的类将是 ServiceClass,该类包含了一个方法:runService()。服务方法将获取实现简单接口 ICollaborator 的 Collaborator 对象。具体的 Collaborator 类中实现了一个方法:executeJob()。Collaborator 是必须正确模拟的类。
第四个类是测试类:ServiceClassTest(实现的性质已经被尽可能地简化)。清单 1 将显示第四个类的代码。
清单 1. 服务类的样例代码
public class ServiceClass { public ServiceClass(){ //no-args constructor } public boolean runService(ICollaborator collaborator){ if("success".equals(collaborator.executeJob())){ return true; } else { return false; } } }
在 ServiceClass 类中,if...else 代码块是一个简单的逻辑分支,根据测试期望说明选取一条路经 —— 而不是另一条路经 —— 之后测试将失败(或通过)的原因。下面显示了 Collaborator 类的源代码。
清单 2. Collaborator 类的样例代码
public class Collaborator implements ICollaborator{ public Collaborator(){ //no-args constructor } public String executeJob(){ return "success"; } }
Collaborator 类也十分简单,它配有无参数的构造函数以及从 executeJob() 方法返回的简单 String。下面的代码显示了 ICollaborator 类的代码。
public interface ICollaborator { public abstract String executeJob(); }
接口 ICollaborator 有一个必须在 Collaborator 类中实现的方法。
以上代码就绪后,让我们继续检验怎样在各种场景中成功地运行 ServiceClass 类的测试。
场景 1:使用 jMock 模拟接口
测试 ServiceClass 类中的服务方法十分简单。假定测试要求为证明 runService() 方法并未运行 —— 换言之,返回的布尔结果是 false。在这种情况下,传递给 runService() 方法的 ICollaborator 对象被模拟 为期望调用 executeJob() 方法,并返回除了 “success” 以外的字符串。通过这种方法,确保把布尔字符串 false 返回给测试。
下面所示的是包含测试逻辑的 ServiceClassTest 类代码。
清单 3. 场景 1 的 ServiceClassTest 类样例代码
import org.jmock.Mock; import org.jmock.cglib.MockObjectTestCase; public class ServiceClassTest extends MockObjectTestCase { private ServiceClass serviceClass; private Mock mockCollaborator; private ICollaborator collaborator; public void setUp(){ serviceClass = new ServiceClass(); mockCollaborator = new Mock(ICollaborator.class); } public void testRunServiceAndReturnFalse(){ mockCollaborator.expects(once()).method\ ("executeJob").will(returnValue("failure")); collaborator = (ICollaborator)mockCollaborator.proxy(); boolean result = serviceClass.runService(collaborator); assertFalse(result); } }
编写测试的时机
用测试模拟框架运行您自己的测试的最佳方法是利用 test-first 灵活方法。首先创建测试并设定期望。仅在测试失败后才编写实现以修正测试。当测试运行正常时,您将编写另一个测试以检查稍后添加到待测试的类中的功能。
如果将在各种测试用例中执行公共操作,则在测试中包括 setUp() 方法是一种很好的想法。包括 tearDown() 方法也很不错,但不作严格要求,除非要运行集成测试。
另请注意,使用 jMock 和 RMock,框架将在测试运行结束时或测试运行期间在所有模拟对象中检查所有期望。并不实际需要为每个模拟期望包括 verify() 方法。当作为 JUnit 测试运行时,测试将通过,如下所示:
图 3. 场景 1 测试通过
ServiceTestClass 类将扩展 jMock CGLIB 的 org.jmock.cglib.MockObjectTestCase 类。mockCollaborator 是一个十分简单的 org.jmock.JMock 类。通常,用 jMock 生成模拟对象有两种方法:
要模拟接口,则使用 new Mock(Class.class) 方法
要模拟具体类,则使用 mock(Class.class, "identifier") 方法
必须注意的是怎样将模拟代理 传递给 ServiceClass 类中的 runService() 方法。使用 jMock,您可以从已创建的模拟对象(其中期望已经被设定)中提取代理实现。这一点在本文稍后的场景中至关重要,尤其是在涉及 RMock 的场景中。
场景 2:使用 jMock 模拟带有默认构造函数的具体类
假定 ServiceClass 类中的 runService() 方法仅接受 Collaborator 类的具体实现。jMock 能够确保先前的测试通过而无需 更改期望吗?是的,只要您能够构造简单默认样式的 Collaborator 类。
更改 ServiceClass 类中的 runService() 方法使其反映以下代码。
清单 4. 经过编辑的场景 2 的 ServiceClass 类
public class ServiceClass { public ServiceClass(){ //no-args constructor } public boolean runService(Collaborator collaborator){ if("success".equals(collaborator.executeJob())){ return true; } else{ return false; } } }
ServiceClass 类的 if...else 逻辑分支保持不变(为了清晰起见)。同时,无参数构造函数仍然适用。注,并不总是需要有创造性逻辑,例如 while...do 子句或 for 循环来正确地测试类的方法。只要有针对类使用的对象的方法执行,简单的模拟期望就足以测试那些执行。
您还必须更改 ServiceClassTest 类以匹配场景,如下所示:
清单 5. 经过编辑的场景 2 的 ServiceClassTest 类
private ServiceClass serviceClass; private Mock mockCollaborator; private Collaborator collaborator; public void setUp(){ serviceClass = new ServiceClass(); mockCollaborator = mock(Collaborator.class, "mockCollaborator"); } public void testRunServiceAndReturnFalse(){ mockCollaborator.expects(once()).method("executeJob").will(returnValue("failure")); collaborator = (Collaborator)mockCollaborator.proxy(); boolean result = serviceClass.runService(collaborator); assertFalse(result); } }
这里有几点需要注意。第一,runService() 方法签名已经不同于以往。它现在不接受 ICollaborator 接口,而接受具体类实现(Collaborator 类)。就测试框架而言,此更改非常重大(注,虽然在本质上反对多态,但是我们将使用传递具体类的示例(仅供举例之用)。在实际的面向对象的场景中绝对不能这样做)。
第二,模拟 Collaborator 类的方式已经更改。使用 jMock CGLIB 库可以模拟具体类实现。提供给 jMock CGLIB 的 mock() 方法的附加 String 参数被用作创建的模拟对象的标识符。使用 jMock(当然,还有 RMock)时,在单一测试用例内每个模拟对象设置都要求有惟一标识符。这对于在公共的 setUp() 方法中或在实际测试方法内定义的模拟对象来说是正确的。
第三,测试方法的原始期望并未更改。仍然要求有 false 证明才能使测试通过。这是十分重要的,因为通过展示使用的测试框架足够灵活、可以适应各种输入带来的更改、同时仍然允许获得不变的测试结果,使它们在无法调节输入生成同样的结果时展示了其实际限制。
现在,重新运行作为 JUnit 测试的测试。测试将通过,如下所示:
图 4. 场景 2 测试通过
在下一个场景中,情况会变得略微复杂一些。您将使用 RMock 框架来相对缓解一下这种困难的情形。
场景 3:使用 jMock 和 RMock 模拟带有非默认构造函数的具体类
首先像以前一样尝试使用 jMock 来模拟 Collaborator 对象 —— 只是这一次,Collaborator 没有默认的无参数构造函数。注,保留布尔 false 结果的测试期望。
同时假定 Collaborator 对象要求使用字符串和原始的 int 作为传递给构造函数的参数。清单 6 显示了对 Collaborator 对象所做的更改。
清单 6. 经过编辑的场景 3 的 Collaborator 类
public class Collaborator{ private String collaboratorString; private int collaboratorInt; public Collaborator(String string, int number){ collaboratorString = string; collaboratorInt = number; } public String executeJob(){ return "success"; } }
Collaborator 类构造函数仍然十分简单。用传入参数设定类字段。这里不必使用任何其他逻辑,并且其 executeJob() 函数保持不变。
重新运行测试,并且示例的所有其他组件保持不变。结果是灾难性的测试失败,如下所示:
图 5. 场景 3 测试失败
以上测试是作为简单的 JUnit 测试运行的,没有代码覆盖。您可以用大多数代码覆盖工具(例如,Cobertura 或 EclEmma)来运行本文中列出的任何一个测试。但是,用 Eclipse 内的代码覆盖工具运行 RMock 测试时会带来一些问题(参见 表 1)。以下代码显示了实际堆栈跟踪的代码片段。
清单 7. 场景 3 中测试失败的堆栈跟踪
引用
...Superclass has no null constructors but no arguments were given
at net.sf.cglib.proxy.Enhancer.emitConstructors(Enhancer.java:718)
at net.sf.cglib.proxy.Enhancer.generateClass(Enhancer.java:499)
at net.sf.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:216)
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:660)
.....
.....
at net.sf.cglib.proxy.Enhancer.emitConstructors(Enhancer.java:718)
at net.sf.cglib.proxy.Enhancer.generateClass(Enhancer.java:499)
at net.sf.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:216)
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:660)
.....
.....
失败原因是 jMock 无法通过没有无参数构造函数的类定义创建可行的模拟对象。实例化 Collaborator 对象的惟一方法是提供两个简单参数。您现在必须找到一种方法把参数提供给模拟对象实例化过程以达到同样的效果,这就是使用 RMock 的原因。
用 RMock 测试框架更正失败的测试
要更正测试,必须执行一些修改。这些更改可能显得十分重要,但是实际上,它们是一种相对简单的解决方法,利用两种框架的强大功能来实现目的。
必需的第一项更改是使测试类成为 RMock TestCase,而不是成为 jMock CGLIB TestCase。目的是在测试本身内启用属于 RMock 的那些模拟对象的较容易的配置并且 —— 更重要的是 —— 在最初设置期间启用这些配置。经验证明,如果测试类扩展的整个 TestCase 对象属于 RMock,则通过两个框架构造和使用模拟对象将更容易。此外,乍看之下,快速确定模拟对象的流程将更容易一些(在这里,流程 用于描述使用模拟对象作为参数甚或作为其他模拟对象的返回类型的情况)。
必需的第二项更改是(至少)构造一个保存传递给 Collaborator 类的构造函数的参数实际值的对象数组。为了清晰起见,还可以包括构造函数接受的类类型的类型数组并可以传递该数组,以及刚刚描述为参数的对象数组以实例化模拟 Collaborator 对象。
第三项更改涉及用正确语法构造对 RMock 模拟对象的一个或多个期望。而第四项也是最后一项必需的更改是使 RMock 模拟对象脱离记录状态转入就绪状态。
实现 RMock 更改
清单 9 显示了对 ServiceClassTest 类的最终修改。它还显示了 RMock 及其相关功能的引入。
清单 9. 修正场景 3 的 ServiceClassTest 类
import com.agical.rmock.extension.junit.RMockTestCase; public class ServiceClassTest extends RMockTestCase { private ServiceClass serviceClass; private Collaborator collaborator; public void setUp(){ serviceClass = new ServiceClass(); Object[] objectArray = new Object[]{"exampleString", 5}; collaborator = (Collaborator)intercept(Collaborator.class, objectArray, "mockCollaborator"); } public void testRunServiceAndReturnFalse(){ collaborator.executeJob(); modify().returnValue("failure"); startVerification(); boolean result = serviceClass.runService(collaborator); assertFalse(result); } }
首先,需要注意测试的期望仍未改变。RMockTestCase 类的导入预示着引入 RMock 框架功能。接下来,测试类现在将扩展 RMockTestCase,而不是 MockObjectTestCase。稍后,我将向您展示在 TestClass 对象仍为 RMockTestCase 类型的对象的测试用例中重新引入 MockObjectTestCase。
使用 intercept() 方法的备选方法
通过 RMock,您可以使用 intercept() 方法仅模拟具体类。可以使用 RMock mock() 方法模拟具体类和接口。仅当需要模拟那几个重要方法时,使用 interface() 方法。此方法被视为经过改进的 mock() 方法。
在 setUp() 方法内,用 Collaborator 类的构造方法所需的实际 值实例化对象数组。该数组被立刻传递给 RMock 的 intercept() 方法来帮助实例化模拟对象。方法的签名类似于 jMock CGLIB mock() 方法的签名,因为这两种方法将接纳惟一模拟对象标识符作为参数。模拟对象到 Collaborator 类型的类强制转换十分有必要,因为 intercept() 方法将返回类型 Object。
在测试方法本身 testRunServiceAndReturnFalse() 之内,您可以看到更多更改。模拟 Collaborator 对象的 executeJob() 方法将被调用。在此阶段,模拟对象处于记录状态 —— 即简单地定义对象将一直期望的方法调用,因此,模拟将相应地记录期望。下一行是对模拟对象的通知,用于确保当它遇到 executeJob() 方法时,它应当返回字符串 failure。因此,使用 RMock,您只需通过调用方法而不调用模拟对象(并传递它可能需要的任何参数)来描述期望,然后修改该期望以相应地调整任何返回类型。
最后,调用 RMock 方法 startVerification() 把模拟 Collaborator 对象转为就绪状态。模拟对象现已准备好在 ServiceClass 类中作为实际对象使用。该方法非常重要并且必须调用它才能避免测试初始化失败。
测试更改
再次重新运行 ServiceClassTest 以达到最终的肯定结果:在模拟对象实例化期间提供的参数造成了所有差别。图 6 显示 JUnit 测试已经通过。
图 6. 使用 RMock 的场景 3 测试成功
assertFalse(result) 代码行表示与场景 1 相同的测试期望,而 RMock 像 jMock 以前那样维持测试成功。在许多方面,这都十分重要,但是这里更重要的特点可能是实践了修正失败测试的灵活 原则而不更改测试期望。惟一的差别是使用了另一个框架。
在下一个场景中,您将在一种特殊情况下使用 jMock 和 RMock。没有一个框架能够仅凭自身就实现正确结果,除非在测试内形成某种联合。
场景 4:jMock 和 RMock 之间的特定协作
如前所述,我希望检验两个框架必须协同工作才能实现某个结果的情况。否则,构建良好的测试每次都将失败。在某些情况下,使用 jMock 还是 RMock 并不重要,例如,当需要模拟的接口或类存在于已经签名的 JAR 中时。此类情况十分少见,但是当测试针对安全专有的产品(通常是这样或那样的一类现有软件)中的应用程序编程接口 (API) 编写代码时可能会出现此情况。
清单 10 显示了两个框架完成测试用例的示例。
清单 10. 场景 4 的测试示例
public class MyNewClassTest extends RMockTestCase{ private MyNewClass myClass; private MockObjectTestCase testCase; private Collaborator collaborator; private Mock mockClassB; public void setUp(){ myClass = new MyNewClass(); testCase = new MyMockObjectTestCase(); mockClassB = testCase.mock(ClassB.class, "mockClassB"); mockClassB.expects(testCase.once()).method("wierdMethod"). will(testCase.returnValue("passed")); Class[] someClassArray = new Class[]{String.class, ClassA.class, ClassB.class}; Object[] someObjectArray = new Object[] {"someArbitraryString", new ClassA(), (ClassB)mockClassB.proxy()}; collaborator = (Collaborator)intercept (Collaborator.class, someClassArray, someObjectArray, "mockCollaborator"); } public void testRMockAndJMockInCollaboration(){ startVerification(); assertTrue(myClass.executeJob(collaborator)); } private class MyMockObjectTestCase extends MockObjectTestCase{} private class MyNewClass{ public boolean executeJob(Collaborator collaborator){ collaborator.executeSomeImportantFunction(); return true; } } }
在 setUp() 方法内,根据为扩展 jMock-CGLIB MockObjectTestCase 对象而创建的私有内部类实例化了新 "testcase"。使用任何 jMock 功能时,这个小解决方法对于确保整个测试类为 RMock TestCase 对象十分有必要。例如,您将设定类似 testCase.once() 而不是类似 once() 的 jMock 期望,因为 TestClass 对象将扩展 RMockTestCase。
构建基于 ClassB 类的模拟对象并向其提供期望。然后您将使用它帮助实例化 RMock Collaborator 模拟对象。待测试的类是 MyNewClass 类(在这里显示为私有内部类)。同时,其 executeJob() 方法将接收 Collaborator 对象并运行 executeSomeImportantFunction() 方法。
清单 11 和 12 分别显示了 ClassA 和 ClassB 的代码。ClassA 是没有实现的简单类,而 ClassB 显示了阐明要点所需的最少细节。
清单 11. ClassA 类
public class ClassA{}
此类只是我使用的一个虚构类,用于强化一个要点:要模拟构造函数接收对象参数的类,有必要使用 RMock。
清单 12. ClassB 类
public class ClassB{ public ClassB(){} public String wierdMethod(){ return "failed"; } }
ClassB 类的 wierdMethod 将返回 failed。这是十分重要的,因为该类必须简短地返回另一个字符串才能使测试通过。
清单 13 显示了测试示例的最重要部分:Collaborator 类。
清单 13. Collaborator 类
public class Collaborator { private String _string; private ClassA _classA; private ClassB _classB; public Collaborator(String string, ClassA classA, ClassB classB) throws Exception{ _string = string; _classA = classA; if(classB.wierdMethod().equals("passed")){ _classB =classB; } else{ throw new Exception("Something bad happened"); } } public void executeSomeImportantFunction(){ } }
注,首要的是,使用 jMock 框架模拟了 ClassB 类。使用 RMock,没有一种实际方法从模拟对象中提取和使用代理,以便在测试 setUp() 方法中的其他位置使用该代理。使用 RMock,仅当调用 startVerification() 方法之后,才显示代理对象。本例中的优点是使用 jMock,因为在需要返回自我模拟对象的情况下,可以 获得设置其他模拟对象所需的信息。
反过来,需要注意的第二点是您不能使用 jMock 框架来模拟 Collaborator 类。原因是该类没有无参数构造函数。此外,在构造函数内有某种逻辑,这种逻辑将确定是否先获得实例。事实上,出于本次讨论的目的,ClassB 中的 wierdMethod() 方法必须返回 passed 才能使 Collaborator 对象被实例化。但是,另请注意,在默认情况下,方法总是返回 failed。测试成功明显需要模拟 ClassB。
此外,不同于先前的示例,此场景中的类数组作为附加参数被包含到了 intercept() 方法中。对此不作严格要求,但是用它作为密钥可以快速识别在实例化 RMock 测试对象时使用的对象类。
运行新测试用例。这一次,您将看到成功的结果。图 7 将显示令人愉快的结果。
图 7. RMock 与 jMock 协作使场景 4 测试成功
Collaborator 模拟对象已被正确设置,并且 mockClassB 对象将按预期执行。
快速查看测试工具差别
正如您已经在场景中看到的,jMock 和 RMock 都是用于测试 Java 代码的强大工具。但是,用于开发和测试的任何其他工具总是有限制。实际上,其他测试工具都是可用的,但是这些测试工具的运行情况都不如 RMock 和 jMock(在 Java 技术中)。个人经验告诉我 Microsoft® .NET 框架也附带了一些功能强大的工具(例如 TypeMock),但是那超出了本文(实际上还有平台)的范围。
表 1 显示了两个框架之间的一些不同之处以及随着时间的推移遇到的可能问题,尤其是在 Eclipse 环境中。
表 1. RMock 与 jMock 测试框架之间的不同之处
测试模拟样式 jMock RMock
可以模拟接口 是:新的 Mock() 方法 是:mock() 方法
可以模拟具体类 是:带有 CGLIB 的 mock() 方法 是:mock() 或 intercept() 方法
可以模拟任何具体类 否:无参数构造函数必须存在 是
可以随时获得代理 是 否:仅当 startVerification() 处于就绪状态后
使用其他Eclipse插件的问题无已知问题 是:与 Eclipse 的 CoverClipse 插件存在内存冲突
结束语
我鼓励您使用这些框架,利用它们的力量来生成单元测试的结果。许多 Java 开发人员不习惯于频繁编写测试。而且如果需要编写测试,通常都是十分简单、覆盖方法的主要功能目标的测试。要测试代码的某些 “难以达到的” 部分,jMock 和 RMock 都是优秀的选择。
使用 jMock 和 RMock 将极大地减少代码中的 bug,提高使用经过证明的方法测试编程逻辑的技巧。此外,阅读文档并用这些框架和其他框架的改进版本进行测试(并减少构造不好的代码)将对提高开发人员技巧有着额外的帮助。
发表评论
-
应用设计模式编写易于单元测试的代码
2008-07-24 15:14 926单元测试是软件开发的一个重要组成部分,通过在软件设计、 ... -
单元测试利器 JUnit 4
2008-07-24 14:44 2189本文主要介绍了如 ... -
使用 Spring 2.5 TestContext 测试框架
2008-07-24 14:27 2597Spring 2.5 TestContext 测试框架用 ... -
TestNG 使 Java 单元测试轻而易举
2008-07-24 13:31 1427JUnit 框架是 Java 语言单 ... -
再讨论spring的单元测试
2008-07-01 22:31 1021在我以前一篇文章关于s ... -
spring中的单元测试的策略
2008-07-01 22:30 1585本文主要介绍使用spring提供的对junit的扩展机制来进行 ...
相关推荐
### 使用JUnit Framework进行单元测试编写的关键知识点 #### 单元测试的重要性及JUnit Framework的地位 - **单元测试**:是软件开发中的一个重要环节,它通过独立地测试程序中的最小可测试单元(如函数或方法),...
在案例中,你将看到如何创建测试类,定义测试方法,设置测试数据,以及如何利用JUnit和Spring/Struts提供的工具来编写高效的单元测试。通过分析和运行源码,你可以了解如何将这些概念应用于实际项目,提高代码质量。...
JUnit是由 Erich Gamma 和 Kent Beck 编写的一个回归测试框架(regression testing framework)。Junit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的...
JUnit是Java编程语言中最常用的单元测试框架之一,它允许开发者编写可重复运行的测试用例,以确保代码的正确性和稳定性。JUnit3.8.1是该框架的一个较早版本,尽管现在已经有更新的版本(如JUnit5),但了解其基本...
单元测试是对程序中最小可测试单元进行检查和验证,通常是函数或方法。它的目的是确保代码按预期工作,减少缺陷,并且便于重构。通过单元测试,开发者可以快速定位问题,提高代码的可维护性。 Junit是一个开源的...
为了在用友NC框架中使用Junit进行单元测试,首先需要确保测试类继承了`nc.bs.framework.test.AbstractTestCase`。这是因为在用友NC框架中,所有的测试类都应该继承这个基类,以便能够访问到框架提供的各种资源和服务...
单元测试:程序猿编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试。 3 Junit单元测试框架的作用以及好处. 用来对类中的方法功能进行有目的的测试,以保证程序的正确性和稳定性。 能够让方法...
单元测试是对软件中的最小可测试单元进行检查和验证,如方法、函数或类。Junit提供了丰富的注解和断言功能,使得编写测试用例变得简单直观。 在Spring Boot项目中,我们可以使用`@RunWith(SpringRunner.class)`注解...
通过本文的学习,我们不仅了解了 JUnit 的基本概念和作用,还掌握了如何在 Eclipse 中使用 JUnit 进行简单的单元测试。掌握单元测试对于提高软件质量至关重要,而 JUnit 作为 Java 开发者常用的单元测试框架,其强大...
通过以上介绍,我们可以看到 JUnit 不仅提供了简洁的 API 进行单元测试的编写,而且支持多种运行方式,方便开发者根据不同场景选择合适的测试策略。在实际项目开发中合理运用 JUnit,可以帮助提高代码质量,减少后期...
JUnit 是一个广泛使用的 Java 语言的单元测试框架,而 JUnit4 是其一个重要的版本,提供了丰富的功能和改进,使得编写和执行单元测试变得更加简洁和高效。 在 JUnit4 中,测试类通常会继承自 `org.junit.Test` 类,...
总之,Spring整合JUnit进行单元测试是一个强大的组合,它能帮助开发者高效地编写高质量的代码。通过正确地配置和使用Spring提供的测试支持,我们可以确保每个组件都能独立、正确地工作,从而增强整个应用的稳定性和...
Junit单元测试是Java开发中的一个关键组成部分,它允许开发者对代码进行小规模的验证,确保各个函数或方法按照预期工作。在这个“junit单元测试示例”中,我们将深入探讨如何配置和执行一个基本的JUnit测试。 首先...
JUnit 是一个广泛使用的开源Java测试框架,专为编写和运行可重复的单元测试而设计。它遵循xUnit架构,该架构是为单元测试框架设立的一种设计模式。JUnit 的核心特性包括: 1. **断言(Assertions)**:用于验证测试...
使用`Junit`,开发者可以快速地编写出简洁、可读性强的测试代码,从而确保代码功能的正确性。 `dbunit` 是一个与`Junit`配合使用的扩展库,专注于数据库的单元测试。它允许开发者在测试前后填充或清理数据库数据,...
JUnit是Java编程语言中最常用的单元测试框架之一,它允许开发者编写可重复运行的测试用例,以确保代码的正确性和稳定性。单元测试是软件开发过程中的一个重要环节,它旨在对程序中的最小可测试单元——通常是函数或...