Android TDD Series: Test-Driving Data Access Part 1 - SQLiteOpenHelper
It’s been a while since my last post in my Android TDD series, and I think it is time for the next entry. Previously we discussed test-driving view classes in Android, including Activities and Fragments. This laid the foundation for displaying content to the user. Having done that we can now focus on providing that content.
Data access in Android is not overly complex, however there are several core components you’ll need to understand and be able to write tests for. In the upcoming entries for this series we’ll focus on data access layer and it’s components. These will include (from the “bottom” of the stack up): SQLiteOpenHelper, ContentProviders, and the LoaderManager callback mechanisms. Each of these components will get you a step closer to providing data to the view layers.
Out of the box Android supports using a SQLite database to store and manage data. While you have other options available for data access I can’t say I have any experience with these. Furthermore, it is always good to have a foundational knowledge of what the framework is actually doing, and the SQLiteOpenHelper provides basic facilities for database management.
Lets examine the SQLiteOpenHelper. This class is responsible for creating and upgrading your database. It also gives you an optional hook for opening the database. We are primarily going to be interested in the first two use cases, creating and upgrading your database. The first step we will take is to create a class which extends SQLiteOpenHelper (I’ll use an example from my open-source app, The Grocery Reminder):
package com.groceryreminder.data;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
public class ReminderDBHelper extends SQLiteOpenHelper{
public ReminderDBHelper(Context applicationContext) {
super(applicationContext, "some name", null, 1);
}
@Override
public void onCreate(SQLiteDatabase db) {
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
Notice we are overriding two methods here to satisfy the implementation of the abstract class, SQLiteOpenHelper. The first is the onCreate()
method, which is where we will implement the logic to create our database. Lets look at how we can set up our first test for this class.
package com.groceryreminder.data;
import android.database.sqlite.SQLiteDatabase;
import com.groceryreminder.BuildConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowApplication;
import static org.junit.Assert.assertEquals;
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class ReminderDBHelperTest {
private static final String REMINDER_DATABASE_NAME = "grocery_reminder.sqlite";
private ReminderDBHelper dbHelper;
@Test
public void whenTheDBHelperIsCreatedThenTheDatabaseNameShouldBeSet() {
ShadowApplication context = Shadows.shadowOf(RuntimeEnvironment.application);
dbHelper = new ReminderDBHelper(context.getApplicationContext());
assertEquals(REMINDER_DATABASE_NAME, dbHelper.getDatabaseName());
}
}
Our first test expected that the database name is set correctly. This is accomplished in the constructor of the ReminderDBHelper, which calls the super class constructor. Lets get that test passing.
//snip
public class ReminderDBHelper extends SQLiteOpenHelper{
//snip
private static final String REMINDER_DATABASE_NAME = "grocery_reminder.sqlite";
public ReminderDBHelper(Context applicationContext) {
super(applicationContext, REMINDER_DATABASE_NAME, null, 1);
}
//snip
}
Now that we have given our database a name lets move on to test-driving the creation of the tables we’re interested in.
@Test
public void whenTheDBHelperIsCreatedThenTheRemindersTableShouldBeCreated() {
ContentValues reminderValues = new ContentValues();
//Here ReminderContract.Reminders.DESCRIPTION is a column name in the database.
reminderValues.put(ReminderContract.Reminders.DESCRIPTION, ReminderContract.Reminders.DESCRIPTION);
insertReminderValues(reminderValues);
SQLiteDatabase readableDatabase = dbHelper.getReadableDatabase();
//Here ReminderContract.Reminders.PROJECT_ALL is an array of Strings representing the database columns we need
Cursor cursor = readableDatabase.query("reminders", ReminderContract.Reminders.PROJECT_ALL, "", null, null, null,
//ReminderContract.Reminders.SORT_ORDER_DEFAULT is a String representing the ORDER BY clause in a SQL query, minus
//the ORDER BY.
ReminderContract.Reminders.SORT_ORDER_DEFAULT, null);
assertTrue(cursor.moveToNext());
assertEquals(1, cursor.getInt(0));
assertEquals(ReminderContract.Reminders.DESCRIPTION, cursor.getString(1));
cursor.close();
cursor = null;
}
private void insertReminderValues(ContentValues reminderValues) {
SQLiteDatabase writableDatabase = dbHelper.getWritableDatabase();
writableDatabase.insert("reminders", "", reminderValues);
}
So, we’re doing a lot in this test. First, you’ll notice that nowhere in this test do we actually call onCreate()
. This is because the onCreate()
method is called when the SQLiteOpenHelper is created via the constructor as part of the Android framework. The next thing you’ll notice is that we get a handle on a writeable database to insert values into it, and finally we perform a query on a readable version of the database to get the values back out. This is how we test that the table we are about to create actually exists. Now we’ll implement the onCreate()
method.
//snip
private static final String CREATE_REMINDERS_TABLE_SQL =
"CREATE TABLE reminders (" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" description varchar(200) " +
" )";
@Override
public void onCreate(SQLiteDatabase db) {
Log.d("ReminderDBHelper", "Creating the reminders table.");
db.execSQL(CREATE_REMINDERS_TABLE_SQL);
}
//snip
We now have a table called reminders two fields: id and description and our test should now pass. A very similar operation can be performed for the onUpgrade()
method, with the exception that you’ll need to pass in a writeable database and call onUpgrade()
directly (see ReminderDBHelperTest for a full example).
This is the essential pattern for testing the an implementation of the SQLiteOpenHelper, which is the first step in providing data access in Android. The next step we will take will be implementing a ContentProvider, which acts as a standard interface and wrapper to hide data access implementation details from the rest of the app.
Thanks again and happy database creation!