最近一直在着力推动部门单元测试工作,发现一些不规范的地方,特此尝试翻译下用户指南,方便大家参考。
原文参考:http://code.google.com/p/jmockit/ 及 http://jmockit.googlecode.com/svn/trunk/www/tutorial/BehaviorBasedTesting.html
- 前言:开发者可以借助JMockit这个强大的测试辅助工具,利用mock 的API去解决平常在单元(或者集成) 测试中遇到的常见的可测试性问题。开发者同样可以很方便快捷的对final类、静态方法或者构造函数等进行mock模拟。在Jmcokit的世界中,没有 其他任何的限制。
Jmockit的API是极致简单、易用的。在编写测试代码时,不需要在测试代码中嵌入一些特定的方法或者注解,当然不包括一些必须的、有意义的方法和注解(还是有一定的必要).一般而言,使用JMockit的mock API可以使测试代码更结构化,可读性变得更强。public void testDoOperationAbc(final DependencyXyz mock) { new NonStrictExpectations() { AnotherDependency anotherMock; { anotherMock.doSomething("test"); result = 123; } }; new ServiceAbc().doOperation("some data"); new Verifications() {{ mock.complexOperation(true, anyInt, null); times = 1; }}; }Mocked types
在Jmockit的工具箱中,Expectations 期望和Verifications 验证API对基于行为的单元测试提供了丰富的支持。这类型的测试的焦点是当前的单元测试代码如何和其他相依赖的单元进行交互。这些行为单元可以是定义在生 产环境中的class类,method方法,以及构造函数。
两个单元之间的交互通常以方法或者构造函数调用的形式出现。以参数和返回值的形式在当前单元和被依赖的单元之间传递构成的调用集合,定义了我们感兴趣的测 试代码行为。另外,对于给定的一个测试,可能需要判定调用集合在执行期间的执行顺序。注意,一个单元测试应该只是执行可测试的单元代码。这些实现所依赖的 关系不一定需要被执行,因为这些依赖并不是当前单元测试的一部分。所以,单元测试的目标应该是测试一些业务逻辑,这些逻辑是和真实行为需要依赖的外部关系 或者环境是隔离的。现在,对于给定的单元,我们一般不需要隔离所有的依赖,但需要隔离如下这些:1)一些依赖单元本身已经(或者将来会有,只是目前还没有 实现而已)拥有自己的单元测试。2)由于一些特殊原因,在测试环境中,一些并不是很容易快速的执行的单元(因为它们可能会写数据库或者发送邮件等等)。当 我们编写一个特殊的单元测试时,我们一般假设它对外部的依赖是满足一些期望的。如果不是的话,那就是外部的依赖本身所需要负的责任了,并应该满足这些期 望。
- Mocked 类型
一般来说,在单元测试代码中,方法调用、构造函数以及那些相互依赖 /协助的单元都是我们mock的目标。 Mock技术提供了一种隔离外部依赖的可测试的单元机制。我们通过使用mocked声明,可以在指定的测试代码对一些特殊的依赖进行 mock模拟,也就是说,一个mocked类型,应该是单元测试中的一个依赖类型,这些类型可以是引用、接口、抽象类、具体的类、final 类等等。
默认情况下,被mock的类型的所有方法在测试期间都被 mock实现。如果一个mock类型被声明为类,那么除了java.lang.Object之外,该类的父类将被递归mock。因此,继承的方法也将自动 被mock。同样,对于声明为类的mock类型,其所有构造函数也将被 mock。甚至,无论方法或者构造函数的修饰符是否是private,stati,final,native等,这些方法和构造函数都会被mock掉,对 于mock类型来说,修饰符的定义变得如此不重要了。
在一个测试调用中,如果当一个方法或者构造函数被mock了,则其原始的实现代码将不会被执行,取而代之的是,可以通过 jmockit显式或者隐式指定测试调用。下面的示例代码,展示了一个mock类型的基本调用图。在这个指南中,我们通常使用类似这样的代码。当然,粗体 部分的注释才是我们现在的重点。
@Test
public void doBusinessOperationXyz()
{
...
new Expectations() // 一个称为期望的结构块
{
Dependency mockInstance; // "Dependency"是我们需要mock的类型
...
{
...
// "mockInstance" 是一个mock实例,在测试代码中自动提供如下类型的mock使用方法
mockInstance.mockedMethod(...);
...
}
};
...
}
就如我们即将在下面看到的一样,这里提供了好几种方式来声明mock类型,同样提供几种特殊修饰语法以满足一些特殊的测试mock。但大多数情况 下,例如变量都是通过一些特定的注解及注解本身的属性来指定mock,例如 @Mocked, @NonStrict, @Injectable等等。这些注解标记可以在实例的域或者测试方法的参数中使用。
- Expectations(期望)
Expectations是一个给定的单元测试相关的mock方法/构造函数的调用集合。对于一个同样的方法或者构造函数,一个Expectations 可能会覆盖到多个不同调用,但是它不需要(不一定)覆盖到单元测试方法执行期间的所有调用(invocations)。一个特定的调用是否匹配给定的 expectation,不仅依赖方法/构造函数的签名,而且依赖运行时方面参数(aspects),例如被调用的方法类实例、参数值以及调用次数等等。 因此,对于给定的expectation,可以(可选)指定几种不同类型的匹配约束。
对于具有一个或者多个参数调用的情况,可能需要对每一个参数指定精确的参数值。例如,"test string"可以指定为一个字符串参数,从而匹配到那些具有相应的精确的参数调用的expectation。正如我们后面将所见到的一样,我们可以通过 指定更多相关的约束来匹配一个具有不同参数值的集合,而不是指定精确的参数值。
下面的例子展示了类Dependency的方法someMethod(int, String)的一个期望(expectation ),这个期望是通过指定严格(精确)的参数值来匹配这个方法的调用。值得注意的是,对于这个mock方法,这个期望本身就是一个特定的隔离调用。在测试代 码中,没有什么特殊的API的参与其中。但是,在我们所感兴趣的测试中,这个调用不能认为是一个真实的调用。它在这里的目的唯一作用是可以用于指定 expectation而已。
@Test
public void doBusinessOperationXyz()
{
...
new Expectations()
{
Dependency mockInstance;
...
{
...
// 一个mock实例的mock方法
mockInstance.someMethod(1, "test");
...
}
};
//这里应该是一个单元测试方法调用,这个调用可能匹配(也可能不匹配)上面指定的expectations
}
在之后我们理解了recording、 replaying、 verifying调用的差异后,我们将看到更多的 expectations。
- Record-Replay-Verify模型
任何一个测试至少可以划分三个相互独立的阶段,这些阶段将按顺序执行,一次执行一个阶段,例如下面的示例:@Test public void someTestMethod() { // 1. 准备阶段:测试执行之前所需要的所有东西都可以编写在这里 ... // 2. 单元测试代码在这里执行,通常是通过调用public方法来执行 ... // 3. 验证阶段:验证上面所执行的单元测试代码真正执行了其业务逻辑 ... }
首先,我们有一个准备阶段,单元测试代码所需要的对象和数据都可以在这里创建或者从其他地方加载进来。之后,测试的代码被执行。最后,测试结果和期望结果进行比较。
这个著名的三阶段也叫做Arrange, Act, Assert 语法,或者简称为"AAA",不同的词语而已,意思实际就是一样。
在基于行为的测试上下文中,通过使用 mock类型(以及他们对应的mock实例),我们可以辨别出来下面的交互流程直接对应上面描述的哪些测试阶段。
(1) Record(记录)阶段:在这里将记录下哪些调用会被期望执行,这些都是发生在测试的准备阶段,而且在真正测试代码执行调用之前。
(2) Replay(重播)阶段:我们感兴趣的mock 调用将在这里有机会被执行,就好像真正的单元测试代码被执行一样。这些在Record阶段记录下来的 mock方法 /构造函数调用将在这里重播(执行),尽管这些mock调用在record和replay阶段通常不是一对一对应的。
(3)Verify(验证)阶段:所有的真实调用应该在这里和期望值进行校验,这些动作发生在测试验证阶段。
利用JMockit工具来编写基于行为的测试代码,通常符合下面的经典模板:
import mockit.*;
... other imports ...
public class SomeTest
{
// 零个或者更多的mock 属性, 这些属性对于整个类的所有测试方法来说是通用的。
@Mocked Collaborator mockCollaborator;
@NonStrict AnotherDependency anotherDependency;
...
@Test
public void testWithRecordAndReplayOnly(mock parameters)
{
// 如果这里需要测试前的准备,可以在这里执行,但对于Jmockit 来说,对此没特别要求。当然这里也可以为空。
new Expectations() // 一个期望块
{
// 零个或者多个局部 mock 属性域
{
// 一个或者多个mock对象(类型)的调用,这些调用会被Expectations记录(Recorded)下来
//一些没有被mock的方法、对象类型等同样可以在这个期望块里面调用
}
};
// 单元测试代码真正业务逻辑在此执行
// 如果需要,可以在这里进行验证代码编写,当然可以利用JUnit/TestNG 断言
}
@Test
public void testWithReplayAndVerifyOnly(mock parameters)
{
// 如果这里需要测试前的准备,可以在这里执行,但对于Jmockit 来说,对此没特别要求。当然这里也可以为空。
// 单元测试代码真正业务逻辑在此执行
new Verifications() {{ // 一个验证块
// 一个或者多个mock对象(类型)的调用,这些调用用于验证结果是否正确
//一些没有被mock的方法、对象类型等同样可以在这个验证块里面调用
}};
// 如果需求,这里可以添加其他额外的验证代码,
// 当然,这些验证可以编写在这里,也可以在Verifications块之前
}
@Test
public void testWithBothRecordAndVerify(mock parameters)
{
//如果这里需要测试前的准备,可以在这里执行,但对于Jmockit 来说,对此没特别要求。当然这里也可以为空。
new NonStrictExpectations() { // 同样是一个期望块
//零个或者多个局部 mock 属性域
{
// 一个或者多个mock对象(类型)的调用,这些调用会被Expectations记录(Recorded)下来
}
};
// 单元测试代码真正业务逻辑在此执行
new VerificationsInOrder() {{ // 同样是一个验证块
// 一个或者多个mock对象(类型)的调用,这些调用将期望按照特定的顺序进行比较。
}};
// 如果需求,这里可以添加其他额外的验证代码,
// 当然,这些验证可以编写在这里,也可以在Verifications块之前
}
在上面的模板中,还存在一些其他额外的变量,但是从本质上来说,这些expectation 期望块是属于Record阶段的,而且在单元测试真正业务逻辑执行前被执行,当然,自然而然,verification 验证块是属于验证阶段的。一个测试方法可以包含任意数量(含零个)的期望块,验证块也是一样。
事实上,我们可以利用现代JAVA IDE工具的高级功能"代码折叠",对匿名内部类进行折叠展示。下面图片显示了再IntelliJ IDEA中的折叠效果: