11
23

jMock Cookbook

作者: 舞命小丢 分类: java, 测试 0 comments

经过不懈的努力,jMock的Cookbook终于翻译完了.
很多人可能认为比较简单,没有必要翻译.
对于我个人来说,翻译过程是我对jMock更加深入的学习的过程.也是提高自己英语的过程.
也希望更多的人去关注这个测试框架,也对更多朋友有所帮助.

入门
定义期望
模拟方法的返回值
从模拟方法抛出异常
匹配参数值
精确指定期望参数匹配值
期望方法多于(少于)一次
期望调用顺序
期望一个调用在两个其他调用之间
忽略不相关的模拟对象
在测试的Set-Up中覆盖期望定义
匹配对象和方法
编写新的匹配器
编写新的行为
使用脚本轻松定义行为
使用模拟对象测试多线程代码
模拟泛型
模拟抽象的和实际类
使用非Java语言来使用jMock
更新jMock 1到jMock 2
在Maven构建中使用jMock
理解jMock 2中的方法调度
在Eclipse 插件测试中模拟类

jMock Cookbook 原文地址:

Get Started
Define Expectations
Return Values from Mocked Methods
Throw Exceptions from Mocked Methods
Match Parameter Values
Precisely Specify Expected Parameter Values
Expect Methods More (or Less) than Once
Expect a Sequence of Invocations
Expect [...]

11
23

测试多线程代码
一个问题频繁的在jMock用户的邮件列表被提问就是”我如何使用模拟对象测试一个大量产生新线程的类”? 问题是如果它们不在同一个线程比测试自己运行jMock会忽略不期望调用.当模拟对象收到一个不期望的调用,它实际会抛出一个AssertionFailedError,但是错误会终结当前线程,而非测试运行器线程.
这是一个例子.我们有一个guard和一个alarm.当guard通知(notice)burglar时,它应该仅敲响alarm一次.

public interface Alarm {
void ring();
}
 
@Test
public void ringsTheAlarmOnceWhenNoticesABurglar() {
final Alarm alarm = context.mock(Alarm.class);
Guard guard = new Guard(alarm);
 
context.checking(new Expectations() {{
oneOf (alarm).ring();
}});
 
guard.notice(new Burglar());
}

这是一个会使测试失败的Guard实现:

public class Guard [...]

11
23

jMock Cookbook 编写新的行为

作者: 舞命小丢 分类: java, 测试 1 comment

编写新的行为
JMock期望做两件事:测试它们收到的期望的方法调用和存根那些方法的行为.几乎所有方法都可以通过三个基本方法中的一个模拟:返回空(一个void方法),返回一个结果给调用或抛出一个异常. 然而有时,你需要存根一个方法的副作用,例如一个方法通过一个作为它的参数的一个被传递的对象引用回调给访问者. 幸运的是,jMock可以很容易为不常用的行为方法编写自定义的桩,并且这样做可以保持你测试的易读.
这是一个简单的例子:我们想测试一个FruitPicker对象,它从一个FruitTree对象集合中收集fruit.它会通过引用一个Collection给FruitTree的pickFruit方法来这么做. FruitTree通过添加它们的fruit到它们接收到的集合来实现pickFruit. FruitTree接口如下显示:

public interface FruitTree {
void pickFruit(Collection<Fruit> collection);
}

为了测试我们对象的行为我们将需要模拟FruitTree接口,并且我们特别需要存根pickFruit方法的副作用.jMock为返回值,迭代器和抛出异常提供了行为,并分组其他行为,但我们将必须编写我们自己的行为类来存根副作用.
来编写一个行为:
1. 编写一个类实现Action接口.这是一个接口,它添加一个元素到作为方法的第一个参数接收到的集合.

