物种起源
任何框架和lib都有其实际需求来源,纯粹为了技术或概念而code的项目,大概都还在象牙塔里。
Jmock 的首页上第一句话相当简明:
JMock is a library that supports test-driven development of Java code with mock objects.
了解JMOCK,从两个概念入手:
TDDUnit tests are so named because they each test one unit of code. Whether a module of code has hundreds of unit tests or only five is irrelevant. A test suite for use in TDD should never cross process boundaries in a program, let alone network connections. Doing so introduces delays that make tests run slowly and discourage developers from running the whole suite. Introducing dependencies on external modules or data also turns unit tests into integration tests. If one module misbehaves in a chain of interrelated modules, it is not so immediately clear where to look for the cause of the failure.
为什么叫unit test(单元测试)?单元测试就是测试单个method的功能,这也就是为什么我们写junit测试时,总是一个个testMethodXXX。TDD里提倡单元测试的单元性,需要将外部依赖性(比如DB, network)隔离出来。有两个好处,一是让test 飞一飞,跑的更快,二是界定boundaries,因为逻辑交叉,一个逻辑失败,会导致好多测试失败。
老实说,这两点有些似是而非。test越接近产品环境,越有可能发现产品环境下的bug。另外continuous integration中说测试可以放在晚上跑,所以速度慢不是问题。连锁反应是个问题,可能会导致找bug时道路曲折。但测试失败应该不是个经常发生的问题把。
anyway,怎么保证隔离呢?that's why need mock..
mock object
In a unit test, mock objects can simulate the behavior of complex, real (non-mock) objects and are therefore useful when a real object is impractical or impossible to incorporate into a unit test
mock object就是使用method stub来实现定义的interface,比如:
BEGIN ThermometerRead(Source insideOrOutside) RETURN 28 END ThermometerRead
方法不作任何逻辑处理,直接return一个预定的结果。
针对上述概念,最简单直接的一种实现方法是每个接口增加一个test实现。在需要的地方使用test实现来作测试。这种方法理论上可行, why?
首先你会形成class disaster,增加无数个test class。其次缺少灵活性,在stub实现往往是输出一个固定结果,而测试时有可能需要针对不同条件来返回不同值。
关于class disaster的问题, jmock使用代理来避免。关于return的问题,jmock引入了action的概念。
简单点,实现原则:
proxy 声明方法的调用声明方法的输入/输出其具体实现,本人到没有dig。只是对其中的一种语法比较感兴趣, Expectations使用双大括号声明{{ }} (Double-Brace Block)。java中有这种语法?本文结尾给出揭晓
首先引入jmock的lib,使用eclipse中的maven-plugin自动添加
jmock-2.5.1.jarhamcrest-core-1.1.jarhamcrest-library-1.1.jar可能会有类加载的问题,因为junit 里面也打包了hamcrest。如果有java.lang.SecurityException class "org.hamcrest.TypeSafeMatcher" 问题。可以将junit改为junit-no-dep,或者改变类加载的搜索顺寻。
要测试的方法为Publisher的publish方法
public class Publisher { public void add(Subscriber subscriber) { //... } public void publish(String msg) { //... } } public interface Subscriber { public void receive(String msg); }
我们可以使用一个实现好的Subscriber,在publish之后,通过Subscriber的行为来判断msg是否receive。显然,如引言中所述,引入Subscriber的依赖,单元测试的边界变得模糊,我们到底是测试publish呢,还是测试receive呢?
jmock 测试code如下:
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() {{ oneOf (subscriber).receive(message); }}); // execute publisher.publish(message); // verify context.assertIsSatisfied(); } }
1. 生成要测试的object,Publisher为真publisher, Subscriber则是mock生成。
2. 让target对象使用mock对象,或者将mock对象设置到测试的target对象中
3. 声明mock对象的expectations,就是mock的哪些方法被调用,调用的输入输出是什么。例子中,声明subscriber的receive被调用一次,输入参数是message,没有输出。
4. 运行要测试的方法
5. 进行verify, context.assertIsSatisfied(),来assert调用是否和expectation一致。在实际测试中,我们往往加上自己的assert,assert测试方法的return(本例中target方法也没有输出)
从上面可以看出,使用jmock的一个关键部分就是声明expectations。
具体每个部分说明如下:
方法触发的次数,使用较多的是
oneOf 触发一次
exactly(n).of 触发n次
allowing 任意次
方法参数的匹配,使用with声明,主要使用same, equal, any等
oneOf (mock).doSomething(with(equal(1))); //参数为单一参数,值equal1
oneOf (mock).doSomething(with(same(1))) // 参数为单一参数,直 == 1
oneOf (mock).doSomething(with(any(Class<T> type))) //参数为单一参数,只要是class类型即可。
method invocation后的,使用will声明,主要使用return和throwException等
will(returnValue(v))
will(throwException(e))
状态机都出来了。哥还真用了一把。在最先的版本中,我们使用Spring来注入Mockery,所有test共用了一个Mockery,这样各个unit test的Expecting相互影响。怎么隔离开来呢?加上state condition
在每个测试的开头声明一个States
final States pen = context.states("mymethod").startsAs("start");
在expecting的method invocation加入when
oneOf (turtle).forward(10); when(pen.is("start")); 在测试结束时,将状态机的状态设置为非start的其他状态
invocation 的顺序,语法类似于States
Invocations that are expected in a sequence must occur in the order in which they appear in the test code. A test can create more than one sequence and an expectation can be part of more than once sequence at a time.
Add jmock-legacy-2.5.1.jar, cglib-nodep-2.1_3.jar and objenesis-1.0.jar to your CLASSPATH. 连final class夜可以被mock,引入jdave即可
private Mockery context = new Mockery() {{ setImposteriser(ClassImposteriser.INSTANCE); }};
自动生成mock对象
自动生成States/Sequence @Auto States pen; @Auto Sequence events;
由于我们系统的domain model很复杂(遗留原因),这时引入jmock,我们发现生成mock对象的过程,似乎是将所有Domain object和implementation又重新写了一遍。所有上述limit一下子暴露无遗。
目前暂时没有想到有效的应对下述系统的测试方法:
1. db design is complex/legacy
2. domain model (wsdl/xsd) design is complex. because we reuse the old business-rules, while those business-rules is not examine and verify carefully in the past.
一个遗留的db加上一个复杂的domain model,虽然business logic很简单,没有任何复杂的商业计算,只是简单的DAO -> WSDL Bean的操作,实现起来让开发人员觉得纷繁芜杂,难以入手。
在本文完成之时,我们决定仍然使用Jmock, 效果如何,下回分解。
这是一个trick,DoubleBraceInitialization
private static final Set<String> VALID_CODES = new HashSet<String>() {{ add("XZ13s"); add("AB21/X"); add("YYLEX"); add("AR2D"); }};
The first brace creates a new AnonymousInnerClass, the second declares an instance initializer block that is run when the anonymous inner class is instantiated
第一个大括号声明了一个Annoymous的InnerClass,例子中声明了一个Hashset的子类;第二个括号则是 instance initializer block,实例的初始化模块。expectations的声明相当明了。
使用mock,除了前文所说的isolate(隔离)外,模拟(simulate)也是另外一个重要原因,比如浏览器表单提交。。。
参见cookbook
http://www.jmock.org/cookbook.html
关于更多的java Idioms
http://www.c2.com/cgi/wiki?JavaIdioms