期望方法多于(少于)一次
入门仅演示了期望对一个模拟对象的一个调用.测试经常需要使用不同基准的期望来允许一些方法调用发生,但如果不发生不会失败,期望方法被调用多次或不是全部,或忽略不相关的拟对象.
一个期望的调用次数定义了期望的这个方法允许被调用最小和最大次数.它在期望中的模拟对象前被指定.
invocation-count (mock).method(parameters); ...
JMock定义了下列基准:
one | 调用期望一次且仅一次. |
exactly(n).of | 调用期望正是n次.注意:one是exactly(1)方便的表达. |
atLeast(n).of | 调用期望至少n次. |
atMost(n).of | 调用期望最多n次. |
between(min, max).of | 调用期望至少min次,最多max次. |
allowing | 调用允许任何次但不必须发生. |
ignoring | 和allowing相同. Allowing和ignoring应该被选择来使测试代码清晰表达意图. |
never | 调用根本不期望.这会使测试更加明确和易懂. |
Expecting vs. Allowing
expecting和allowing一个调用时最重要的区别是基准表达.
如果一个调用是允许的,在测试中它可以发生,但如果调用不发生,测试依然通过.如果一个调用被期望,则相反,它必须在测试中发生,如果它不发生,测 试失败.当定义期望时你必须选择是期望还是允许每个调用.总的来说,我们发现如果你允许查询和期望命令测试可以保持灵活.一个查询是除了查询对象状态没有 任何副作用的方法,一个命令是可以返回也可以不返回结果有副作用的方法. 当然,这并不适用于所有情况.如果你在测试中期望的方法被调用的次数没有其它约束遵循经验方法是有用的.
期望调用顺序
顺序常用来定义期望必须按照它们在测试代码中出现的顺序调用.一个测试可以创建多个顺序,一个期望可以依次是多个顺序的部分.
定义一个新的顺序:
JUnit 3
final Sequence sequence-name = sequence("sequence-name");
JUnit 4
final Sequence sequence-name = context.sequence("sequence-name");
Other
final Sequence sequence-name = context.sequence("sequence-name");
来期望一个调用顺序,按顺序编写期望并添加inSequence(sequence)语句到每一个期望.例如:
oneOf (turtle).forward(10); inSequence(drawing);
oneOf (turtle).turn(45); inSequence(drawing);
oneOf (turtle).forward(10); inSequence(drawing);
在顺序中的期望可以有任何调用次数.如果一个顺序中的一个期望被允许而不是被期望,它可以在顺序中被忽略.
状态
状态用于指定调用仅在一些状态通过其他期望调用起始或/和终止时会发生.一个测试可以定义多个状态机,一个调用在多个状态机的一个状态中可以约束发生.
定义新的状态机:
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"));
忽略不相关的模拟对象
下列期望在测试期间忽略模拟对象fruitbat.
ignoring (fruitbat);
测试期间fruitbat的任何方法可以被调用任何次数,或者根本不被调用.
当一个忽略对象在测试期间被调用,它会返回一个”零”值,这依赖于调用方法的返回类型:
返回类型 | “零”值 |
---|---|
boolean | false |
数字 | 0 |
字符串 | “” (空串) |
数组 | 空数组 |
模拟类型 | 被忽略的模拟 |
任何其他类型 | null |
一个使用模拟返回值类型的方法会返回另一个忽略模拟对象,这使忽略期望非常强大.返回对象会也会返回”零”值,任何它返回的模拟值也会被忽略,任何它们返回的模拟值将被忽略,等等.这就允许你聚焦测试在一个对象的功能的不同方面,并忽略不相关的合作者.
例如,下列声明忽略Hibernate的 SessionFactory,和从getCurrentSession()方法得到当前会话,和从Transaction的 beginTransaction(),commit()和rollback()方法返回的Transaction.所你可以指定这个对象的这个行为不依 赖于它如何配合事务.并在其他测试中测试事务管理.
ignoring (hibernateSessionFactory);
在测试的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
1 public class ChildTest extends MockObjectTestCase { 2 States test = states("test"); 3 4 Parent parent = mock(Parent.class); 5 6 // This is created in setUp 7 Child child; 8 9 @Override 10 public void setUp() { 11 checking(new Expectations() {{ 12 ignoring (parent).addChild(child); when(test.isNot("fully-set-up")); 13 }}); 14 15 // Creating the child adds it to the parent 16 child = new Child(parent); 17 18 test.become("fully-set-up"); 19 } 20 21 public void testRemovesItselfFromOldParentWhenAssignedNewParent() { 22 Parent newParent = context.mock(Parent.class, "newParent"); 23 24 checking(new Expectations() {{ 25 oneOf (parent).removeChild(child); 26 oneOf (newParent).addChild(child); 27 }}); 28 29 child.reparent(newParent); 30 } 31 }
JUnit 4
1 @RunWith(JMock.class) 2 public class ChildTest { 3 Mockery context = new JUnit4Mockery(); 4 States test = mockery.states("test"); 5 6 Parent parent = context.mock(Parent.class); 7 8 // This is created in setUp 9 Child child; 10 11 @Before 12 public void createChildOfParent() { 13 mockery.checking(new Expectations() {{ 14 ignoring (parent).addChild(child); when(test.isNot("fully-set-up")); 15 }}); 16 17 // Creating the child adds it to the parent 18 child = new Child(parent); 19 20 test.become("fully-set-up"); 21 } 22 23 @Test 24 public void removesItselfFromOldParentWhenAssignedNewParent() { 25 Parent newParent = context.mock(Parent.class, "newParent"); 26 27 context.checking(new Expectations() {{ 28 oneOf (parent).removeChild(child); 29 oneOf (newParent).addChild(child); 30 }}); 31 32 child.reparent(newParent); 33 } 34 }
Other
public class ChildTest extends TestCase { Mockery context = new Mockery(); States test = context.states("test"); Parent parent = context.mock(Parent.class); // This is created in setUp Child child; @Override public void setUp() { context.checking(new Expectations() {{ ignoring (parent).addChild(child); when(test.isNot("running")); }}); // Creating the child adds it to the parent child = new Child(parent); test.become("fully-set-up"); } public void testRemovesItselfFromOldParentWhenAssignedNewParent() { Parent newParent = context.mock(Parent.class, "newParent"); context.checking(new Expectations() {{ oneOf (parent).removeChild(child); oneOf (newParent).addChild(child); }}); child.reparent(newParent); } }
匹配对象或方法
虽然匹配器通常用来指定可接受的参数值,但是它们也可以在期望中,使用类似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");
编新的匹配器
jMock和Hamcrest提供了很多匹配器类和可以让你指定方法调用的可接受参数值的工厂函数.然而,有时预定义的约束不能让你指定一个期望足够精确表达你的意思或保持你的测试的灵活性.在这种情况下,你可以轻松地定义新的匹配器,可以无缝地扩展jMock中定义的一套存在的集合.
一个匹配器是一个实现了org.hamcrest.Matcher接口的对象.它做两件事情:
- 报告一个参数值是否满足约束(matches方法).
- 生成一个可读的表述来包含到测试失败消息中(describeTo方法从SelfDescribing接口继承).
来创建一个新的匹配器:
1.写一个类继承Hamcrest的BaseMatcher或TypeSafeMatcher基类.下列匹配器类测试一个字符串是否以一个给定的前缀开始.
1 import org.hamcrest.AbstractMatcher; 2 3 public class StringStartsWithMatcher extends TypeSafeMatcher<String> { 4 private String prefix; 5 6 public StringStartsWithMatcher(String prefix) { 7 this.prefix = prefix; 8 } 9 10 public boolean matchesSafely(String s) { 11 return s.startsWith(prefix); 12 } 13 14 public StringBuffer describeTo(Description description) { 15 return description.appendText("a string starting with ").appendValue(prefix); 16 } 17 }
2.编写一个良好命名的工厂方法,这个方法创建一个你的新匹配器的实例.
1 @Factory 2 public static Matcher<String> aStringStartingWith( String prefix ) { 3 return new StringStartsWithMatcher(prefix); 4 }
工厂方法的这点是使测试代码阅读清楚,因此考虑当在一个期望中使用时它看起来将如何使用.
3.使用你的工厂方法来在你测试中创建匹配器.下列期望指定logger对象的error方法必须被使用一个以”FATAL”开始的字符串为参数被调用一次.
1 public class MyTestCase { 2 ... 3 4 public void testSomething() { 5 ... 6 7 context.checking(new Expectations() {{ 8 oneOf (logger).error(with(aStringStartingWith("FATAL"))); 9 }}); 10 ... 11 } 12 }
一个重要的规则
匹配器对象必须是无状态的.
当调度每个调用时,jMock使用匹配器来发现一个匹配调用参数的期望. 这就意味着测试期间它将调用匹配器多次,甚至在期望已匹配和调用后.事实上,jMock对它将什么时候会调用匹配器调用多少次没有提供保证.这不影响无状 态的匹配器,但意味着有状态的匹配器函数无法预测.
如果你想维护调用响应的状态,使用行为,而不是匹配器.
重要信息
关于如何写匹配器的更多信息可以从Hamcrest项目获得.
同是可以参看我翻译的Hamcrest指南
编写新的行为
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接口.这是一个接口,它添加一个元素到作为方法的第一个参数接收到的集合.
1 public class AddElementsAction<T> implements Action { 2 private Collection<T> elements; 3 4 public AddElementsAction(Collection<T> elements) { 5 this.element = elements; 6 } 7 8 public void describeTo(Description description) { 9 description.appendText("adds ") 10 .appendValueList("", ", ", "", elements) 11 .appendText(" to a collection"); 12 } 13 14 public Object invoke(Invocation invocation) throws Throwable { 15 ((Collection<T>)invocation.parameterValues.get(0)).addAll(elements); 16 return null; 17 } 18 }
2.在实例化你的新行为类的地方编写一个工厂方法,并导入它到你的测试中.你可以使这个工厂方法为一个public static方法,或者可以把所有你自己的工厂方法放入一个单独的类使更容易导入.工厂方法不必须是一个静态导入.如果在一个测试中需要它,你可以编写它 作为测试的一个方法.
1 public static <T> Action addElements(T... newElements) { 2 return new AddElementsAction<T>(Arrays.asList(newElements)); 3 }
3.传递工厂方法结果给will方法.
1 final FruitTree mangoTree = mock(FruitTree.class); 2 final Mango mango1 = new Mango(); 3 final Mango mango2 = new Mango(); 4 5 context.checking(new Expectations() {{ 6 oneOf (mangoTree).pickFruit(with(any(Collection.class))); 7 will(addElements(mango1, mango2)); 8 }}); 9 10 ...
脚本自定义行为
在一个期望中使用自定义行为是很简单的,但是需要很多代码.你需要定义一个实现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, mango2));
}});
...
测试多线程代码
一个问题频繁的在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 { private Alarm alarm; public Guard( Alarm alarm ) { this.alarm = alarm; } public void notice(Burglar burglar) { startRingingTheAlarm(); } private void startRingingTheAlarm() { Runnable ringAlarmTask = new Runnable() { public void run() { for (int i = 0; i < 10; i++) { alarm.ring(); } } }; Thread ringAlarmThread = new Thread(ringAlarmTask); ringAlarmThread.start(); } }
虽然那个实现是不正确的,但那时测试通过,因为模拟的Alarm会抛出一个AssertionFailedError在ringAlarmThread线程上,而是测试运行器线程.
问题的根本是对于继承测试尝试使用模拟对象.模拟对象一般用来单元测试:从系统的部分中孤立的测试单元.然而,线程由于它们的天性,需要一些集成测试的性质.并发和同步是系统范围的事物,并且创建线程的代码必须利用操作系统的能力这样做.
一个解决方案是从任务运行的细节中分离出需要运行任务的对象,并在它们之间建立接口.我们可以通过模拟任务运行器来测试需要运行任务的对象.Java标准的并发类库仅定义了一个这样的接口:java.util.concurrent.Executor.在我们的例子中,我们可以通过传递一个Executor给Guard,并改变Guard,以至于它访问executor来运行任务替代显示创建新的线程.
我们的测试然后看起来是这样的:
@Test public void ringsTheAlarmOnceWhenNoticesABurglar() { final Alarm alarm = context.mock(Alarm.class); Executor executor = ... // What goes here? Guard guard = new Guard(alarm, executor); context.checking(new Expectations() {{ oneOf (alarm).ring(); }}); guard.notice(new Burglar()); }
但是我们如何实现我们测试的Executor呢?如果我使用Executor创建一个线程,那我们就又回到了我们开始的地方,并且测试依然错误的通过. 我们需要运行任务在同一个线程作为测试运行器.为了这样做,我们可以模拟Executor接口,并使用一个自定义行为来回调任务的run方法.然而,那并不正确的反应后台任务如何工作:任务执行在notify方法返回前,替代以后.当一个任务修改对象状态时这个差异是意义重大的.
我们需要一个实现Executor接口的对象,并且在notice(Burglar)方法返回之后让我们显式的运行任务.jMock提供了一个仅做这些的类:DeterministicExecutor.使用它,我们的测试看起来是这样的:
@Test public void ringsTheAlarmOnceWhenNoticesABurglar() { final Alarm alarm = context.mock(Alarm.class); DeterministicExecutor executor = new DeterministicExecutor(); Guard guard = new Guard(alarm, executor); guard.notice(new Burglar()); context.checking(new Expectations() {{ oneOf (alarm).ring(); }}); executor.runUntilIdle(); }
然后一个使用Executor的Guard实现看起来像这样:
public class Guard { private Alarm alarm; private Executor executor; public Guard(Alarm alarm, Executor executor) { this.alarm = alarm; this.executor = executor; } public void notice(Burglar burglar) { startRingingTheAlarm(); } private void startRingingTheAlarm() { Runnable ringAlarmTask = new Runnable() { public void run() { for (int i = 0; i < 10; i++) { alarm.ring(); } } }; executor.execute(ringAlarmTask); } }
它依然是不正确的.但是至少现在我们的测试可以发现错误.
容易的测试更深处,从Guard剥离Executor的好处是在它的实现中隐藏并发策略.并发是一个系统的问题并且应该在Guard对象外控制.通 过传递一个合适的Executor给它的构造函数,应用程序可以容易的适配Guard给应用程序线程策略,而不必改变任何Guard的实现.
然而这个技术仅测试对象的功能行为.它不测试同步.你将还需要集成测试,把实际并发代码放到负载下,并且尝试发现同步错误.然而通过分离功能和同步问题你将知道在你的负载测试中的任何同步问题导致的错误,而不是在功能行为的bug.