public class AddElementsAction<T> implements Action {
private Collection<T> elements;
 
public AddElementsAction(Collection<T> elements) {
this.element = elements;
}
 
public void describeTo(Description description) {
[...]

11
22

脚本自定义行为
在一个期望中使用自定义行为是很简单的,但是需要很多代码.你需要定义一个实现Action接口的类和一个使期望更易读的工厂方法.jMock脚本扩展让你使用BeanShell脚本在期望中内嵌定义自定义行为.
因为脚本表现为一个字符串,所以它不能和重构工具很好的配合.
首先,你需要添加下列jar文件到你的classpath:

jmock-script-2.5.1.jar
bsh-core-2.0b4.jar

然后,你需要导入perform工程方法到你的测试中.

import static org.jmock.lib.script.ScriptedCallbackAction.perform;

你可以然后使用perform工厂方法并使用BeanShell脚本定义自定义行为.这个脚本可以通过$0(第一个参数),$1,$2,等得到模拟方法的参数.例如,下面脚本回调给作为第一个(且仅一个)参数传递的Runnable.

checking(new Expectations() {{
oneOf (executor).execute(with(a(Runnable.class))); will(perform("$0.run()"));
}}

一个脚本可以传递原始值作为参数来回调.例如,下面脚本添加一个数字到集合:

checking(new Expectations() {{
oneOf (collector).collect(with(a(Collection.class))); will(perform("$0.add(2)"));
}}

如果你想脚本引用一个测试中定义的对象,你必须使用”where”语句定义一个脚本变量.在perform语句中可以在任何位置附加任何多个语句来定义变量.例如,来从自定义行为方法到使用BeanShell脚本转化代码,我们必须为被添加到集合的所有芒果列表定义一个脚本变量.

final FruitTree mangoTree = mock(FruitTree.class);
final Mango mango1 = new Mango();
final Mango mango2 = new Mango();
 
context.checking(new Expectations() {{
oneOf (mangoTree).pickFruit(with(a(Collection.class))); will(perform("$0.addAll(mangoes)")
.where("mangoes", Arrays.asList(mango1, [...]

11
22

匹配对象或方法
虽然匹配器通常用来指定可接受的参数值,但是它们也可以在期望中,使用类似jMock1的API用来指定可接受对象或方法. 为了做到这一点,在调用次数语句中你通常直接引用一个模拟对象的地方使用一个匹配器.然后链式语句一起来定义期望调用.支持下列语句:

method(m)
一个匹配被期望的匹配器m的方法

method(r)
一个方法,它的名字匹配被期望的正则表达式

with(m1, m2, … mn)
参数必须匹配m1 到 mn.

withNoArguments()
必须没有参数

这种形式的期望可以像普通期望一样跟随排序约束(顺序和状态)和行为(例如,返回值或抛出异常).
范例
来允许在任何模拟对象上的任何bean属性的getter调用:

allowing (any(Object.class)).method("get.*").withNoArguments();

允许一个方法在一个模拟对象的集合中任意一个上调用一次:

oneOf (anyOf(same(o1),same(o2),same(o3))).method("doSomething");

11
21

使用jMock和ClassImposteriser模拟类
因为jMock使用Java的默认反射能力,所以jMock框架的默认配置仅仅可以模拟接口,不能模拟类.(事实上,我们考虑那对于我们来说是件好事,因为它鼓励设计聚焦在对象之间的通信,而不是静态分类和数据存储.)然而ClassImposteriser扩展类使用CGLIB2.1和Objenesis类库来像接口一样创建类的模拟对象.当使用遗留代码工作时对于分解紧耦合类之间的依赖关系是很有用的.
ClassImposteriser创建模拟实例不需要调用模拟类的构造函数.所以带有有参数或调用可覆盖的对象方法的构造函数的类可以被安全的模拟.然而ClassImposteriser不能创建最终类或最终方法的模拟.
如果你想模拟最终类或者最终方法,JDave类库包含了一个反最终(unfinalizer)代理设备,它在JVM加载它们前能够使类变为非最终的. 它们然后就可以通过ClassImposteriser模拟.
来使用ClassImposteriser:
1. 添加jmock-legacy-2.5.1.jar, cglib-nodep-2.1_3.jar 和 objenesis-1.0.jar到你的CLASSPATH.
2. 导入ClassImposterise到你测试类r into the Mockery of your test class:
JUnit 3

import org.jmock.Expectations;
import org.jmock.integration.junit3.MockObjectTestCase;
import org.jmock.lib.legacy.ClassImposteriser;
 
public class ConcreteClassTest extends MockObjectTestCase {
{
setImposteriser(ClassImposteriser.INSTANCE);
}
 

}

JUnit 4

import org.jmock.Mockery;
import org.jmock.Expectations;
import org.jmock.integration.junit4.JUnit4Mockery;
import org.jmock.lib.legacy.ClassImposteriser;
 
@RunWith(JMock.class)
public class ConcreteClassTest {
private [...]

11
21

jMock Cookbook 模拟泛型

作者: 舞命小丢 分类: java, 测试 0 comments

模拟泛型
Java的泛型系统不能够和它的动态反射API很好的工作.对于jMock,这意味着当你模拟泛型时编译器会警告你可能静态类型错误.警告是不正确的.避免它们最好的方法是在模拟对象声明的变量上使用一个注释抑制警告.
例如,下列泛型接口:

public interface Juicer<T extends Fruit> {
Liquid juice(T fruit);
}

在一个测试中,你需要模拟这个接口,如下:

Juicer<Orange> orangeJuicer = context.mock(Juicer<Orange>.class, "orangeJuicer");
Juicer<Coconut> coconutJuicer = context.mock(Juicer<Coconut>.class, "coconutJuicer");

然而,依照java语法结构这是不正确的.你实际必须写成下列内容:

Juicer<Orange> orangeJuicer = (Juicer<Orange>)context.mock(Juicer.class, "orangeJuicer");
Juicer<Coconut> coconutJuicer = (Juicer<Coconut>)context.mock(Juicer.class, "coconutJuicer");

那些行,虽然在语句结构和语义上都是正确的,但是产生了安全警告.为了避免这些警告,你必须使用@SuppressWarnings注释变量声明:

@SuppressWarnings("unchecked")
Juicer<Orange> orangeJuicer = context.mock(Juicer.class, "orangeJuicer");
 
@SuppressWarnings("unchecked")
Juicer<Coconut> coconutJuicer = context.mock(Juicer.class, "coconutJuicer");

希望Java未来版本会提高泛型系统使这不是必须的.

11
16

编新的匹配器
jMock和Hamcrest提供了很多匹配器类和可以让你指定方法调用的可接受参数值的工厂函数.然而,有时预定义的约束不能让你指定一个期望足够精确表达你的意思或保持你的测试的灵活性.在这种情况下,你可以轻松地定义新的匹配器,可以无缝地扩展jMock中定义的一套存在的集合.
一个匹配器是一个实现了org.hamcrest.Matcher接口的对象.它做两件事情:

报告一个参数值是否满足约束(matches方法).
生成一个可读的表述来包含到测试失败消息中(describeTo方法从SelfDescribing接口继承).

来创建一个新的匹配器:
1.写一个类继承Hamcrest的BaseMatcher或TypeSafeMatcher基类.下列匹配器类测试一个字符串是否以一个给定的前缀开始.

import org.hamcrest.AbstractMatcher;
 
public class StringStartsWithMatcher extends TypeSafeMatcher<String> {
private String prefix;
 
public StringStartsWithMatcher(String prefix) {
this.prefix = prefix;
}
 
public boolean matchesSafely(String s) {
return s.startsWith(prefix);
[...]

11
16

在测试的Set-Up中覆盖期望定义
如果为了把测试下的对象放入已知的状态中你在一个测试的set-up中定义期望, 你有时想要在测试本身中忽略那些期望.例如, 你可能会在set-up中允许调用一些模拟对象,然后想要在测试中期望调用那些模拟对象.如果你在测试中仅仅定义附加期望, 在set-up中定义的允许期望优先考虑,测试期望将不会得到满足,因此测试失败.
在这种情况下,当期望有效时使用状态机来控制.这个状态机表现为测试状态,而不是在测试通信下的对象状态.

定义一个状态机,为它命名,例如:”test”.
在测试的set-up中,定义当”test”状态机不再”fully-set-up”状态中时期望为有效地.
在测试s的et-up最后,将”test”状态机移动到”fully-set-up”状态中.
和平时一样在你的测试中定义期望.

结果是在set-up中定义的仅应用于测试不在”running”中的期望将在测试本身中被忽略.
JUnit 3

public class ChildTest extends MockObjectTestCase {
States test = states("test");
 
Parent parent = mock(Parent.class);
 
// This is created in setUp
Child child;
 
@Override
public void setUp() {
[...]

11
16

状态
状态用于指定调用仅在一些状态通过其他期望调用起始或/和终止时会发生.一个测试可以定义多个状态机,一个调用在多个状态机的一个状态中可以约束发生.
定义新的状态机:
JUnit 3

final States state-machine-name = context.states("state-machine-name").startsAs("initial-state");

JUnit 4

final States state-machine-name = states("state-machine-name").startsAs("initial-state");

Other

final States state-machine-name = context.states("state-machine-name").startsAs("initial-state");

起始状态是可选的.如果没有指定,状态机会使用一个匿名的起始状态.
下列条款约束调用仅在明确的状态中发生,并定义了一个调用将如何改变状态机的当前状态.

when(state-machine.is(”state-name”));
约束最后一次期望调用仅当状态机是在命名状态中时发生

when(state-machine.isNot(”state-name”));
约束最后一次期望仅当状态机不再命名状态中时发生

then(state-machine.is(”state-name”));
当调用发生时改变状态机状态

例如:

final States pen = context.states("pen").startsAs("up");
oneOf (turtle).penDown(); then(pen.is("down"));
oneOf (turtle).forward(10); when(pen.is("down"));
oneOf (turtle).turn(90); when(pen.is("down"));
oneOf (turtle).forward(10); when(pen.is("down"));
oneOf (turtle).penUp(); then(pen.is("up"));

收藏 & 分享

Powered by 17fav.com