JMock for TestNG (or JUnit-free JMock)

JMock for TestNG
Last days I have created a small utility class that would allow me to work with JMock without depending on JUnit. If you develop tests using TestNG and you need mocks a la JMock, than here it is (I have removed the imports so that the listing doesn’t go too long):

/**
 * An utility class to allow static usage of JMock (removes the restriction
 * to subclass JMock specific classes).
 * 
 * @author Alexandru Popescu
 */
public class JMock {
    static final Constraint ANYTHING = new IsAnything();
    private static final WeakHashMap<Thread, List<Verifiable>> s_mockObjects = 
            new WeakHashMap<Thread, List<Verifiable>>();
    
    /**
     * Creates a mock object that mocks the given type.  
     * The mock object is named after the type;  the exact
     * name is calculated by {@link #defaultMockNameForType}.
     *
     * @param mockedType The type to be mocked.
     * @return A {@link Mock} object that mocks <var>mockedType</var>.
     */
    public static Mock mock(Class mockedType) {
        return mock(mockedType, defaultMockNameForType(mockedType));
    }

    /**
     * Creates a mock object that mocks the given type and is explicitly given a name.
     * The mock object is named after the type;  
     * the exact name is calculated by {@link #defaultMockNameForType}.
     *
     * @param mockedType The type to be mocked.
     * @param roleName The name of the mock object
     * @return A {@link Mock} object that mocks <var>mockedType</var>.
     */
    public static Mock mock( Class mockedType, String roleName ) {
        Mock newMock = new Mock(newCoreMock(mockedType, roleName));
        registerToVerify(newMock);
        return newMock;
    }
    
    public static Mock mock(Class mockedClass, 
                            String roleName, 
                            Class[] constructorArgumentTypes, 
                            Object[] constructorArguments) {
        Mock newMock = new Mock(newClassCoreMock(mockedClass, 
                                                 roleName, 
                                                 constructorArgumentTypes, 
                                                 constructorArguments));
        registerToVerify(newMock);
        return newMock;
    }

    public static Mock mock(Class mockedClass, 
                            Class[] constructorArgumentTypes, 
                            Object[] constructorArguments) {
        return mock(mockedClass, 
                    defaultMockNameForType(mockedClass), 
                    constructorArgumentTypes, 
                    constructorArguments);
    }

    public static Stub returnValue(Object o) {
        return new ReturnStub(o);
    }

    public static Stub returnValue(boolean result ) {
        return returnValue(new Boolean(result));
    }

    public static Stub returnValue(byte result ) {
        return returnValue(new Byte(result));
    }

    public static Stub returnValue(char result ) {
        return returnValue(new Character(result));
    }

    public static Stub returnValue(short result ) {
        return returnValue(new Short(result));
    }

    public static Stub returnValue(int result ) {
        return returnValue(new Integer(result));
    }

    public static Stub returnValue(long result ) {
        return returnValue(new Long(result));
    }

    public static Stub returnValue(float result ) {
        return returnValue(new Float(result));
    }

    public static Stub returnValue(double result ) {
        return returnValue(new Double(result));
    }

    public static Stub returnIterator(Collection collection) {
        return new ReturnIteratorStub(collection);
    }
    
    public static Stub returnIterator(Object[] array) {
        return new ReturnIteratorStub(array);
    }
    
    public static Stub throwException(Throwable throwable) {
        return new ThrowStub(throwable);
    }

    public static InvocationMatcher once() {
        return new InvokeOnceMatcher();
    }

    public static InvocationMatcher atLeastOnce() {
        return new InvokeAtLeastOnceMatcher();
    }

    public static InvocationMatcher exactly(int expectedCount) {
        return new InvokeCountMatcher(expectedCount);
    }
    
    public static InvocationMatcher never() {
        return new TestFailureMatcher("not expected");
    }

    public static InvocationMatcher never( String errorMessage ) {
        return new TestFailureMatcher("not expected ("+errorMessage+")");
    }
    
    public static Stub onConsecutiveCalls(Stub... stubs) {
        return new StubSequence(stubs);
    }
    
