总评
Mock 测试是一种常见的测试方法。通常在执行测试的时候,测试代码往往需要与一些真实对象进行交互,又或者被测代码的执行需要依赖真实对象的功能。此时,我们可以使用一个轻量级的、可控制的 Mock 对象来取代真实对象,模拟真实对象的行为和功能,从而方便我们测试。 jMock 便是这种方法的一种实现。
jMock 是一个利用 Mock 对象来测试 Java 代码的轻量级测试工具。毫不例外,它也是 xUnit 家族的一员,因为它从 JUnit 发展而来,是 JUnit 的一个增强库。 jMock 用法简单,易于掌握。利用它,我们可以很容易地快速构造出所需的 Mock 对象,从而得以方便快捷地编写单元测试代码,很适合测试驱动开发的流畅进行。
功能和特点
使用 jMock ,我们就不必像以往那样,停下测试代码的编写工作,转而去写专门的 Mock 对象。而且, jMock 允许你以一种十分灵活的方式来精确定义对象之间彼此交互的约束,从而更好地模拟和刻画对象间的调用关系。 jMock 的这种对象间调用关系的约束表达十分简洁和紧凑,这
使得测试代码的编写变得十分简洁,同时又能很好地利用 Mock 对象来达成测试意图。此外, jMock 也很容易扩展,我们可以很方便地添加自定义需求。 jMock 可以和既有的其他测试框架,如 JUnit ,很好地整合在一起,共同使用。
背景介绍
jMock 的官方网站上有关于该项目开发团队的人员介绍。目前具有提交权限的开发人员有 5 位,他们分别是: Steve Freeman TimMackinnon Nat Pryce Mauro Talevi JoeWalnes 。值得一提的是,这几位开发者几乎都是来自以敏捷实践见长的 ThoughtWorks 。其中, Steve Freeman Nat Pryce 共同参加了 2006 4 月在英国牛津举行的 ACCU Conference ,并做了主题演讲,内容是关于 jMock APIs 的演化,以及在 Java C# 领域,内嵌式 DSL (领域特定语言)的编写技术。
作为 jMock 项目主要开发者之一的 SteveFreeman ,是敏捷软件开发方面的独立咨询师,他还是英国地区极限编程实践的早期推广者。他与除 Mauro Talevi 外的另 3 jMock 作者共同撰写了一篇名为 “Mock Roles, not Objects” 的论文,探讨了有关 mock 测试技术方面的经验。这篇论文被收录于 2004 年的 OOSPA 论文集中,在 jMock 的官方主页可以找到该论文的电子版。除了 jMock 之外,几位开发者还开发了 jMock C# 实现版本 ——nMock
除了开发人员以外,还有一些 jMock 项目的贡献者,他们为项目提供建议、补丁及文档。不过, jMock 的在线文档资源并不是很丰富,好在 jMock 的代码简单而又精巧,因此有兴趣的读者不妨深入代码来一探究竟。
参考资料
网站类
jMock 的官方网站。在这里你可以找到 jMock 的最新下载版本,了解有关 jMock 的最新消息,还有相关的文档资源,告诉你如何用 jMock 来编写测试代码,如何掌握约束,以及与同类型 Mock 测试工具的对比。
EasyMock 的官方网站。这是一个与 jMock 有着类似功能的 Mock 测试框架。目前已经更新到了 2.3 版本,从 2.2.2 版本开始, EasyMock 除了支持对接口的模拟外,还支持对类的模拟。
 
一个消息发布与订阅系统的例子
此处,我们通过一个简单的示例来为读者示范 jMock 的使用方法。这是一个简化了的消息发布与订阅系统的例子,是典型的 Observer 模式,熟悉设计模式的读者对此一定不会陌生。我们用 Publisher 来代表消息发送方,用 Subscriber 来代表消息订阅方(即接收方)。以下是 Subscriber 接口的定义:
interface Subscriber {
void receive(String message);
}
一个 Publisher 可以将消息(此处以 String 类型的字符串对象来表达)发送给 0 个或多个 Subscriber 的具体实现类。而 Subscriber 的具体实现类则通过 Publisher 提供的接口向其注册。在本例中,我们旨在测试 Publisher 的执行逻辑,而不关心具体 Subscriber 的实现逻辑。为此,我们需要构造一个 Mock 对象用以模拟 Subscriber 的行为。然后将其注册到 Publisher 里。
首先,我们必须引入 jMock 的相关包,并构造一个 Mockery 对象。该对象是 jMock 提供 Mock 能力的统一入口,后面我们将利用它来模拟 Subscriber 的行为,并用它来检验 Publisher Subscriber 的模拟对象调用过程的正确性。
 import org.jmock.Expectations;
