Mocking Classes

Ulrik Sandberg

Mocking a class rather than an interface might present some interesting obstacles. Perhaps you have ran into the dreaded:

Unexpected method call toString():
  toString(): expected: 0, actual: 1

You think nothing of it and happily add an expectation for toString. Then you get:

Unexpected method call toString():
  toString(): expected: 1, actual: 2

The reason is that EasyMock itself calls toString for each expectation, so it's a never-ending story. However, it appears only when your class under test has overridden any (or all) of toString, equals or hashCode. There is a way around it. You can tell EasyMock's MockClassControl which methods you want it to mock. Yes, you heard it right: "which methods you want it to mock". I know, you'd rather want to tell it which methods you don't want it to mock, like toString, equals and hashCode.

I'll show you a couple of utility classes that let you do that. We ran into this problem in a previous project and wrote the classes below. They let you get away with this neat code that should be used whenever you mock a class:

public class MyClassTest extends TestCase {
   private MockControl myClassControl;
   private MyClass myClassMock;
 
   public void setUp(){
      myClassControl = TestUtils.createClassControl(MyClass.class);
      myClassMock = (MyClass) myClassControl.getMock();
   }
   ...
}

Here are the classes:

package se.jayway.testutils; 
 
import org.easymock.MockControl; 
 
/**
 * Various utility functions for testing.
 *
 * @author Adam Skogman
 */
public final class TestUtils { 
 
    private TestUtils() {
        super();
    } 
 
    /**
     * Get a MockClassControl for the supplied class, using the first found
     * public or protected constructor and not mocking the toString(), equals()
     * and hashCode() methods.
     *
     * @param cls
     *            the class to create a MockClassControl for.
     * @return a MockClassControl for the class.
     * @see MockClassControlHelper
     */
    public static MockControl getMockClassControl(Class cls) {
        return getMockClassControl(
                cls,
                MockClassControlHelper.DONT_MOCK_STD_METHODS);
    } 
 
    /**
     * Get a MockClassControl for the supplied class, using the first found
     * public or protected constructor. If mockStdMethods is false, the
     * toString(), equals() and hashCode() methods will not be mocked.
     *
     * @param cls
     *            the class to create a MockClassControl for.
     * @param mockStdMethods
     *            whether toString(), equals() and hashCode() will be mocked.
     * @return a MockClassControl for the class.
     * @see MockClassControlHelper
     */
    public static MockControl getMockClassControl(
            Class cls,
            boolean mockStdMethods) {
        MockClassControlHelper helper = new MockClassControlHelper(
                mockStdMethods);
        return helper.getMockClassControl(cls);
    }
}
package se.jayway.testutils; 
 
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set; 
 
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.easymock.MockControl;
import org.easymock.classextension.MockClassControl; 
 
/**
 * Helper class to easily create mock class controls.
 *
 * EasyMockClassExtension v1.1 has a couple of undocumented 'features' which are
 * rather annoying.
 *
 * First of all, if the class we want to mock does not have any public
 * constructor, it is not possible to do
 * MockClassControl.createControl(MyClass.class), since the search for
 * constructors is limited to 'public' access modifier if class and parameter
 * values are not specified.
 *
 * Secondly, if toString(), equals() or hashCode() is overridden in the class we
 * want to mock, parameter matching on the resulting mock will not work, since
 * the above methods will be mocked by default, causing mock control validation
 * to fail.
 *
 * As a solution this helper class will by default instruct MockClassControl not
 * to mock toString(), equals() and hashCode() (alternatively any methods
 * specified by a call to setStdMethods()).
 *
 * Methods in Object will not be mocked.
 *
 * @author Mattias Arthursson
 */
public class MockClassControlHelper {
    Log log = LogFactory.getLog(MockClassControlHelper.class); 
 
    /**
     * Indicates that standard methods will be mocked.
     */
    public static final boolean MOCK_STD_METHODS = true; 
 
    /**
     * Indicates that standard methods will be mocked.
     */
    public static final boolean DONT_MOCK_STD_METHODS = false; 
 
    private boolean mockStdMethods; 
 
    private Collection stdMethods; 
 