    public static Stub doAll(Stub... stubs) {
        return new DoAllStub(stubs);
    }
    
    public static IsEqual eq(Object operand) {
        return new IsEqual(operand);
    }

    public static IsEqual eq(boolean operand ) {
        return eq(new Boolean(operand));
    }

    public static IsEqual eq(byte operand ) {
        return eq(new Byte(operand));
    }

    public static IsEqual eq(short operand ) {
        return eq(new Short(operand));
    }

    public static IsEqual eq(char operand ) {
        return eq(new Character(operand));
    }

    public static IsEqual eq(int operand ) {
        return eq(new Integer(operand));
    }

    public static IsEqual eq(long operand ) {
        return eq(new Long(operand));
    }

    public static IsEqual eq(float operand ) {
        return eq(new Float(operand));
    }

    public static IsEqual eq(double operand ) {
        return eq(new Double(operand));
    }

    public static IsCloseTo eq( double operand, double error ) {
        return new IsCloseTo(operand, error);
    }

    public static IsSame same( Object operand ) {
        return new IsSame(operand);
    }

    public static IsInstanceOf isA( Class operandClass ) {
        return new IsInstanceOf(operandClass);
    }

    public static StringContains stringContains(String substring) {
        return new StringContains(substring);
    }

    public static StringContains contains(String substring) {
        return stringContains(substring);
    }
    
    public static StringStartsWith startsWith(String substring ) {
        return new StringStartsWith(substring);
    }
    
    public static StringEndsWith endsWith(String substring ) {
        return new StringEndsWith(substring);
    }
    
    public static IsNot not(Constraint c ) {
        return new IsNot(c);
    }

    public static And and(Constraint left, Constraint right ) {
        return new And(left, right);
    }

    public static Or or(Constraint left, Constraint right ) {
        return new Or(left, right);
    }

    /// HINT: not sure about these following 3
    public static Object newDummy( Class dummyType ) {
        return Dummy.newDummy(dummyType);
    }

    /// HINT: what is this?
    public static Object newDummy( Class dummyType, String name ) {
        return Dummy.newDummy(dummyType, name);
    }

    /// HINT: what is this?
    public static Object newDummy( String name ) {
        return Dummy.newDummy(name);
    }
    
    public static void assertThat(Object actual, Constraint constraint) {
        if (!constraint.eval(actual)) {
            StringBuffer message = new StringBuffer("\nExpected: ");
            constraint.describeTo(message);
            message.append("\n    got : ").append(actual).append('\n');
            throw new AssertionError(message);
        }
    }

    public static void assertThat(boolean actual, Constraint constraint) {
        assertThat(new Boolean(actual), constraint);
    }

    public static void assertThat(byte actual, Constraint constraint) {
        assertThat(new Byte(actual), constraint);
    }

    public static void assertThat(short actual, Constraint constraint) {
        assertThat(new Short(actual), constraint);
    }

    public static void assertThat(char actual, Constraint constraint) {
        assertThat(new Character(actual), constraint);
    }

    public static void assertThat(int actual, Constraint constraint) {
        assertThat(new Integer(actual), constraint);
    }

    public static void assertThat(long actual, Constraint constraint) {
        assertThat(new Long(actual), constraint);
    }

    public static void assertThat(float actual, Constraint constraint) {
        assertThat(new Float(actual), constraint);
    }

    public static void assertThat(double actual, Constraint constraint) {
        assertThat(new Double(actual), constraint);
    }
    
    public static HasPropertyWithValue hasProperty(String propertyName, Constraint expectation) {
        return new HasPropertyWithValue(propertyName, expectation);
    }
    
    public static HasProperty hasProperty(String propertyName) {
       return new HasProperty(propertyName);
    }
    
    public static HasToString toString(Constraint toStringConstraint) {
        return new HasToString(toStringConstraint);
    }
    
    public static IsCompatibleType compatibleType(Class baseType) {
        return new IsCompatibleType(baseType);
    }
    
    public static IsIn isIn(Collection collection) {
        return new IsIn(collection);
    }
    
    public static IsIn isIn(Object[] array) {
        return new IsIn(array);
    }
    