class PublisherTest extends TestCase {
Mockery context = new Mockery();
...
}
现在,我们来编写测试方法,该测试方法的测试场景是:由 Publisher 向注册其中的一个 Subscriber 实例发送一条消息。
public void testOneSubscriberReceivesAMessage() {
...
}
我们先利用 Mockery 实例来构造一个模拟的 Subscriber 对象,再构造一个 Publisher 对象,并将 Subscriber 注册其中,然后,我们再定义一则待发布的消息。
final Subscriber subscriber = context.mock(Subscriber.
class);
Publisher publisher = new Publisher();
publisher.add(subscriber);
final String message = "message";
紧接着,我们利用 Mockery 来为模拟的 Subscriber 对象定义 “Expectations”—— 指定 Publisher Subscriber 的交互规则。此处的 Expectations jMock 框架的一个概念。简言之, Expectations 是一组约束规则,它用于定义在测试运行期间,我们期望 Mock 对象接受调用的方式。例如在本例中,我们期望 Publisher 会调用 Subscriber receive 方法一次,并且 receive 方法会接收到一个 String 类型的 message 对象。有关 Expectations 的详细说明请见后文。
 context.checking(new Expectations() {{
one (subscriber).receive(message);
}});
Expectations jMock 的一大特色,是它有别于其他 Mock 测试工具的主要特征。 jMock 提供了一整套丰富而灵活、简洁而紧凑的 Expectations ,其表达形式也很接近自然语言。整个 Mock 对象的构造过程,即是利用一两行 Expectations 的定义来完成的。这是内嵌式 DSL 的一个典型应用,按 jMock 作者的说法, jMock Expectations Mock 测试这一特殊领域的 DSL 。接下来,我们开始调用 Publisher 的执行逻辑,并验证调用后的结果:
publisher.publish message ;
context.assertIsSatisfied();
此处,我们再次利用了 Mockery 实例,用以验证 Publisher Subscriber 的调用是否如期执行。假如调用并非如预期的那样,则测试会失败。
以下是完整的示例代码:
import org.jmock.Mockery;
import org.jmock.Expectations;
class PublisherTest extends TestCase {
Mockery context = new Mockery();
public void testOneSubscriberReceivesAMessage() {
// set up
final Subscriber subscriber = context.
mock(Subscriber.class);
Publisher publisher = new Publisher();
publisher.add(subscriber);
final String message = "message";
// expectations
context.checking(new Expectations() {{
one (subscriber).receive(message);
}});
// execute
publisher.publish(message);
// verify
context.assertIsSatisfied();
}
}
通过上面的示例,我们可以归纳出利用 jMock 进行 Mock 测试的一般过程,用伪代码整理如下:
... 创建 Mockery 对象 ...
public void testSomeAction() {
...
一些 set up 的工作 ...
context.checking(new Expectations() {{
...
此处定义 Expectations ...
}});
...
调用被测逻辑 ...
context.assertIsSatisfied();
...
执行其他断言 ...
}

Expectations
用法简介
jMock Expectations 具有如下结构:
invocation-count (mock-object).method(argumentconstraints);
inSequence(sequence-name);
when(state-machine.is(state-name));
will(action);
then(state-machine.is(new-state-name));
其中, mock-object 是事先构造好的 Mock 对象,如前例的 Subscriber ;而 method 则是即将接受调用的 Mock 对象的方法名称,如前例 Subscriber 接口的 receive 方法。除去 invocation-count mock-object 外,后续内容都是可选的。同时,你也可以根据实际需要,为某个 Expectation 追加多个 inSequence when will then 子句。
invocation-count
代表期望的方法调用次数, jMock 提供了表达方法调用次数的多种手段,如表 18-1 所示:
18-1 jMock 提供的方法调用次数的表达形式
 
argument-constraints
代表方法调用传入参数的约束条件,可以是精确匹配的条件,如下例所示, calculator add
法只期望接受两个整数 1 作为参数:
one (calculator).add(1, 1);
也可以利用 with 子句定义模糊匹配条件,同样是 calculator add 方法,在下例中则期望接受任意 int 类型的参数:
allowing (calculator).add(with(any(int.class)),
with(any(int.class)));
any 外, jMock 还提供了各种其他形式的参数约束子句,如表 18-2 所示:
18-2 jMock 提供的参数约束子句
 
 
will
代表方法调用返回情况的约束条件, jMock 提供的返回约束如表 18-3 所示:
18-3 jMock 提供的返回约束
 
 

