一个例子
下面是一个在单元测试中演示Mocquer用法的例子,假设存在一个类FTPConnector。
package org.jingle.mocquer.sample;
import java.io.IOException;
import java.net.SocketException;
import org.apache.commons.net.ftp.FTPClient;
public class FTPConnector {
//ftp server host name
String hostName;
//ftp server port number
int port;
//user name
String user;
//password
String pass;
public FTPConnector(String hostName,
int port,
String user,
String pass) {
this.hostName = hostName;
this.port = port;
this.user = user;
this.pass = pass;
}
/**
* Connect to the ftp server.
* The max retry times is 3.
* @return true if succeed
*/
public boolean connect() {
boolean ret = false;
FTPClient ftp = getFTPClient();
int times = 1;
while ((times <= 3) && !ret) {
try {
ftp.connect(hostName, port);
ret = ftp.login(user, pass);
} catch (SocketException e) {
} catch (IOException e) {
} finally {
times++;
}
}
return ret;
}
/**
* get the FTPClient instance
* It seems that this method is a nonsense
* at first glance. Actually, this method
* is very important for unit test using
* mock technology.
* @return FTPClient instance
*/
protected FTPClient getFTPClient() {
return new FTPClient();
}
}
cnnect()方法尝试连接FTP服务器并且登录。如果失败了,他可以尝试三次。如果操作成功返回真。否则返回假。这个类使用org.apache.commons.net.FTPClient来生成一个实际的连接。他有一个初看起来毫无用处的保护方法getFTPClient()。实际上这个方法对使用伪技术的单元测试是非常重要的。我会在稍后解释。
一个JUnit测试实例FTPConnectorTest被用来测试connect()方法的逻辑。因为我们想要将单元测试环境从其他因素中(如外部FTP服务器)分离出来,因此我们使用Mocquer来模拟FTPClient。
package org.jingle.mocquer.sample;
import java.io.IOException;
import org.apache.commons.net.ftp.FTPClient;
import org.jingle.mocquer.MockControl;
import junit.framework.TestCase;
public class FTPConnectorTest extends TestCase {
/*
* @see TestCase#setUp()
*/
protected void setUp() throws Exception {
super.setUp();
}
/*
* @see TestCase#tearDown()
*/
protected void tearDown() throws Exception {
super.tearDown();
}
/**
* test FTPConnector.connect()
*/
public final void testConnect() {
//get strict mock control
MockControl control =
MockControl.createStrictControl(
FTPClient.class);
//get mock object
//why final? try to remove it
final FTPClient ftp =
(FTPClient)control.getMock();
//Test point 1
//begin behavior definition
try {
//specify the method invocation
ftp.connect("202.96.69.8", 7010);
//specify the behavior
//throw IOException when call
//connect() with parameters
//"202.96.69.8" and 7010. This method
//should be called exactly three times
control.setThrowable(
new IOException(), 3);
//change to working state
control.replay();
} catch (Exception e) {
fail("Unexpected exception: " + e);
}
//prepare the instance
//the overridden method is the bridge to
//introduce the mock object.
FTPConnector inst = new FTPConnector(
"202.96.69.8",
7010,
"user",
"pass") {
protected FTPClient getFTPClient() {
//do you understand why declare
//the ftp variable as final now?
return ftp;
}
};
//in this case, the connect() should
//return false
assertFalse(inst.connect());
//change to checking state
control.verify();
//Test point 2
try {
//return to preparing state first
control.reset();
//behavior definition
ftp.connect("202.96.69.8", 7010);
control.setThrowable(
new IOException(), 2);
ftp.connect("202.96.69.8", 7010);
control.setVoidCallable(1);
ftp.login("user", "pass");
control.setReturnValue(true, 1);
control.replay();
} catch (Exception e) {
fail("Unexpected exception: " + e);
}
//in this case, the connect() should
//return true
assertTrue(inst.connect());
//verify again
control.verify();
}
}
这里创建了一个严格的MockObject。伪对象变量有一个final修饰符因为变量会在匿名内部类中使用,否则有产生编译错误。
在这个测试方法中包含两个测试点。第一个是什么时候FTPClient.connect()始终抛出异常,也就是说FTPClient.connect()返回假。
try {
ftp.connect("202.96.69.8", 7010);
control.setThrowable(new IOException(), 3);
control.replay();
} catch (Exception e) {
fail("Unexpected exception: " + e);
}
MockControl在调用伪对象connect()方法传入参数202.96.96.8作为主机地址及7010作为端口号时会抛出IOException异常。这个方法调用预期执行三次。在行为定义后,replay()改变伪对象状态为工作态。这里的try/catch块包裹着FTPClient.connect()的定义,因为他定义了抛出IOException异常。
FTPConnector inst = new FTPConnector("202.96.69.8",
7010,
"user",
"pass") {
protected FTPClient getFTPClient() {
return ftp;
}
};
上面的代码创建一个重写了getFTPClient()方法的FTPConnector实例。这样就桥接了创建的伪对象给用来测试的目标。
assertFalse(inst.connect());
在这里预期connect()应该返回假。
control.verify();
最后,改变伪对象到验证态。
第二个测试点是什么时候FTPClient.connect()前两次抛出异常而第三次会成功,这时FTPClient.login()当然也是成功的,这意味着FTPConnector.connect()会返回真。
这个测试点是在前一个测试点之后运行,因此需要将MockObject的状态通过reset()重新置为准备态。
总结
模拟技术将测试的对象从其他外部因素中分离出来。在JUnit框架中集成模拟技术使得单元测试更加简单和优雅。EasyMock是一个好的伪装工具,可以为特定接口创建伪对象。在Dunamis协助下,Mocquer扩展了EasyMock的功能,他可以为类创建伪对象。这篇文章简单介绍了Mocquer在单元测试中的使用。