    public static IsCollectionContaining collectionContaining(Constraint elementConstraint) {
        return new IsCollectionContaining(elementConstraint);
    }
    
    public static IsCollectionContaining collectionContaining(Object element) {
        return collectionContaining(eq(element));
    }
    
    public static IsArrayContaining arrayContaining(Constraint elementConstraint) {
        return new IsArrayContaining(elementConstraint);
    }

    public static IsArrayContaining arrayContaining(Object element) {
        return arrayContaining(eq(element));
    }

    public static IsArrayContaining arrayContaining(boolean element) {
        return arrayContaining(new Boolean(element));
    }

    public static IsArrayContaining arrayContaining(byte element) {
        return arrayContaining(new Byte(element));
    }

    public static IsArrayContaining arrayContaining(short element) {
        return arrayContaining(new Short(element));
    }

    public static IsArrayContaining arrayContaining(char element) {
        return arrayContaining(new Character(element));
    }

    public static IsArrayContaining arrayContaining(int element) {
        return arrayContaining(new Integer(element));
    }

    public static IsArrayContaining arrayContaining(long element) {
        return arrayContaining(new Long(element));
    }

    public static IsArrayContaining arrayContaining(float element) {
        return arrayContaining(new Float(element));
    }

    public static IsArrayContaining arrayContaining(double element) {
        return arrayContaining(new Double(element));
    }
    
    public static IsMapContaining mapContaining(Constraint keyConstraint, Constraint valueConstraint) {
        return new IsMapContaining(keyConstraint, valueConstraint);
    }
    
    public static IsMapContaining mapContaining(Object key, Object value) {
        return mapContaining(eq(key), eq(value));
    }

    public static IsMapContaining mapWithKey(Object key) {
        return mapWithKey(eq(key));
    }

    public static IsMapContaining mapWithKey(Constraint keyConstraint) {
        return new IsMapContaining(keyConstraint, ANYTHING);
    }

    public static IsMapContaining mapWithValue(Object value) {
        return mapWithValue(eq(value));
    }

    public static IsMapContaining mapWithValue(Constraint valueConstraint) {
        return new IsMapContaining(ANYTHING, valueConstraint);
    }

    /**
     * Verify the expected behavior for the mock registered by the current thread.
     */
    public static void verify() {
        List<Verifiable> mocks = s_mockObjects.get(Thread.currentThread());
        if(null != mocks) {
            for(Verifiable verifiable : mocks) {
                verifiable.verify();
            }
        }
    }
    
    /**
     * Verify the expected behavior for the mocks defined as fields of the arguement object.
     * 
     * @param object the object to be inspected
     */
    public static void verifyObject(Object object) {
        Verifier.verifyObject(object);
    }

    /**
     * Helper method that delegates to {@link #verify()} and {@link verifyObject(Object)}.
     */
    public static void verifyAll(Object object) {
        verify();
        verifyObject(object);
    }
    
    /**
     * Verify the expected behavior for the mocks registered by the current thread and
     * also releases them.
     */
    public static synchronized void verifyAndRelease() {
        Thread currentThread= Thread.currentThread();
        List<Verifiable> mocks = s_mockObjects.get(currentThread);
        if(null != mocks) {
            for(Verifiable verifiable : mocks) {
                verifiable.verify();
            }
        }
        mocks.clear();
        s_mockObjects.put(currentThread, null);
    }
    
    /**
     * Helper method delegating to {@link #verifyAndRelease()} and {@link #verifyObject(Object)}.
     */
    public static void verifyAllAndRelese(Object object) {
        verifyAndRelease();
        verifyObject(object);
    }
    
    ///
    private static synchronized void registerToVerify(Verifiable verifiable) {
        List<Verifiable> mocks = s_mockObjects.get(Thread.currentThread());
        if(null == mocks) {
            mocks = new ArrayList<Verifiable>();
            s_mockObjects.put(Thread.currentThread(), mocks);
        }
        
        mocks.add(verifiable);
    }

    private static DynamicMock newCoreMock(Class mockedType, String roleName ) {
        return new CoreMock(mockedType, roleName);
    }

