ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Mock Objects in Unit Tests Mock Objects in Unit Tests

by Lu Jian
01/12/2005

The use of mock objects is a widely employed unit testing strategy. It shields external and unnecessary factors from testing and helps developers focus on a specific function to be tested.

EasyMock is a well-known mock tool that can create a mock object for a given interface at runtime. The mock object's behavior can be defined prior encountering the test code in the test case. EasyMock is based on java.lang.reflect.Proxy, which can create dynamic proxy classes/objects according to given interfaces. But it has an inherent limitation from its use of Proxy: it can create mock objects only for interfaces.

Mocquer is a similar mock tool, but one that extends the functionality of EasyMock to support mock object creation for classes as well as interfaces.

Introduction to Mocquer

Mocquer is based on the Dunamis project, which is used to generate dynamic delegation classes/objects for specific interfaces/classes. For convenience, it follows the class and method naming conventions of EasyMock, but uses a different approach internally.

MockControl is the main class in the Mocquer project. It is used to control the the mock object life cycle and behavior definition. There are four kinds methods in this class.

JUnit Pocket Guide

Related Reading

JUnit Pocket Guide
By Kent Beck

An Example

Below is an example that demonstrates Mocquer's usage in unit testing.

Suppose there is a class named 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();
    }
}

The connect() method can try to connect to an FTP server and log in. If it fails, it can retry up to three times. If the operation succeeds, it returns true. Otherwise, it returns false. The class uses org.apache.commons.net.FTPClient to make a real connection. There is a protected method, getFTPClient(), in this class that looks like nonsense at first glance. Actually, this method is very important for unit testing using mock technology. I will explain that later.

A JUnit test case, FTPConnectorTest, is provided to test the connect() method logic. Because we want to isolate the unit test environment from any other factors such as an external FTP server, we use Mocquer to mock the 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();
    }
}

A strict MockObject is created. The mock object variable declaration has a final modifier because the variable will be used in the inner anonymous class. Otherwise, a compilation error will be reported.

There are two test points in the test method. The first test point is when FTPClient.connect() always throws an exception, meaning FTPConnector.connect() will return false as result.


try {
    ftp.connect("202.96.69.8", 7010);
    control.setThrowable(new IOException(), 3);
    control.replay();
} catch (Exception e) {
    fail("Unexpected exception: " + e);
}

The MockControl specifies that, when calling connect() on the mock object with the parameters 202.96.96.8 as the host IP and 7010 as the port number, an IOException will be thrown. This method invocation is expected to be called exactly three times. After the behavior definition, replay() changes the mock object to the working state. The try/catch block here is to follow the declaration of FTPClient.connect(), which has an IOException defined in its throw clause.


FTPConnector inst = new FTPConnector("202.96.69.8",
                                     7010,
                                     "user",
                                     "pass") {
    protected FTPClient getFTPClient() {
        return ftp;
    }
};

The code above creates a FTPConnector instance with its getFTPClient() overridden. It is a bridge to introduce the created mock object into the target to be tested.


assertFalse(inst.connect());

The expected result of connect() should be false on this test point.


control.verify();

Finally, change the mock object to the checking state.

The second test point is when FTPClient.connect() throws exceptions two times and succeeds on the third time, and FTPClient.login() also succeeds, meaning FTPConnector.connect() will return true as result.

This test point follows the procedure of previous test point, except that the MockObject should change to the preparing state first, using reset().

Conclusion

Mock technology isolates the target to be tested from other external factors. Integrating mock technology into the JUnit framework makes the unit test much simpler and neater. EasyMock is a good mock tool that can create a mock object for a specified interface. With the help of Dunamis, Mocquer extends the function of EasyMock. It can create mock objects not only for interfaces, but also classes. This article gave a brief introduction to Mocquer's usage in unit testing. For more detailed information, please refer to the references below.

References

Lu Jian is a senior Java architect/developer with four years of Java development experience.


Return to ONJava.com

Pragmatic Unit Testing in Java with Junit

Related Reading

Pragmatic Unit Testing in Java with Junit
By Andy Hunt, Dave Thomas

Copyright © 2009 O'Reilly Media, Inc.