The jMock Cookbook简译

The jMock Cookbook简译

1.Set Up the Class Path

在这个简单的例子中,我们开始为一个发布订阅系统写一个mock测试用例。在这个系统中,一个消息发送者(Publisher)可以将消息发送给0或多个订阅者(Subscriber)。我们开始为发送者写一个测试用例。

订阅者接口如下:

interface Subscriber {
    void receive(String message);
}

我们下面会测试一个订阅者的情况,测试时需要先mock一个订阅者。

设置classpath

为了使用jMock 2.6.1,你需要将下面的jar文件添加到class path中:

  • jmock-2.6.1.jar
  • hamcrest-core-1.3.jar
  • hamcrest-library-1.3.jar
  • jmock-junit4-2.6.1.jar
  • Write the Test Case

首先我们导入jMock的类,定义一个测试用例并创建一个用于mock对象的context。context用于 mock Publisher依赖的对象并检查mock的对象是否正确执行。

import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.integration.junit4.JMock;
import org.jmock.integration.junit4.JUnit4RuleMockery;

public class PublisherTest {
    @Rule public JUnitRuleMockery context = new JUnitRuleMockery();
    ...
}

注意:上面的代码只能工作在最近的jMock候选版本(2.6.0RC1)和JUnit 4.7 及以后的版本。

在老版本的jMock和JUnit 4中你可以使用JMock test runner,这种用法没有上面的例子使用Rules灵活。

import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.integration.junit4.JMock;
import org.jmock.integration.junit4.JUnit4RuleMockery;

@RunWith(JMock.class)
public class PublisherTest {
    Mockery context = new JUnit4Mockery();
    ...    
}

写测试用例的执行方法:

@Test
public void oneSubscriberReceivesAMessage() {
    ...
}

首先我们在测试类中创建context类,然后创建一个Publisher对象,然后mock一个Subscriber对象用来接收消息,再然后将Subscriber注册到Publisher中。最后我们创建一个用于发布的消息message。

final Subscriber subscriber = context.mock(Subscriber.class);

Publisher publisher = new Publisher();
publisher.add(subscriber);

final String message = "message";

接下来我们为Subscriber的方法定义期望:期望receive方法被调用并且调用时有一个参数。

context.checking(new Expectations() {{
    oneOf (subscriber).receive(message);
}});

接下是我们要是测试的方法.

publisher.publish(message);

测试完成后测试用例必须验证Subscriber对象的调用是否与预期一致,不一致则测试失败。我们不需要写额外的代码验证mock对象是否验证通过,JMock test runner会自动验证。

这里是完整的测试代码:

import org.jmock.integration.junit4.JMock;
import org.jmock.integration.junit4.JUnit4Mockery;
import org.jmock.Expectations;

@RunWith(JMock.class)
public class PublisherTest {
    Mockery context = new JUnit4Mockery();

    @Test
    public void oneSubscriberReceivesAMessage() {
        // 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() {{
            oneOf (subscriber).receive(message);
        }});

        // execute
        publisher.publish(message);
    }
}

附Publisher代码:

public class Publisher {
    private List<Subscriber> sList = new LinkedList<>();

    public void publish(String message) {
        sList.forEach(subscriber -> subscriber.receive(message));
    }

    public void add(Subscriber subscriber) {
        sList.add(subscriber);
    }

}

2.Automagically Creating Mocks, States & Sequences

使用@Mock注解可以自动生成一个mock对象,避免样板化的代码.

@Mock Cheese cheddar;
@Mock Cheese jarlsberg;

对使用了@Auto注解的State和Sequence对象,jMock会在测试用例执行前自动实例化对象

@Auto States pen;
@Auto Sequence events;

3.Specifying Expectations

使用双大括号(Double-Brace Block)定义Expectations.如下所示:

public void testSomeAction() {
    ... set up ...

    context.checking(new Expectations() {{
        ... expectations go here ...
    }});

    ... code being tested ...

    ... assertions ...
}

expectations块可以包含多个expectation,每一个expectation有如下结构:

invocation-count (mock-object).method(argument-constraints);
inSequence(sequence-name);
when(state-machine.is(state-name));
will(action);
then(state-machine.is(new-state-name));