inSequence
用于定义多个方法调用的执行顺序, inSequence 子句可以定义多个,其在测试代码中出现的次序,便是方法调用的执行顺序。为了定义一个新的顺序,首先需要定义一个 Sequence 对象,如下所示:
final Sequence sequence-name = context.sequence("sequencename");
而后,为了定义方法调用的执行顺序,可以在依序写好的每个 Expectation 后面添加一个 inSequence 子句。如下所示:
one (turtle).forward(10); inSequence(drawing);
one (turtle).turn(45); inSequence(drawing);
one (turtle).forward(10); inSequence(drawing);
when then
用于定义方法仅当某些条件为 true 的时候才调用执行,从而进一步对 Mock 对象的调用情况进行约束。在 jMock 中,这种约束是通过状态机的形式来达成的。首先,我们需要定义一个状态机实例,其中的初始状态( initial-state )是可选的:
final States state-machine-name =
context.states("state-machine-name").startsAs("initialstate");
然后,我们可以利用 when 子句来定义当处于某状态时方法被调用执行,利用 then 来定义当某方法被调用执行后,状态的迁移情况。举例如下:
final States pen = context.states("pen").startsAs("up");
one (turtle).penDown(); then(pen.is("down"));
one (turtle).forward(10); when(pen.is("down"));
one (turtle).turn(90); when(pen.is("down"));
one (turtle).forward(10); when(pen.is("down"));
one (turtle).penUp(); then(pen.is("up"));
 
jMock 的官方网站可以下载到当前的最新版本。目前,它的最新版本是 2007 8 月发布的 2.4.0 版。该版本引入了对 JUnit 4.4 的支持,并做了许多小的改进。
jMock 2006 年发布 1.2.0 版以后,其 API 组成有了较大的变化,进而对 jMock 的使用方法也产生了影响。目前,在 jMock 的官方网站上分别有 jMock 1 jMock 2 两个系列版本的下载,而相应的文档也各自有两份。可以认为, jMock 1 jMock 2 是两个并行独立的分支。可能这一点对于以往习惯了使用 jMock 1 系列版本进行 Mock 测试代码编写的开发者而言会有些困惑。不过,既然是两个并行独立的分支,并且目前也都已进入了 “Stable” 阶段,对于 jMock 的老用户而言,就无需担心因 jMock 版本升级而造成的代码不兼容问题了,因为我们仍然可以使用 jMock 1 ,而不必刻意升级到 jMock 2 。当然,如果是新启动的项目,那么使用 jMock 2 会是更好的选择。
社区视角
jMock 的使用可以给基于 Mock 技术的单元测试编写提供很大的便利性,利用它我们可以快速编写出 Mock 对象,进而对被测对象进行隔离测试。
其实,在 Mock 测试领域里,还有不少优秀的 Mock 测试框架,比如时常被人们与 jMock 相提并论的 EasyMock ,优秀的开源软件 Spring 中便使用了 EasyMock ,而 Spring 自身也提供了一组方便的 Mock 对象,这些 Mock 对象与 Spring 的发布包一起发布。 jMock 并非绝对优于其他同类软件,每个 Mock 测试框架都有其自身的特点,也各有优缺点。
此处,笔者摘选 jMock 作为介绍对象的主要原因,是其在 Mock 对象构造方面的独特方式。简洁而接近自然语言的表达形式,是 DSL 技术的一个有趣应用,同时也使得 Mock 对象的构造过程既简单又精确。此外,以往人们在使用 jMock 1 的时候,往往会抱怨 jMock 要求 TestCase 必须继承自 MockObjectTestCase 。而这一点在以单根继承为特征的 Java 语言里是很忌讳的,因为这样将阻碍测试用例继承其他的父类。不过,从上面的例子中大家已经看到, jMock 2 在这方面做了改进,不再对 TestCase 有特殊父类的限制,这应该说是一个很大的改善。
此外,在 Mock 技术的运用方面也需要有一个准确的把握,过多的使用 Mock 对象也可能会导致问题出现。当一个对象在测试之前需要构造过多的 Mock 对象时,当测试代码中构造 Mock 对象的代码逻辑占据了绝大多数篇幅时,往往意味着被测代码本身存在着设计上的问题。而像 jMock 这样的 Mock 测试工具的引入,使得 Mock 对象的构造变得十分容易,这往往也会助长代码中 “Bad Smell” 的蔓延:即便代码中存在过度的耦合也没有关系,因为似乎一切可以很方便地依赖于 Mock 。没有银弹,设计的缺陷是单纯的 Mock 技术所无法解决的,也不是它本该解决的,这属于技术的误用。
最后需要指出的是,虽然 jMock 提供了很多对 Mock 对象方法调用进行约束的表达手段,但是多数时候我们只需要用到其中的很小一部分即可。过多的对 Mock 对象方法调用的约束,也就意味着你需要对被测代码所依赖的其他对象有更多的依赖和认识,而一旦被依赖的对象面临重构,则往往会导致相关测试用例的失败。有时候,修复这些失败的测试用例往往是一件很繁琐的事情,这对 TDD 和重构的流畅实践是一大障碍。