技术开发 频道

单元测试中的伪对象

    一个例子
    下面是一个在单元测试中演示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在单元测试中的使用。

0
相关文章