Thursday, July 02, 2009

Android: Testing on the Android platform - Mock objects











This document can be read in Google Docs (http://docs.google.com/View?id=ddwc44gs_186chdgdwdj), cut and paste link if you have problems accessing it.







In a previous post we created a parallel project to hold our tests. Our project is still empty and has only a TestSuite

Now we will explore how to create the actual tests and in this post we will be introducing mock objects.


If we are Test Driven Development purists we may argue about the use of mock objects and be more inclined to use real ones. Martin Fowler calls these two styles the classical and mockist Test Driven Development dichotomy in his great article Mocks aren't stubs.


Temperature Converter

We will be using our well know Temperature Converter application, the same we have used in many other examples, however we will be introducing some modifications and new tests.


 





EditNumber

Temperature Converter uses two fields where temperatures, as signed decimal numbers, can be entered. Let's extend EditText to have a specialized View with this behavior.


Test Driven Development

We are starting writing our tests for our yet inexistent class EditNumber. We decided to extend EditText and we know that EditTexts can add a listener, actually a TextWatcher, to provide methods that are called whenever EditText's text changes.
And this is precisely where we are introducing a mock TextWatcher to check method invocations while text changes.
EasyMock will help us achieve this. This is not an EsyMock tutorial, we will just be analyzing its use in Android, so if you are not familiar with it I would recommend you to take a look at the documentation available in its web site.
Add easymock-2.5.1.jar to the Tests project properties.

testTextChanged

This test will excersise EditNumber behavior checking the method calls on the TextWatcher mock and verify the results.
We are using an InstrumentationTestCase because we are interested in testing EditNumber in isolation of other components or Activities. Later on we will be introducing Functional Tests where we test the whole Activity.
sai and sar are two String arrays containing the inputs and results expected.
We will be using a special Comparator, stringCmp, because we are interested in comparing the String content for different classes used by the Android like Editable, CharSequence, String, etc.

/**
 * EditNumberTests
 */
package com.codtech.android.training.temperatureconverter.tests.view;

import static com.codtech.android.training.temperatureconverter.tests.TestUtils.stringCmp;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.reset;
import static org.easymock.EasyMock.verify;
import android.content.Context;
import android.test.InstrumentationTestCase;
import android.text.Editable;
import android.text.TextWatcher;

import com.codtech.android.training.temperatureconverter.view.EditNumber;

/**
 * @author diego
 *
 */
public class EditNumberTests extends InstrumentationTestCase {

    private static final String TAG = "EditNumberTests";
    private EditNumber editNumber;

    /**
     * @param name
     */
    public EditNumberTests(String name) {
        super();
        setName(name);
    }

    /* (non-Javadoc)
     * @see junit.framework.TestCase#setUp()
     */
    protected void setUp() throws Exception {
        super.setUp();
        final Context context = getInstrumentation().getTargetContext();
        editNumber = new EditNumber(context);
        editNumber.setFocusable(true);
    }

    /* (non-Javadoc)
     * @see android.test.InstrumentationTestCase#tearDown()
     */
    protected void tearDown() throws Exception {
        super.tearDown();
    }
   
    /**
     * testTextChanged
     */
    public final void testTextChanged() {
        String[] sai = new String[] {null, "", "1", "123", "-123", "0",
            "1.2", "-1.2", "1-2-3", "+1", "1.2.3" };

        String[] sar = new String[] {"",   "", "1", "123", "-123", "0",
            "1.2", "-1.2", "123",   "1",  "1.23"  };

        
        // mock
        final TextWatcher watcher = createMock(TextWatcher.class);
        editNumber.addTextChangedListener(watcher);
        
        for (int i=1; i < sai.length; i++) {
            // record
            watcher.beforeTextChanged(stringCmp(sar[i-1]), eq(0),
                eq(sar[i-1].length()), eq(sar[i].length()));

            watcher.onTextChanged(stringCmp(sar[i]), eq(0),
                eq(sar[i-1].length()), eq(sar[i].length()));
                        watcher.afterTextChanged(stringCmp(
                Editable.Factory.getInstance().newEditable(sar[i])));


            // replay
            replay(watcher);

                        // exersise
            editNumber.setText(sai[i]);

            // test
            final String actual = editNumber.getText().toString();
            assertEquals(sai[i] + " => " + sar[i] + " => " + actual, sar[i], actual);

            // verify
            verify(watcher);
            
            // reset
            reset(watcher);
        }
    }
}

stringCmp

This is the Comparator we are using
    public static final class StringComparator<T> implements Comparator<T> {

        /**
         * Constructor
         */
        public StringComparator() {
            super();
        }
        
        /* (non-Javadoc)
         * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
         *
         * Return the {@link String} comparison of the arguments.
         */
        @Override
        public int compare(T object1, T object2) {
            return object1.toString().compareTo(object2.toString());
        }       
    }
    
    /**
     * Return {@link EasyMock.cmp} using a {@link StringComparator} and
     * {@link LogicalOperator.EQUAL}

     *
     * @param <T> The original class of the arguments
     * @param o The argument to the comparison
     * @return {@link EasyMock.cmp}
     */
    public static <T> T stringCmp(T o) {
        return EasyMock.cmp(o, new StringComparator<T>(), LogicalOperator.EQUAL);
    }


Implementing EditNumber

This is what we want for our EditNumber. This is an extremely simple custom View but same techniques can be used in more complicated cases.


/**
 * EditNumber
 */
package com.codtech.android.training.temperatureconverter.view;

import android.content.Context;
import android.text.InputFilter;
import android.text.method.DigitsKeyListener;
import android.util.AttributeSet;
import android.widget.EditText;

/**
 * @author diego
 *
 */
public class EditNumber extends EditText {


    /**
     * @param context
     */
    public EditNumber(Context context) {
        super(context);
        init(null);
    }

    /**
     * @param context
     * @param attrs
     */
    public EditNumber(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs);
    }

    /**
     * @param context
     * @param attrs
     * @param defStyle
     */
    public EditNumber(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(attrs);
    }

    
    private void init(AttributeSet attrs) {
        final InputFilter[] filters = new InputFilter[] {
            DigitsKeyListener.getInstance(true, true) };

        
        setFilters(filters);
    }
   
}



