Monday, January 2, 2012

Android unit testing with test database

In order to make your tests use a different database to your live one, have your test class extend ProviderTestCase2:

    public class MyProviderTest  extends ProviderTestCase2

Override two constructors:
    public MyProviderTest()
    {
        this(MyContentProvider.class, "my_authority_from_manifest");
    }


    public MyProviderTest(Class providerClass, String providerAuthority)
    {
        super(providerClass, providerAuthority);
    }


What this does is, in the setup() method, creates a IsolatedContext instead of the usual Context. Additionally, the base Context of this IsolatedCopntext is a RenamingDelegateContext. This class contains a field to store a prefix for the name of your database. It defauls to "test.".


In your tests, wherever you need a context object, call getMockContext(). This will return the IsolatedContext.

Sunday, January 1, 2012

Unit Testing Android When ContentProvider is Used

I've been having 'fun' and games with unit tests and ContentProviders this week :-( I thought it would be good practise to take an app which directly accesses a db for it's data, and to instead implement a ContentProvider and access the data that way. Touchy feely android and all that.

The tutorial I used was here and it all seemed fairly straight forward, although authority/uris were new to me.

My problems started when I came to changing my 'unit' tests (quoted as REAL unit tests wouldn't touch the db, of course). I had lots of failing tests! I picked one out to concentrate on:

    public void testAddNewVehicle()
{
Vehicle vehicle1 = new Vehicle("xyz", "my car", 111f);
long result = VehicleProvider.addVehicle(getContext(), vehicle1);

assertTrue(1 == result);

ArrayList vehicles = VehicleProvider.getVehicles(getContext());

assertEquals(1, vehicles.size());
assertEquals("xyz", vehicles.get(0).getRegistrationNo());
assertEquals("my car", vehicles.get(0).getDescription());
assertEquals(111.0f, vehicles.get(0).getInitialMileage());
}

So, it creates then adds a vehicle to the db, then retrieves the vehicle(s) from the db and asserts against it. This test failed at the:
    assertEquals(1, vehicles.size());
line. Hmmm... so the insert worked, but the retrieval didn't. Nice. Even nicer, the test passed in 'run' mode (as opposed to debug, in eclipse). Joy.

I ended up opening a command prompt at the database directory of the emulator. What I found was the database didn't exist on the file system after the insertion of the vehicle, so although the insertion was successful, when getVehicles() came along and retrieved data from the db, it didn't exist. As part of the getVehicles(), getWriteableDatabase() is called which creates the database if it does not exist. So getVehicles() would create a new db and of course it was empty, and the vehicles.size() was 0, not 1.

A couple of weeks ago I played about with viewing the Android source in eclipse, so I thought I'd make use of that ability here. I downloaded the 1.6 source (the app was running on 1.6) and stepped through my test. What I found (with the help of logging in onCreate() of the ContentProvider*) was that when the test is run, something extra happens; ContentProvider.onCreate() is called before the test begins. This is perhaps part of the testAndroidTestCaseSetupProperly() test that automatically gets added to test cases in Android.

This seemed to partially initialise the db. What this means is that the first time I try to access the db, Android thinks the db is already up and available and doesn't go through the full initialisation process. This is an excerpt from the Android source for getWriteableDatabase() in SQLiteOpenHelper:

    public synchronized SQLiteDatabase getWritableDatabase() {
        if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) {
            return mDatabase;  // The database is already open for business
        }
        //lots more initialisation here
    }


What was happening in the if statement was that mDatabase was not null and was open. So Android was saying, there you go, here's your database.

But why the difference in 'run' mode. The logging in the onCreate() of the ContentProvider showed that it was being executed once pre-test, once during the test and one other time between these two! I don't know what was going here or why this 'hid' the issue I'd found in debug mode, I was just happy to be making progress in fixing things.

Here's the logcat from debug:

01-01 20:29:49.024: D/UKMPG(1554): onCreate in ContentProvider
01-01 20:29:51.565: D/UKMPG(1572): onCreate in ContentProvider

and from run:
01-01 20:30:37.505: D/UKMPG(1612): onCreate in ContentProvider
01-01 20:30:39.884: D/UKMPG(1631): onCreate in ContentProvider
01-01 20:30:39.904: D/UKMPG(1631): onCreate in ContentProvider

It's not displayed here, but the very first entry for both run and debug does not contain my application package name, which I assume means it was not run by my app, but by the testAndroidTestCaseSetupProperly() Android method. Notice it also is executed in a different thread. So for some reason when using 'run' to run the test, onCreate is executed twice by my app...


I did manage to fix the test by making sure the db was closed before exiting the onCreate in the ContentProvider, something they didn't do in the tutorial. That's my excuse!

Happily, things work as expected in run and debug modes for the application itself - just a single call to onCreate int the ContentProvider :-)

My next task is to work out how to use the Android mock classes to for the tests to use a test db.

* It's not possible to hit break points in this pre-test run

Some related Stackoverflow questions:
http://stackoverflow.com/questions/8679041/android-unit-test-fails-in-debug-passes-otherwise
http://stackoverflow.com/questions/8688252/what-does-testandroidtestcasesetupproperly-do

NOTE: out of interest I just had a look at the source for the testAndroidTestCaseSetupProperly() method. All it does is check that the Context object is not null:

public void testAndroidTestCaseSetupProperly() {
        assertNotNull("Context is null. setContext should be called before tests are run",
                mContext);        
    }