如果只想获取mock类的调用次数,则其它从句都是可选的.你可以给一个expectation使用任意多个inSequence, when, will and then从句.

下面的例子定义了多个期望:

@Test public void
returnsCachedObjectWithinTimeout() {
    context.checking(new Expectations() {{
        oneOf (clock).time(); will(returnValue(loadTime));
        oneOf (clock).time(); will(returnValue(fetchTime));

        allowing (reloadPolicy).shouldReload(loadTime, fetchTime); will(returnValue(false));

        oneOf (loader).load(KEY); will(returnValue(VALUE));
    }});

    Object actualValueFromFirstLookup = cache.lookup(KEY);
    Object actualValueFromSecondLookup = cache.lookup(KEY);

    assertSame("should be loaded object", VALUE, actualValueFromFirstLookup);
    assertSame("should be cached object", VALUE, actualValueFromSecondLookup);
}

因为expectations使用匿名内部类定义,所以expectations块内引用的mock对象(或其它值)都必须是final.将mock的对象定义为变量,将要测试的值定义为常量可以让测试更方便.

一个测试中可以包含多个expectation块.后面定义的expectation块会追加到前面定义的块中.分开写会让提高代码的可读性,更清晰.

@Test public void
returnsCachedObjectWithinTimeout() {
    context.checking(new Expectations() {{
        oneOf (clock).time(); will(returnValue(loadTime));
        oneOf (loader).load(KEY); will(returnValue(VALUE));
    }});

    Object actualValueFromFirstLookup = cache.lookup(KEY);

    context.checking(new Expectations() {{
        oneOf (clock).time(); will(returnValue(fetchTime));            
        allowing (reloadPolicy).shouldReload(loadTime, fetchTime); will(returnValue(false));
    }});

    Object actualValueFromSecondLookup = cache.lookup(KEY);

    assertSame("should be loaded object", VALUE, actualValueFromFirstLookup);
    assertSame("should be cached object", VALUE, actualValueFromSecondLookup);
}

Expectations不必定义到test方法中,可以抽象到一个方法中或在启动方法(setup)方法中.

@Test public void
returnsCachedObjectWithinTimeout() {
    initiallyLoads(VALUE);

    Object valueFromFirstLookup = cache.lookup(KEY);

    cacheHasNotExpired();

    Object valueFromSecondLookup = cache.lookup(KEY);

    assertSame("should have returned cached object",
               valueFromFirstLookup, valueFromSecondLookup);
}

private void initiallyLoads(Object value) {
    context.checking(new Expectations() {{
        oneOf (clock).time(); will(returnValue(loadTime));
        oneOf (loader).load(KEY); will(returnValue(value));
    }});
}

private void cacheHasNotExpired() {
    context.checking(new Expectations() {{
        oneOf (clock).time(); will(returnValue(fetchTime));            
        allowing (reloadPolicy).shouldReload(loadTime, fetchTime); will(returnValue(false));
    }});
}

4.Returning Values from Mocked Methods

除非特殊声明,否则jMock会对非void的返回值方法调用时,会生成一个合适的返回值.多数情况下我们需要为mock方法的调用定义一个返回值.

The Simple Case

可以在expectation块中通过”will”定义期望的返回值.

oneOf (calculator).add(2, 2); will(returnValue(5));

如果返回值不是5,jMock会测试失败.

Returning Iterators over Collections
“returnIterator”方法可以指定返回一个集合类.

final List<Employee> employees = new ArrayList<Employee>();
employees.add(alice);
employees.add(bob);

context.checking(new Expectations() {{
    oneOf (department).employees(); will(returnIterator(employees));
}});

一个简单的方法是在returnIterator方法中声明集合中的每个对象.

context.checking(new Expectations() {{
    oneOf (department).employees(); will(returnIterator(alice, bob));
}});

注意:returnIterator每次返回一个新的对象,returnValue每次调用返回同一个对象.

Returning Different Values on Consecutive Calls

有两种方式实现为不同的调用返回不同的值.第一种是在定义多个expectations:

