关于单元测试,模拟对象一直是不可缺少的,尤其对于复杂的应用来说。
这么多的模拟对象框架中,个人觉得比较好用的当属EasyMock了。当然JMock也不错。
下面简单介绍一下EasyMock 。
EasyMock 2 主要用于给指定的接口提供模拟对象。 <o:p></o:p>
模拟对象只是模拟领域代码直接的部分行为,能检测是否他们如定义中的被使用。使用 Mock 对象,来模拟合作接口,有助于隔离测试相应的领域类。 <o:p></o:p>
创建和维持 Mock 对象经常是繁琐的任务,并且可能会引入错误。 EasyMock 2 动态产生 Mock 对象,不需要创建,并且不会产生代码。 <o:p></o:p>
有利的方面: <o:p></o:p>
不需要手工写类来处理 mock 对象。 <o:p></o:p>
支持安全的重构 Mock 对象:测试代码不会在运行期打断当重新命名方法或者更改方法参数。 <o:p></o:p>
支持返回值和例外。 <o:p></o:p>
支持检察方法调用次序,对于一个或者多个 Mock 对象。 <o:p></o:p>
不利的方面: 2.0 仅使用于 java 2 版本 5.0 或者以上
以一个例子来说明如何使用EasyMock:
假设有一个合作接口Collaborator:
package org.easymock.samples;<o:p></o:p> <o:p> </o:p> public interface Collaborator {<o:p></o:p> void documentAdded(String title);<o:p></o:p> void documentChanged(String title);<o:p></o:p> void documentRemoved(String title);<o:p></o:p> byte voteForRemoval(String title);<o:p></o:p> byte[] voteForRemovals(String[] title);<o:p></o:p> }<o:p></o:p> |
我们主要的测试类为:
package org.easymock.samples;<o:p></o:p> import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; public class ClassUnderTest { private Set<Collaborator> listeners = new HashSet<Collaborator>(); private Map<String, byte[]> documents = new HashMap<String, byte[]>(); public void addListener(Collaborator listener) { listeners.add(listener); } public void addDocument(String title, byte[] document) { boolean documentChange = documents.containsKey(title); documents.put(title, document); if (documentChange) { notifyListenersDocumentChanged(title); } else { notifyListenersDocumentAdded(title); } } public boolean removeDocument(String title) { if (!documents.containsKey(title)) { return true; } if (!listenersAllowRemoval(title)) { return false; } documents.remove(title); notifyListenersDocumentRemoved(title); return true; } public boolean removeDocuments(String[] titles) { if (!listenersAllowRemovals(titles)) { return false; } for (String title : titles) { documents.remove(title); notifyListenersDocumentRemoved(title); } return true; } private void notifyListenersDocumentAdded(String title) { for (Collaborator listener : listeners) { listener.documentAdded(title); } } private void notifyListenersDocumentChanged(String title) { for (Collaborator listener : listeners) { listener.documentChanged(title); } } private void notifyListenersDocumentRemoved(String title) { for (Collaborator listener : listeners) { listener.documentRemoved(title); } } private boolean listenersAllowRemoval(String title) { int result = 0; for (Collaborator listener : listeners) { result += listener.voteForRemoval(title); } return result > 0; } private boolean listenersAllowRemovals(String[] titles) { int result = 0; for (Collaborator listener : listeners) { result += listener.voteForRemovals(titles); } return result > 0; } } |
第一个Mock 对象<o:p></o:p>
我们将创建
test case
并且围绕此理解相关的
EasyMock
包的功能。第一个测试方法,用于检测是否删除一个不存在的文档,不会发通知给合作类。
<o:p>package org.easymock.samples;<o:p></o:p> <o:p> </o:p> import junit.framework.TestCase;<o:p></o:p> <o:p> </o:p> public class ExampleTest extends TestCase {<o:p></o:p> <o:p> </o:p> private ClassUnderTest classUnderTest;<o:p></o:p> private Collaborator mock;<o:p></o:p> <o:p> </o:p> protected void setUp() {<o:p></o:p> classUnderTest = new ClassUnderTest();<o:p></o:p> classUnderTest.addListener(mock);<o:p></o:p> }<o:p></o:p> <o:p> </o:p> public void testRemoveNonExistingDocument() { <o:p></o:p> // This call should not lead to any notification<o:p></o:p> // of the Mock Object: <o:p></o:p> classUnderTest.removeDocument("Does not exist");<o:p></o:p> }<o:p></o:p> }<o:p></o:p> </o:p> |
对于多数测试类,使用
EasyMock 2,
我们只需要静态引入
org.easymock.EasyMock
的方法。
<o:p></o:p> import static org.easymock.EasyMock.*;<o:p></o:p> import junit.framework.TestCase;<o:p></o:p> <o:p> </o:p> public class ExampleTest extends TestCase {<o:p></o:p> <o:p> </o:p> private ClassUnderTest classUnderTest;<o:p></o:p> private Collaborator mock;<o:p></o:p> <o:p></o:p> }<o:p></o:p> |
为了取得Mock 对象,需要:<o:p></o:p>
l 创建Mock 对象从需要模拟的接口<o:p></o:p>
l 记录期待的行为<o:p></o:p>
l 转换到Mock对象,replay状态。<o:p></o:p>
例如:
<o:p></o:p> protected void setUp() {<o:p></o:p> mock = createMock(Collaborator.class); // 1<o:p></o:p> classUnderTest = new ClassUnderTest();<o:p></o:p> classUnderTest.addListener(mock);<o:p></o:p> }
<o:p> public void testRemoveNonExistingDocument() {<o:p></o:p> // 2 (we do not expect anything)<o:p></o:p> replay(mock); // 3<o:p></o:p> classUnderTest.removeDocument("Does not exist");<o:p></o:p> }</o:p> |
在执行第三步后,mock 为Collaborator
接口的Mock对象,并且期待没有什么调用。这就意味着,如果我们改变ClassUnderTest去调用此接口的任何方法,则Mock对象会抛出AssertionError:<o:p></o:p>
<o:p></o:p> java.lang.AssertionError: <o:p></o:p> Unexpected method call documentRemoved("Does not exist"):<o:p></o:p> at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:29)<o:p></o:p> at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:44)<o:p></o:p> at $Proxy0.documentRemoved(Unknown Source)<o:p></o:p> at org.easymock.samples.ClassUnderTest.notifyListenersDocumentRemoved(ClassUnderTest.java:74)<o:p></o:p> at org.easymock.samples.ClassUnderTest.removeDocument(ClassUnderTest.java:33)<o:p></o:p> at org.easymock.samples.ExampleTest.testRemoveNonExistingDocument(ExampleTest.java:24)<o:p></o:p> ...<o:p></o:p> |
增加行为<o:p></o:p>
让我们开始第二个测试。如果
document
被
classUnderTest
增加,我们期待调用
mock.documentAdded()
在Mock对象使用document的标题作为参数:<o:p></o:p>
<o:p></o:p> public void testAddDocument() {<o:p></o:p> mock.documentAdded("New Document"); // 2<o:p></o:p> replay(mock); // 3<o:p></o:p> classUnderTest.addDocument("New Document", new byte[0]); <o:p></o:p> }<o:p></o:p> |
如果
classUnderTest.addDocument("New Document", new byte[0])
调用期待的方法,使用错误的参数,Mock对象会抛出AssertionError:
<o:p></o:p> java.lang.AssertionError: <o:p></o:p> Unexpected method call documentAdded("Wrong title"):<o:p></o:p> documentAdded("New Document"): expected: 1, actual: 0<o:p></o:p> at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:29)<o:p></o:p> at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:44)<o:p></o:p> at $Proxy0.documentAdded(Unknown Source)<o:p></o:p> at org.easymock.samples.ClassUnderTest.notifyListenersDocumentAdded(ClassUnderTest.java:61)<o:p></o:p> at org.easymock.samples.ClassUnderTest.addDocument(ClassUnderTest.java:28)<o:p></o:p> at org.easymock.samples.ExampleTest.testAddDocument(ExampleTest.java:30)<o:p></o:p> ...<o:p></o:p> |
同样,如果调用多次此方法,则也会抛出例外:
<o:p></o:p> java.lang.AssertionError: <o:p></o:p> Unexpected method call documentAdded("New Document"):<o:p></o:p> documentAdded("New Document"): expected: 1, actual: 1 (+1)<o:p></o:p> at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:29)<o:p></o:p> at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:44)<o:p></o:p> at $Proxy0.documentAdded(Unknown Source)<o:p></o:p> at org.easymock.samples.ClassUnderTest.notifyListenersDocumentAdded(ClassUnderTest.java:62)<o:p></o:p> at org.easymock.samples.ClassUnderTest.addDocument(ClassUnderTest.java:29)<o:p></o:p> at org.easymock.samples.ExampleTest.testAddDocument(ExampleTest.java:30)<o:p></o:p> ...<o:p></o:p> |
验证行为<o:p></o:p>
当我们指定行为后,我们将验证实际发生的。当前的测试将会判断是否Mock对象会真实调用。可以调用verify(mock)
来山正是否指定的行为被调用。
<o:p></o:p> public void testAddDocument() {<o:p></o:p> mock.documentAdded("New Document"); // 2 <o:p></o:p> replay(mock); // 3<o:p></o:p> classUnderTest.addDocument("New Document", new byte[0]);<o:p></o:p> verify(mock);<o:p></o:p> }<o:p></o:p> |
如果失败,则抛出AssertionError<o:p></o:p>
期待明显数量的调用<o:p></o:p>
到现在,我们的测试只是调用一个简单的方法。下一个测试将会检测是否已经存在document导致mock.documentChanged()
调用。为了确认,调用三次
<o:p></o:p> public void testAddAndChangeDocument() {<o:p></o:p> mock.documentAdded("Document");<o:p></o:p> mock.documentChanged("Document");<o:p></o:p> mock.documentChanged("Document");<o:p></o:p> mock.documentChanged("Document");<o:p></o:p> replay(mock);<o:p></o:p> classUnderTest.addDocument("Document", new byte[0]);<o:p></o:p> classUnderTest.addDocument("Document", new byte[0]);<o:p></o:p> classUnderTest.addDocument("Document", new byte[0]);<o:p></o:p> classUnderTest.addDocument("Document", new byte[0]);<o:p></o:p> verify(mock);<o:p></o:p> }<o:p></o:p> |
为了避免重复的mock.documentChanged("Document")
,EasyMock提供一个快捷方式。可以通过调用方法expectLastCall().times(int times)
来指定最后一次调用的次数。
<o:p></o:p> public void testAddAndChangeDocument() {<o:p></o:p> mock.documentAdded("Document");<o:p></o:p> mock.documentChanged("Document");<o:p></o:p> expectLastCall().times(3);<o:p></o:p> replay(mock);<o:p></o:p> classUnderTest.addDocument("Document", new byte[0]);<o:p></o:p> classUnderTest.addDocument("Document", new byte[0]);<o:p></o:p> classUnderTest.addDocument("Document", new byte[0]);<o:p></o:p> classUnderTest.addDocument("Document", new byte[0]);<o:p></o:p> verify(mock);<o:p></o:p> }<o:p></o:p> |
指定返回值<o:p></o:p>
对于指定返回值,我们通过封装expect(T value)
返回的对象并且指定返回的值,使用方法andReturn(Object returnValue)于expect(T value)
.返回的对象。<o:p></o:p>
例如:<o:p></o:p>
<o:p></o:p> public void testVoteForRemoval() {<o:p></o:p> mock.documentAdded("Document"); // expect document addition<o:p></o:p> // expect to be asked to vote for document removal, and vote for it<o:p></o:p> expect(mock.voteForRemoval("Document")).andReturn((byte) 42);<o:p></o:p> mock.documentRemoved("Document"); // expect document removal<o:p></o:p> replay(mock);<o:p></o:p> classUnderTest.addDocument("Document", new byte[0]);<o:p></o:p> assertTrue(classUnderTest.removeDocument("Document"));<o:p></o:p> verify(mock);<o:p></o:p> } <o:p></o:p> <o:p> </o:p> public void testVoteAgainstRemoval() {<o:p></o:p> mock.documentAdded("Document"); // expect document addition<o:p></o:p> // expect to be asked to vote for document removal, and vote against it<o:p></o:p> expect(mock.voteForRemoval("Document")).andReturn((byte) -42);<o:p></o:p> replay(mock);<o:p></o:p> classUnderTest.addDocument("Document", new byte[0]);<o:p></o:p> assertFalse(classUnderTest.removeDocument("Document"));<o:p></o:p> verify(mock);<o:p></o:p> }<o:p></o:p> |
取代
expect(T value)
调用,可以通过expectLastCall()
.
来代替