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