oneOf (anObject).doSomething(); will(returnValue(10));
oneOf (anObject).doSomething(); will(returnValue(20));
oneOf (anObject).doSomething(); will(returnValue(30));

另一种(更好的)方式是使用onConsecutiveCalls方法:

atLeast(1).of (anObject).doSomething();
   will(onConsecutiveCalls(
       returnValue(10),
       returnValue(20),
       returnValue(30)));

5.Throwing Exceptions from Mocked Methods

使用throwException方法可以为mock方法抛出一个异常

allowing (bank).withdraw(with(any(Money.class)));
    will(throwException(new WithdrawalLimitReachedException());

jMock会检查抛出的异常与try中的异常列表是否匹配.你可以为任何方法定义任何RuntimeException或Error异常.

allowing (bank).withdraw(Money.ZERO);
    will(throwException(new IllegalArgumentException("you cannot withdraw nothing!");

6.Matching Parameter Values

可以为同一个方法在参数不同时定义不同的返回值:

allowing (calculator).add(1,1); will(returnValue(3));
allowing (calculator).add(2,2); will(returnValue(5));
allowing (calculator).sqrt(-1); will(throwException(new IllegalArgumentException());

可以为同一个方法定义不同参数的调用次数:

one   (calculator).load("x"); will(returnValue(10));
never (calculator).load("y");

如果实际参数与expected中的参数相等,则两者会匹配上.大部分情况下,使用equal方法来验证值是否相等,当时Null值会被提前检查,所以在实际请求参数或expected参数中都可以安全的使用null.数组是否相等是特殊处理的:两个数据必须size相同并且所有的元素equal.如果你想忽略参数或以不同的方式匹配参数,你需要为expectation定义matchers(后面会讲解如何定义).

7.Matchers

多数情况下,expected声明的参数需要与实际调用的参数匹配:

allowing (calculator).add(2,2); will(returnValue(5));

然而有些情况下,你需要定义松散的限制甚至忽略(部分)参数.例如:

allowing (calculator).sqrt(with(lessThan(0));
will(throwException(new IllegalArgumentException());

oneOf (log).append(with(equal(Log.ERROR)), with(stringContaining("sqrt"));

松散限制需要为特殊的参数定义特殊的匹配规则.匹配器是由工厂方法创建的,比如:lessThan, equal and stringContaining.每一个工厂方法的返回结构都必须封装在一个with方法中.

一个使用了参数匹配器的expectation必须用”with”方法封装每一个参数,无论是matcher方法还是一个实际值.

常用的工厂方法匹配器定义在Expectations类中,更多的用静态方法定义在org.hamcrest.Matchers类中.如果你需要,可以在代码中静态引入Matchers类:

import static org.hamcrest.Matchers.*;

Matchers为参数验证提供了更多的可能性,你可以在某些测试场景中使用自定义的matchers.

Basic Matchers

  • Object Equality

匹配器中最常见的equal匹配器,如下所示:参数必须等于1

oneOf (mock).doSomething(with(equal(1)));

相等限制(The equalTo constraint)使用使用equals方法验证期望参数与实际参数是否相等.Null值会被提前校验,所以使用equal(null)或参数是null的匹配器函数都是安全的.数组相等的比较:两个数组长度同等,每一个元素都equal.

  • Object Identity

同一个匹配器(The same matcher)要求传入的参数与期望的参数必须是同一个对象,该限制高于equal,要求是在内存中是同一个对象.例子如下:

Object expected = new Object();
oneOf (mock).doSomething(with(same(expected)));

规则:对值相同的对象是使用equal,同一个行为对象使用same.

  • Substring

字符串包含匹配器(The stringContaining matcher):期望的参数必须包含匹配器声明的字符串.

oneOf (mock).doSomething(with(stringContaining("hello")));

字符串包含匹配器在测试字符串内容时非常有用.

  • Null or Not Null

aNull(Class) and aNonNull(Class)匹配器,顾名思义,就是限定参数是null或not null.下面例子中限定参数必须是String类型的,第一个是null,第二个not null .

oneOf (mock).doSomething(with(aNull(String.class)), aNonNull(String.class)));
  • Anything

any(Class)限定:只要类型相同,任意值都是可通过验证的.适用于只关心调度,不关心实际场景或结果的情况.适时地使用该匹配器可以在测试代码变化时不需要修改代码中的一些常量.如下列所示:第二个参数只要是字符串类型即可.

oneOf (mock).doSomething(with(eq(1)), with(any(String.class)));
  • Numeric Equality with Error Margin

可以通过equal的一个重载版本来验证浮点型数据是否相等,如果传递在某个区间之内,则认为两个参数相等.如下所示:如果参数在[1-0.002,1+0.002]区间上,则参数与期望相等.

oneOf (mock).doSomething(with(equal(1, 0.002)));
  • Combining Matchers

可以使用组合匹配器来创建自定义(更严格或个松散)的规范.

  • Not — Logical Negation

not匹配器要求参数不能与匹配器中给定的值相等.如下例所示,参数不为1即可.

oneOf (mock).doSomething(with(not(eq(1)));
  • AllOf — Logical Conjunction

AllOf匹配器:参数必须与AllOf中所有的规则匹配.如下,参数必须含有”hello”和”world”字符串.

oneOf (mock).doSomething(with(allOf(stringContaining("hello"),
                                  stringContaining("world"))));
  • AnyOf — Logical Disjunction

anyof匹配器要求参数至少与匹配器中的一个规则匹配.如下,参数必须含有”hello”或”howdy”其中一个.

oneOf (mock).doSomething(with(anyOf(stringContains("hello"),
                                  stringContains("howdy"))));

8.Expecting Methods More (or Less) than Once

在某些情况下,我们期望验证方法的调度次数否则测试失败.期望的调度次数可以为:多次.零次或者忽略不关心的mock对象的调用.

“调用次数()”期望定义了方法允许调用的最大值和最小值,调用次数需要在mock对象前指定.

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方法可以被调用任意次,包括0次.
ignoring与allowing相同.
使用是根据语义选择两者之一, 以使代码可读性更强.
never方法不应该调用.

Expecting vs. Allowing

与allowing匹配器最大的区别是可以指定调用次数.
allowed的方法,如果在测试过程中没有调用,测试是通过的;如果方法是expected的,在测试执行中如果方法没有被调用,测试用例是失败的. 一般来说,查询类的方法用allowing会合适一些.

9.Expect a Sequence of Invocations

Sequences指明了期望必须按指定的顺序被调用.在一个测试代码可以有多个Sequence,一个期望可以包含多个Sequence.

定义一个Sequence:

final Sequence sequence-name = context.sequence("sequence-name");

JMock可以自动实例化Sequences(@Auto),减少重复无聊的代码.

为了顺序调度,需要按顺序写期望(expectations)并在每个期望后增加inSequence(sequence)代码.例如:

oneOf (turtle).forward(10); inSequence(drawing);
oneOf (turtle).turn(45); inSequence(drawing);
oneOf (turtle).forward(10); inSequence(drawing);

在sequence中的期望可以指定任意调用次数,但是如果期望是allowing的,sequence将忽略该期望.

10.状态机

States用来声明期望只有在特定的状态下有效/无效。一个测试中可以定义多个状态机,一个方法也可以声明在多个状态下才能调用。

定义一个状态机:

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

初始状态是可选的.如果没有声明,状态机处于未命名的初始状态.

JMock可以自动实例化States(@Auto),减少重复无聊的代码.

下面的表格说明了方法调用与状态机的状态之间的关系:

状态机说明
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"));

11.忽略不相干的mock对象

下面的期望会在测试执行时忽略mock对象”fruitbat”.

ignoring (fruitbat);

在测试期间fruitbat的方法可以被调用任意次数或这不调用.

当被忽略的对象在测试期间被调用,其将会返回”0”值.具体返回值以来调用方法的返回值:

返回值类型“0”值
booleanfalse
numeric typezero
String“” (empty string)
ArrayEmpty array
Mockable typeA mock that is ignored
Any other typenull

实际上一个有返回值的mock方法被调用时,将额外返回了一个”可忽略的(ignored)”mock对象,这对ignoring期望是非常有用的。该方法也将返回”0”值,如果有其它方法mock该返回值时,其返回值也是ignored的(返回“0”值),任何mock “ignored对象”的方法的返回值也是ignored的. 这样你可以只关心业务功能,忽略不相关的功能.

好难翻译,附原文:
The fact that a method with a mockable return type will return another ignored mock object makes the ignoring expectation very powerful. The returned object will also return “zero” values, and any mockable values it returns will also be ignored, and any mockable values they return will be ignored, and so on. This allows you to focus individual tests on different aspects of an object's functionality, ignoring irrelevant collaborators.

比如,下面ignored了Hibernate SessionFactory类,也会ignore由getCurrentSession()创建的Session,beginTransaction()创建的Transaction,以及commit() 和 rollback()方法.这样你就可以独立测试对象在事务下的行为,而在其他测试中测试事务管理.

ignoring (hibernateSessionFactory);

12.覆盖setup中的期望

如果你在启动(set-up)方法中定义了期望,而且在测试方法中想忽略该期望.比如,你在启动方法中定义了对mock对象的期望,而在测试方法中仍想测试该mock对象,此时如果你只是在测试方法中增加期望,可能会测试失败,因为启动方法中的期望优先级更高,可能不满足实际测试期望.

在这种情况下,使用状态机控制期望可解决这个问题。此时用状态机表示测试的状态,而不是被测试对象通信的状态。

  1. 定义一个状态机并命名,比如“test”。
  2. 在测试类启动(set-up)方法里,定义期望,该期望只在“test”状态机状态为“fully-set-up”时有效。
  3. 启动方法执行完毕后,将状态机“test”的状态设置为“fully-set-up”。
  4. 和平时一样在测试中定义期望。

这样启动方法中定义的期望只在启动方法执行时有效,在测试方法执行时会被忽略。

@RunWith(JMock.class)
public class ChildTest {
    Mockery context = new JUnit4Mockery();
    States test = mockery.states("test");

    Parent parent = context.mock(Parent.class);

    // This is created in setUp
    Child child;

    @Before
    public void createChildOfParent() {
        mockery.checking(new Expectations() {{
            ignoring (parent).addChild(child); when(test.isNot("fully-set-up"));
        }});

        // Creating the child adds it to the parent
        child = new Child(parent);

        test.become("fully-set-up");
    }

    @Test
    public void removesItselfFromOldParentWhenAssignedNewParent() {
        Parent newParent = context.mock(Parent.class, "newParent");

        context.checking(new Expectations() {{
            oneOf (parent).removeChild(child);
            oneOf (newParent).addChild(child);
        }});

        child.reparent(newParent);
    }
}

13.Match Objects or Methods

虽然匹配器通常用于指定具体的参数,但也可以指定可接收的对象或可执行的方法.使用时,在计数的匹配器之后加上此期望即可.此匹配器支持以下几种方法:

方法说明
method(m)期望一个名称为m的方法
method(r)方法的名称需要与正则表达式r匹配
with(m1, m2, … mn)参数必须从m1匹配到mn.
withNoArguments()方法必须无参数

上面表格中的期望可以跟随在sequences和states之后.

Examples

允许调用mock对象的所有getter方法:

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

允许方法使用集合内任一元素调用一次:

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

14.Writing New Matchers

jMock和Hamcrest提供了很多Matcher类和工厂方法用来创建匹配器。有时这些预定义的匹配器可能不能满足实际场景或不能使测试更灵活,在这种情况下,你可以创建一个新的匹配器.

匹配器需要实现org.hamcrest.Matcher interface接口。它需要做两件事情:

  • 声明一个参数是否满足限制条件。
  • 生成一个测试用例失败时时的说明(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);
      }
    
      public StringBuffer describeTo(Description description) {
          return description.appendText("a string starting with ").appendValue(prefix);
      }
    }
  2. 写一个工厂类用于初始化上面定义的匹配器。

    @Factory
    public static Matcher<String> aStringStartingWith( String prefix ) {
      return new StringStartsWithMatcher(prefix);
    }

    工厂方法的名称需要在测试用例中表意清晰准确,需要考虑该名称在期望中读起来顺口。

  3. 在测试用例中使用工厂方法创建匹配器。下面举例了logger类的error方法必须调用一次,并且传入的参数需要以“FATAL”开头。

    public class MyTestCase {
      ...
    
      public void testSomething() {
          ...
    
          context.checking(new Expectations() {{
              oneOf (logger).error(with(aStringStartingWith("FATAL")));
          }});
          ...
      }
    }

实现规则

Matcher对象必须是无状态的。

当方法调用时,jMock会使用匹配器找到一个匹配的期望。这就意味着在测试执行时匹配器会多次被调用,甚至在找到匹配器的期望之后。实际上,jMock并不关心匹配器什么时候调用,调用多少次。这对无状态的匹配器不会有任何问题,但是对有状态的匹配器将产生不可预测的结果。

如果你想持有维护调用返回的状态,请使用Action,而不是Matcher。

更多信息

更新的文档可以查看Hamcrest project.

15.Writing New Actions

JMock期望做两件事情:测试接收的期望方法的调用并对方法调用的行为存根(stub)。基本上所有的方法都能用下面三种方式中的一种存根:无返回(void方法),返回一个结果或异常。然而有时存根一个方法的非返回值的其它影响(you need to stub side effects of a method),比如一个方法通过是它的一个参数返回处理结果。jMock可以很容易的写一个自定义的存根处理这种不常见的情况。

这个有一个简单的例子:我们想测试一个FruitPicker类来从FruitTree类型的集合中收集水果。不同的果树实现各自的收集水果的方法。

例子:

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

我们需要mock FruitTree接口用来测试我们类的行为,但是该接口的返回值是void的,为了存根该方法执行的影响我们需要编写新的Action.

写actoin:

  1. 写一个类实现Action接口.这里定义了一个action用来向第一个集合类型的参数中增加元素。
public class AddElementsAction<T> implements Action {
    private Collection<T> elements;

    public AddElementsAction(Collection<T> elements) {
        this.element = elements;
    }

    public void describeTo(Description description) {
        description.appendText("adds ")
                   .appendValueList("", ", ", "", elements)
                   .appendText(" to a collection");
    }

    public Object invoke(Invocation invocation) throws Throwable {
        ((Collection<T>)invocation.parameterValues.get(0)).addAll(elements);
        return null;
    }
}
  1. 写一个工厂方法用来实例化action。你可以定义一个静态的工厂方法或将工厂方法放到一个类中,这样测试类import会比较简单。工厂方法并不需要静态导入。如果你只是在一个测试中使用,你可以只在测试用例中写工厂方法。
public static <T> Action addElements(T... newElements) {
    return new AddElementsAction<T>(Arrays.asList(newElements));
}
  1. 在will方法中调用工厂方法
final FruitTree mangoTree = mock(FruitTree.class);
final Mango mango1 = new Mango();
final Mango mango2 = new Mango();

context.checking(new Expectations() {{
    oneOf (mangoTree).pickFruit(with(any(Collection.class)));
        will(addElements(mango1, mango2));
}});

...

16.Scripting Custom Actions

定义一个action是简单的但是需要大量的代码。jMock脚本扩展(jMock Scripting Extension)可以让你使用BeanShell脚本在期望中一行代码定义action。

注意:因为脚本在代码中是字符串,所以不方便重构。

第一步,你需要在classpath中添加下面两个jar:

  • jmock-script-2.6.1.jar
  • bsh-core-2.0b4.jar
    第二步,你需要在测试用例中import “perform”工厂方法。
import static org.jmock.lib.script.ScriptedAction.perform;

然后使用BeanShell脚本在perform方法中定义action。在脚本中可以引用方法的参数: 0(), 1,$2,以此类推。比如,下面的脚本会回调第一个参数Runnable的run方法:

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来定义一个脚本变量。

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));
}});