    private static DynamicMock newClassCoreMock(Class mockedClass, 
                                                String roleName,
                                                Class[] constructorArgumentTypes, 
                                                Object[] constructorArguments) {
        return new CGLIBCoreMock(mockedClass, roleName, constructorArgumentTypes, constructorArguments);
    }

    /**
     * Calculates a default role name for a mocked type.
     * @param mockedType
     * @return
     */
    private static String defaultMockNameForType(Class mockedType) {
        return "mock" + Formatting.classShortName(mockedType);
    }
}

It works for mocking both interfaces and classes (through CGLIB).
And here is a short example of usage (JDK5, because I use static imports):

public class TypeAccessFilterTest {
    @Test
    public void jmockBotNormalRequest() throws IOException, ServletException {
        // static imports
        Mock mockBotChecker = mock(BotChecker.class, new Class[0], new Object[0]);
        Mock mockFilterChain= mock(FilterChain.class);
        TypeAccessFilter taf= new TypeAccessFilter();
        taf.setBotChecker((BotChecker) mockBotChecker.proxy());
        
        mockFilterChain.expects(once()).method("doFilter")
                       .with(isA(HttpServletRequest.class),
                             isA(JSessionidBeautifierHttpResponseWrapper.class));
        
        taf.doFilter(m_mockRequest, m_mockResponse, (FilterChain) mockFilterChain.proxy());
        
        verifyAndRelease();
    }

As far as I know the latest versions of EasyMock allow the same JUnit-free usage, which in my opinion is a great thing.

Advertisements

9 Comments

Filed under Uncategorized

9 responses to “JMock for TestNG (or JUnit-free JMock)

  1. Matt Raible

    It’d be great to see this added to either jMock or TestNG.

  2. the_mindstorm

    I agree with you, that’s why I made it public. IMO the best place for it would be jMock, but if it will not go in there, than I guess we will be able to integrate/distribute it along with TestNG (this may lead to version problems, but I will think about it)

  3. Cedric

    Hi Matt,

    Yes, we’ll add this file in the distribution in its own directory, next to spring/ and maven/.

    Thanks Alex!


    Cedric

  4. Eugene Kuleshov

    One of the nice things about JUnit integration is that it does mock verification and release automatically for all mocks you created within given test case.

  5. the_mindstorm

    Eugene, this little verification/release benefit comes at the cost of mandatory inheritance. You can achieve the same behavior with TestNG – JMock, and without this cost: declare an @AfterMethod and just do JMock.verifyAndRelease().

    ./alex

    .w( the_mindstorm )p.

  6. Joe Walnes

    Great! I’ll see to it that this makes it into jMock. As a recent TestNG convertee, I really need this!

    It’s a shame it can’t be done without the static/thread/weakhashmap thingy. It would nice if TestNG allowed you to programmatically register a block of code to be called on cleanup. This would also avoid the need for the explicit call to verifyAndRelease().

    What happens if an exception is raised during the test and verifyAndRelease() isn’t called. Will those mocks get carried over to the next test to run in the same thread?

    I’ll also play with integration for Ham Crest, which is an attempt to build a richer constraint/matcher library, leveraging Java5 features and bridging the gap between JUnit3, JUnit4, TestNG, jMock, EasyMock2 and assertThat.

    cheers
    -joe

  7. Andres Almiray

    Hi Alex, this is great, but it uses generics, what about developers that can’t use JDK5 ?

    I too did something similar, but my intent was to reuse JMock for JUnit4. Now that I now that TestNG is JUnit friendly and can use the MockTester with TestNG without changing a single line of code. TestNG rocks! I love the synergy it provides.

    BTW, my approach is described at
    http://jroller.com/page/aalmiray/?anchor=using_jmock_without_mockobjecttestcase
    http://jira.codehaus.org/browse/JMOCK-24

    in case you are interested

  8. the_mindstorm

    The dependency on JDK5 are quite minimal and I really think you can very easily rewrite it to be JDK5 free.

    ./alex

    .w( the_mindstorm )p.

  9. Brandon

    How did you get past the dependencies on junit.framework.Assert and junit.framework.AssertionFailedException? I see them in quite a few places within jMock.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s