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

0 comments ↓
There are no comments yet...Kick things off by filling out the form below.
Leave a Comment