...

18.mock泛型

java的泛型在反射api下work并不好。在jMock下,你可能碰到过mock一个泛型类时报静态类型错误。但是这个警告是错误的。最好的阻止warning的方式是在变量上使用注解suppress。

比如下面的泛型接口:

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

在测试用例中,你可能像下面的方式mock这个接口:

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");

两行代码,尽管语法和语义都正确,反应仍然报warnings。为了阻止该警告,你必须使用@SuppressWarnings:

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

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

19.Mock Abstract and Concrete Classes

因为jMock使用的是java标准的反射机制,所以默认情况下只能mock接口,不能mock类。(实际上,我们认为这是一个良好的设计:在设计时,相比类的实现和数据存储,更应关注对象之间的交互)。我们通过ClassImposteriser可以mock类,就像mock接口一样。ClassImposteriser依赖于CGLIB 2.1 and Objenesis libraries。这在处理紧耦合的类时非常有用。

ClassImposteriser并不是通过构造函数创建来mock对象的,所以无论类有什么样的构造函数都可以被安全地被mock。但是ClassImposteriser不能mock final classes 或者 final methods。

如果你想mock final classes 或者 final methods,JDave library提供了在虚拟机执行前去掉final的机制,这样final classes 或者 final methods也可以使用ClassImposteriser mock.