Running the tests

Run the tests. Surprisingly we obtain an error
java.lang.AssertionError:
Unexpected method call onTextChanged(12.3, 0, 1, 4):

onTextChanged(StringComparator<com.codtech.android.training.temperatureconverter.tests.TestUtils.StringComparator>(1.23) == 0, 0, 1, 4): expected: 1, actual: 0
afterTextChanged(StringComparator<com.codtech.android.training.temperatureconverter.tests.TestUtils.StringComparator>(1.23) == 0): expected: 1, actual: 0

at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:43)
at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:72)
at $Proxy0.onTextChanged(Native Method)
at android.widget.TextView.sendOnTextChanged(TextView.java:5905)
at android.widget.TextView.setText(TextView.java:2634)
at android.widget.TextView.setText(TextView.java:2501)
at android.widget.EditText.setText(EditText.java:71)
at android.widget.TextView.setText(TextView.java:2476)
at com.codtech.android.training.temperatureconverter.tests.view.EditNumberTests.testTextChanged(EditNumberTests.java:151)
at java.lang.reflect.Method.invokeNative(Native Method)
at android.test.InstrumentationTestCase.runMethod(InstrumentationTestCase.java:191)
at android.test.InstrumentationTestCase.runTest(InstrumentationTestCase.java:181)
at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:164)
at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:151)
at android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.java:418)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1520)

 When we call editNumber.setText("1.2.3") instead of  the expected "1.23" we obtain "12.3". What really give us the indication of a potential bug is that if we use the UI and type "1.2.3" we obtain the expected value but it differs if we use setText().
Is it a bug in TextView, in DigitsKeyListener, somewhere else ?
We need to find out.


Conclusion



We have seen how sometimes mock objects and particulary EasyMock is able to help us on our mockist flavor Test Driven Development and quickly find bugs that could take much longer to discover using other techniques.


Copyright © 2009 Diego Torres Milano. All rights reserved.



 








Post a Comment