UML软件工程组织

 

 

利用 Eclipse 进行单元测试
 
Michael Nyika (mnyika@gmail.com), 软件工程师, MichaelDKelly.com
 
  本文内容包括:

您的传统代码是不是要求使用匹配的类测试套件才能针对其源代码库运行?针对此类目的,jMock 堪称是一个优秀的测试框架。但是,并不是所有情况都能够适用,尤其是必须以 jMock 不期望的方式构造对象时。为避免生成自定义模拟对象套件才能支持应用程序中的单元测试的麻烦,可以调整 RMock,与 jMock 无缝地结合使用,从而解决这一问题。

 模拟对象将模仿出于指导代码执行的惟一目的而编写的类的行为,以便它在测试时符合代码执行要求。最终,模拟对象数目可以随着应用程序类数目的增长而增长。使用 jMock、RMock 甚至 EasyMock 等框架有助于消除对物理的独立存在的模拟对象集的需求。

EasyMock 框架的一个主要缺点是不能模拟具体类 —— 而只能模拟接口。在本文中,我将向您展示怎样使用 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);
}
}

如果将在各种测试用例中执行公共操作,则在测试中包括 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)
.....
.....

失败原因是 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。

在 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 环境中。

结束语

我鼓励您使用这些框架,利用它们的力量来生成单元测试的结果。许多 Java 开发人员不习惯于频繁编写测试。而且如果需要编写测试,通常都是十分简单、覆盖方法的主要功能目标的测试。要测试代码的某些 “难以达到的” 部分,jMock 和 RMock 都是优秀的选择。

使用 jMock 和 RMock 将极大地减少代码中的 bug,提高使用经过证明的方法测试编程逻辑的技巧。此外,阅读文档并用这些框架和其他框架的改进版本进行测试(并减少构造不好的代码)将对提高开发人员技巧有着额外的帮助。



 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号