如何使用ClassImposteriser:

  1. 在CLASSPATH中增加 jmock-legacy-2.6.1.jar, cglib-nodep-2.1_3.jar和objenesis-1.0.jar。
  2. 在测试类中使用ClassImposteriser:

    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 Mockery context = new JUnit4Mockery() {{
          setImposteriser(ClassImposteriser.INSTANCE);
      }};
    
      ...
    }

    现在你的测试用例可以mock抽象类或具体类:

    Graphics g = context.mock(java.awt.Graphics.class);

22.Use jMock in Maven Builds

jMock 2

在pom文件中增加下面的以来就可以支持jMock2了,所有的jMock和Hamcrest依赖会自动包含进来。

<dependency>
  <groupId>org.jmock</groupId>
  <artifactId>jmock-junit4</artifactId>
  <version>2.6.1</version>
</dependency>

注意:jMock从2.4.0 开始,默认的Junit4依赖是junit:junit-dep。需要在maven中替代标准的junit:junit依赖。maven-surefire-plugin 2.3.1 和2.4 支持只是用属性junit:junit-dep配置。

如果需要mock类,还需要增加依赖:

<dependency>
  <groupId>org.jmock</groupId>
  <artifactId>jmock-legacy</artifactId>
  <version>2.6.1</version>
