关闭

(三)利用 Eclipse 进行单元测试

发表于:2007-7-13 17:27

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:Michael Nyika    来源:IBM

场景 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 测试对象时使用的对象类。

 

《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号