    /**
     * Initialize to not mock standard methods.
     */
    public MockClassControlHelper() {
        this(DONT_MOCK_STD_METHODS);
    } 
 
    /**
     * Initialize to mock standard methods depending on supplied parameter.
     *
     * @param mockStdMethods
     *            whether standard methods should be mocked.
     */
    public MockClassControlHelper(boolean mockStdMethods) {
        this.mockStdMethods = mockStdMethods;
        stdMethods = Arrays.asList(new String[] { "toString", "equals",
                "hashCode" });
    } 
 
    /**
     * Get a MockClassControl for the supplied class using the first found
     * public or protected constructor. Standard methods will be mocked
     * depending on mockStdMethods.
     *
     * @param cls
     *            the class to mock.
     * @return a MockClassControl for the supplied class.
     */
    public MockControl getMockClassControl(Class cls) {
        Constructor constructorToUse = getConstructorToUse(cls); 
 
        Class[] parameters = constructorToUse.getParameterTypes();
        Object[] values = new Object[parameters.length]; 
 
        Method[] methodsToMock = getMethodsToMock(cls); 
 
        return MockClassControl.createControl(
                cls,
                parameters,
                values,
                methodsToMock);
    } 
 
    /**
     * Set the standard methods to omit if mockStdMethods is set to false
     * (default).
     *
     * @param methods
     *            Names of the methods to omit when mocking.
     */
    public void setStdMethods(String[] methods) {
        stdMethods = Arrays.asList(methods);
    } 
 
    /**
     * Set the standard methods to omit if mockStdMethods is set to false
     * (default).
     *
     * @param c
     *            Collection of names of the methods to omit when mocking.
     */
    public void setStdMethods(Collection c) {
        stdMethods = c;
    } 
 
    /**
     * Get the methods that will not be mocked.
     *
     * @return A collection of method names.
     */
    public Collection getStdMethods() {
        return stdMethods;
    } 
 
    /**
     * Find a constructor to use. If public constructors are found, the first of
     * these is selected. Otherwise we check for a protected constructor, and
     * the first of these is used.
     *
     * @param cls
     *            Class to select a constructor from.
     * @return a public or protected constructor for the class.
     */
    protected Constructor getConstructorToUse(Class cls) {
        Constructor[] constructors = cls.getConstructors();
        if (constructors.length == 0) {
            // No public constructors found.
            constructors = cls.getDeclaredConstructors();
            if (constructors.length == 0) {
                throw new IllegalArgumentException("Class '" + cls
                        + "' does not have any constructors");
            }
        } 
 
        for (int i = 0; i < constructors.length; i++) {
            int modifiers = constructors[i].getModifiers();
            if (Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers)) { 
 
                // We've found a constructor which is public or
                // protected - use that one.
                return constructors[i];
            }
        } 
 
        // No public or protected constructor found.
        throw new IllegalArgumentException("Class '" + cls
                + "' does not have any public or protected constructors");
    } 
 
    /**
     * Figure out which methods of the class to mock. If mockStdMethods is set
     * to false, the standard methods are subtracted from all found methods for
     * the class. Otherwise all methods for the class is returned.
     *
     * @param cls
     *            The class to get methods for.
     * @return The methods to mock for the class.
     */
    protected Method[] getMethodsToMock(Class cls) { 
 
        Class currentClass = cls;
        Set methods = new HashSet(); 
 
        while (currentClass != null) {
            Method[] methodArray = currentClass.getDeclaredMethods(); 
 
            for (int i = 0; i < methodArray.length; i++) {
                Method method = methodArray[i];
                boolean isStandardMethod = stdMethods.contains(method
                        .getName());
                if (mockStdMethods || !isStandardMethod) {
                    methods.add(method);
                } 
 
                if ((method.getModifiers() & Modifier.FINAL) > 0) {
                    log.warn("Final method, cannot be mocked: " + method);
                }
            } 
 
            currentClass = currentClass.getSuperclass();
            if(currentClass == Object.class){
                currentClass = null;
            }
        } 
 
        return (Method[]) methods.toArray(new Method[methods.size()]);
    }
}

Ulrik Sandberg
Consultant at Jayway

Tags: ,

0 comments ↓

There are no comments yet...Kick things off by filling out the form below.

Leave a Comment