</dependency>
jMock 1

To use jMock 1.2.0 core:

<dependency>
  <groupId>jmock</groupId>
  <artifactId>jmock</artifactId>
  <version>1.2.0</version>
</dependency>

To use the jMock 1.2.0 CGLIB extension:

<dependency>
  <groupId>jmock</groupId>
  <artifactId>jmock-cglib</artifactId>
  <version>1.2.0</version>
</dependency>

23.Understand method dispatch in jMock 2

当一个mock方法被调用时,jMock(的 Mockery searches)将从该方法执行前所有的期望中找到一个匹配的期望。调用结束后,该期望可能不会再被匹配到。比如,期望one只会匹配一次,期望atLeast(1)会匹配多次。

这样你可以为同一个方法的多次调用设置不同的期望。但是,最好向下面这样使用onConsecutiveCalls方法处理:

然而,这个工作模式下可能会让你遇到一些“陷阱”:
* 如果你在某些期望前使用了”allowing” or “ignoring” or “atLeast(n)”,由于前面定义的期望会优先匹配,导致后面的对同一个方法定义的期望不会触发。
* if you create an expectation and then a stub for the same method, the expectation will match, and when it stops matching the stub will be used instead, possibly masking test failures.
* 如果你为一个方法创建了期望然后又创建了存根,这样期望将会开始匹配,而匹配结束后存根将会被执行,这样可能掩盖了测试失败的情况。

最佳方式是为同一个方法不要定义多个相同的期望,而是使用onConsecutiveCalls来创建多个action:

atLeast(1).of (anObject).doSomething();
   will(onConsecutiveCalls(
       returnValue(10),
       returnValue(20),
       throwException(new IOException("no more stuff"))));

如果想定义更复杂的调用顺序限制,请使用sequences或state machines。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值