webermaster webermaster - 1 month ago 14
Android Question

Android Instrumented Test Database magically becomes read-only in @Before

I have been working through some exercises to learn android. The sample project I put together runs fine. But, when I run all of the Instrumented Tests together, the tests for my content provider fail because the database is read-only when deletes are issued to the database. When I run the test class separately, the tests pass with flying colors. My

ContentProvider
test looks like so:

public class TestProvider {

public static final String LOG_TAG = TestProvider.class.getSimpleName();

public void deleteAllRecordsFromProvider() {
InstrumentationRegistry.getTargetContext()
.getContentResolver().delete(
WeatherEntry.CONTENT_URI,
null,
null
);
InstrumentationRegistry.getTargetContext()
.getContentResolver().delete(
LocationEntry.CONTENT_URI,
null,
null
);

Cursor cursor = InstrumentationRegistry.getTargetContext()
.getContentResolver().query(
WeatherEntry.CONTENT_URI,
null,
null,
null,
null
);
assertEquals("Error: Records not deleted from Weather table during delete", 0, cursor.getCount());
cursor.close();

cursor = InstrumentationRegistry.getTargetContext()
.getContentResolver().query(
LocationEntry.CONTENT_URI,
null,
null,
null,
null
);
assertEquals("Error: Records not deleted from Location table during delete", 0, cursor.getCount());
cursor.close();
}

@Before
public void setUp() throws Exception {
deleteAllRecordsFromProvider();
}

@After
public void after() {
InstrumentationRegistry.getTargetContext()
.getContentResolver()
.acquireContentProviderClient(WeatherEntry.CONTENT_URI)
.getLocalContentProvider()
.shutdown();
}

@Test
public void testProviderRegistry() {
PackageManager pm = InstrumentationRegistry.getTargetContext().getPackageManager();

// We define the component name based on the package name from the context and the
// WeatherProvider class.
ComponentName componentName = new ComponentName(InstrumentationRegistry.getTargetContext().getPackageName(),
WeatherProvider.class.getName());
try {
// Fetch the provider info using the component name from the PackageManager
// This throws an exception if the provider isn't registered.
ProviderInfo providerInfo = pm.getProviderInfo(componentName, 0);

// Make sure that the registered authority matches the authority from the Contract.
assertEquals("Error: WeatherProvider registered with authority: " + providerInfo.authority +
" instead of authority: " + WeatherContract.CONTENT_AUTHORITY,
providerInfo.authority, WeatherContract.CONTENT_AUTHORITY);
} catch (PackageManager.NameNotFoundException e) {
// I guess the provider isn't registered correctly.
assertTrue("Error: WeatherProvider not registered at " + InstrumentationRegistry.getTargetContext().getPackageName(),
false);
}
}

@Test
public void testGetType() {
// content://com.example.android.sunshine.app/weather/
String type = InstrumentationRegistry.getTargetContext()
.getContentResolver()
.getType(WeatherEntry.CONTENT_URI);
// vnd.android.cursor.dir/com.example.android.sunshine.app/weather
assertEquals("Error: the WeatherEntry CONTENT_URI should return WeatherEntry.CONTENT_TYPE",
WeatherEntry.CONTENT_TYPE, type);

String testLocation = "94074";
// content://com.example.android.sunshine.app/weather/94074
type = InstrumentationRegistry.getTargetContext().getContentResolver().getType(
WeatherEntry.buildWeatherLocation(testLocation));
// vnd.android.cursor.dir/com.example.android.sunshine.app/weather
assertEquals("Error: the WeatherEntry CONTENT_URI with location should return WeatherEntry.CONTENT_TYPE",
WeatherEntry.CONTENT_TYPE, type);

long testDate = 1419120000L; // December 21st, 2014
// content://com.example.android.sunshine.app/weather/94074/20140612
type = InstrumentationRegistry.getTargetContext().getContentResolver().getType(
WeatherEntry.buildWeatherLocationWithDate(testLocation, testDate));
// vnd.android.cursor.item/com.example.android.sunshine.app/weather/1419120000
assertEquals("Error: the WeatherEntry CONTENT_URI with location and date should return WeatherEntry.CONTENT_ITEM_TYPE",
WeatherEntry.CONTENT_ITEM_TYPE, type);

// content://com.example.android.sunshine.app/location/
type = InstrumentationRegistry.getTargetContext().getContentResolver().getType(LocationEntry.CONTENT_URI);
// vnd.android.cursor.dir/com.example.android.sunshine.app/location
assertEquals("Error: the LocationEntry CONTENT_URI should return LocationEntry.CONTENT_TYPE",
LocationEntry.CONTENT_TYPE, type);
}

@Test
public void testBasicWeatherQuery() {
// insert our test records into the database
WeatherDbHelper dbHelper = new WeatherDbHelper(InstrumentationRegistry.getTargetContext());
SQLiteDatabase db = dbHelper.getWritableDatabase();

ContentValues testValues = TestUtilities.createNorthPoleLocationValues();
long locationRowId = TestUtilities.insertNorthPoleLocationValues(InstrumentationRegistry.getTargetContext());

// Fantastic. Now that we have a location, add some weather!
ContentValues weatherValues = TestUtilities.createWeatherValues(locationRowId);

long weatherRowId = db.insert(WeatherEntry.TABLE_NAME, null, weatherValues);
assertTrue("Unable to Insert WeatherEntry into the Database", weatherRowId != -1);

// Test the basic content provider query
Cursor weatherCursor = InstrumentationRegistry.getTargetContext().getContentResolver().query(
WeatherEntry.CONTENT_URI,
null,
null,
null,
null
);

// Make sure we get the correct cursor out of the database
TestUtilities.validateCursor("testBasicWeatherQuery", weatherCursor, weatherValues);
weatherCursor.close();
}

@Test
public void testBasicLocationQueries() {
// insert our test records into the database
WeatherDbHelper dbHelper = new WeatherDbHelper(InstrumentationRegistry.getTargetContext());
SQLiteDatabase db = dbHelper.getWritableDatabase();

ContentValues testValues = TestUtilities.createNorthPoleLocationValues();
long locationRowId = TestUtilities.insertNorthPoleLocationValues(InstrumentationRegistry.getTargetContext());

// Test the basic content provider query
Cursor locationCursor = InstrumentationRegistry.getTargetContext()
.getContentResolver()
.query(
LocationEntry.CONTENT_URI,
null,
null,
null,
null
);

// Make sure we get the correct cursor out of the database
TestUtilities.validateCursor("testBasicLocationQueries, location query", locationCursor, testValues);

// Has the NotificationUri been set correctly? --- we can only test this easily against API
// level 19 or greater because getNotificationUri was added in API level 19.
if ( Build.VERSION.SDK_INT >= 19 ) {
assertEquals("Error: Location Query did not properly set NotificationUri",
locationCursor.getNotificationUri(), LocationEntry.CONTENT_URI);
}
locationCursor.close();
}

@Test
public void testUpdateLocation() {
// Create a new map of values, where column names are the keys
ContentValues values = TestUtilities.createNorthPoleLocationValues();

Uri locationUri = InstrumentationRegistry.getTargetContext().getContentResolver().
insert(LocationEntry.CONTENT_URI, values);
long locationRowId = ContentUris.parseId(locationUri);

// Verify we got a row back.
assertTrue(locationRowId != -1);
Log.d(LOG_TAG, "New row id: " + locationRowId);

ContentValues updatedValues = new ContentValues(values);
updatedValues.put(LocationEntry._ID, locationRowId);
updatedValues.put(LocationEntry.COLUMN_CITY_NAME, "Santa's Village");

// Create a cursor with observer to make sure that the content provider is notifying
// the observers as expected
Cursor locationCursor = InstrumentationRegistry.getTargetContext().getContentResolver().query(LocationEntry.CONTENT_URI, null, null, null, null);

TestUtilities.TestContentObserver tco = TestUtilities.getTestContentObserver();
locationCursor.registerContentObserver(tco);

int count = InstrumentationRegistry.getTargetContext().getContentResolver().update(
LocationEntry.CONTENT_URI, updatedValues, LocationEntry._ID + "= ?",
new String[]{Long.toString(locationRowId)});
assertEquals(count, 1);

// Test to make sure our observer is called. If not, we throw an assertion.
//
// Students: If your code is failing here, it means that your content provider
// isn't calling getContext().getContentResolver().notifyChange(uri, null);
tco.waitForNotificationOrFail();

locationCursor.unregisterContentObserver(tco);
locationCursor.close();

// A cursor is your primary interface to the query results.
Cursor cursor = InstrumentationRegistry.getTargetContext().getContentResolver().query(
LocationEntry.CONTENT_URI,
null, // projection
LocationEntry._ID + " = " + locationRowId,
null, // Values for the "where" clause
null // sort order
);

TestUtilities.validateCursor("testUpdateLocation. Error validating location entry update.",
cursor, updatedValues);

cursor.close();
}

@Test
public void testInsertReadProvider() {
ContentValues testValues = TestUtilities.createNorthPoleLocationValues();

// Register a content observer for our insert. This time, directly with the content resolver
TestUtilities.TestContentObserver tco = TestUtilities.getTestContentObserver();
InstrumentationRegistry.getTargetContext().getContentResolver().registerContentObserver(LocationEntry.CONTENT_URI, true, tco);
Uri locationUri = InstrumentationRegistry.getTargetContext().getContentResolver().insert(LocationEntry.CONTENT_URI, testValues);

// Did our content observer get called? Students: If this fails, your insert location
// isn't calling getContext().getContentResolver().notifyChange(uri, null);
tco.waitForNotificationOrFail();
InstrumentationRegistry.getTargetContext().getContentResolver().unregisterContentObserver(tco);

long locationRowId = ContentUris.parseId(locationUri);

// Verify we got a row back.
assertTrue(locationRowId != -1);

// Data's inserted. IN THEORY. Now pull some out to stare at it and verify it made
// the round trip.

// A cursor is your primary interface to the query results.
Cursor cursor = InstrumentationRegistry.getTargetContext().getContentResolver().query(
LocationEntry.CONTENT_URI,
null, // leaving "columns" null just returns all the columns.
null, // cols for "where" clause
null, // values for "where" clause
null // sort order
);

TestUtilities.validateCursor("testInsertReadProvider. Error validating LocationEntry.",
cursor, testValues);
cursor.close();

// Fantastic. Now that we have a location, add some weather!
ContentValues weatherValues = TestUtilities.createWeatherValues(locationRowId);
// The TestContentObserver is a one-shot class
tco = TestUtilities.getTestContentObserver();

InstrumentationRegistry.getTargetContext().getContentResolver().registerContentObserver(WeatherEntry.CONTENT_URI, true, tco);

Uri weatherInsertUri = InstrumentationRegistry.getTargetContext().getContentResolver()
.insert(WeatherEntry.CONTENT_URI, weatherValues);
assertTrue(weatherInsertUri != null);

// Did our content observer get called? Students: If this fails, your insert weather
// in your ContentProvider isn't calling
// getContext().getContentResolver().notifyChange(uri, null);
tco.waitForNotificationOrFail();
InstrumentationRegistry.getTargetContext().getContentResolver().unregisterContentObserver(tco);

// A cursor is your primary interface to the query results.
Cursor weatherCursor = InstrumentationRegistry.getTargetContext().getContentResolver().query(
WeatherEntry.CONTENT_URI, // Table to Query
null, // leaving "columns" null just returns all the columns.
null, // cols for "where" clause
null, // values for "where" clause
null // columns to group by
);

TestUtilities.validateCursor("testInsertReadProvider. Error validating WeatherEntry insert.",
weatherCursor, weatherValues);

// Add the location values in with the weather data so that we can make
// sure that the join worked and we actually get all the values back
weatherValues.putAll(testValues);
weatherCursor.close();
// Get the joined Weather and Location data
weatherCursor = InstrumentationRegistry.getTargetContext().getContentResolver().query(
WeatherEntry.buildWeatherLocation(TestUtilities.TEST_LOCATION),
null, // leaving "columns" null just returns all the columns.
null, // cols for "where" clause
null, // values for "where" clause
null // sort order
);
TestUtilities.validateCursor("testInsertReadProvider. Error validating joined Weather and Location Data.",
weatherCursor, weatherValues);
weatherCursor.close();
// Get the joined Weather and Location data with a start date
weatherCursor = InstrumentationRegistry.getTargetContext().getContentResolver().query(
WeatherEntry.buildWeatherLocationWithStartDate(
TestUtilities.TEST_LOCATION, TestUtilities.TEST_DATE),
null, // leaving "columns" null just returns all the columns.
null, // cols for "where" clause
null, // values for "where" clause
null // sort order
);
TestUtilities.validateCursor("testInsertReadProvider. Error validating joined Weather and Location Data with start date.",
weatherCursor, weatherValues);
weatherCursor.close();
// Get the joined Weather data for a specific date
weatherCursor = InstrumentationRegistry.getTargetContext().getContentResolver().query(
WeatherEntry.buildWeatherLocationWithDate(TestUtilities.TEST_LOCATION, TestUtilities.TEST_DATE),
null,
null,
null,
null
);
TestUtilities.validateCursor("testInsertReadProvider. Error validating joined Weather and Location data for a specific date.",
weatherCursor, weatherValues);
weatherCursor.close();
}

@Test
public void testDeleteRecords() {
testInsertReadProvider();

// Register a content observer for our location delete.
TestUtilities.TestContentObserver locationObserver = TestUtilities.getTestContentObserver();
InstrumentationRegistry.getTargetContext().getContentResolver().registerContentObserver(LocationEntry.CONTENT_URI, true, locationObserver);

// Register a content observer for our weather delete.
TestUtilities.TestContentObserver weatherObserver = TestUtilities.getTestContentObserver();
InstrumentationRegistry.getTargetContext().getContentResolver().registerContentObserver(WeatherEntry.CONTENT_URI, true, weatherObserver);

deleteAllRecordsFromProvider();

// Students: If either of these fail, you most-likely are not calling the
// getContext().getContentResolver().notifyChange(uri, null); in the ContentProvider
// delete. (only if the insertReadProvider is succeeding)
locationObserver.waitForNotificationOrFail();
weatherObserver.waitForNotificationOrFail();

InstrumentationRegistry.getTargetContext().getContentResolver().unregisterContentObserver(locationObserver);
InstrumentationRegistry.getTargetContext().getContentResolver().unregisterContentObserver(weatherObserver);
}


static private final int BULK_INSERT_RECORDS_TO_INSERT = 10;

static ContentValues[] createBulkInsertWeatherValues(long locationRowId) {
long currentTestDate = TestUtilities.TEST_DATE;
long millisecondsInADay = 1000 * 60 * 60 * 24;
ContentValues[] returnContentValues = new ContentValues[BULK_INSERT_RECORDS_TO_INSERT];

for (int i = 0; i < BULK_INSERT_RECORDS_TO_INSERT; i++, currentTestDate += millisecondsInADay) {
ContentValues weatherValues = new ContentValues();
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_LOC_KEY, locationRowId);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_DATE, currentTestDate);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_DEGREES, 1.1);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_HUMIDITY, 1.2 + 0.01 * (float) i);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_PRESSURE, 1.3 - 0.01 * (float) i);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_MAX_TEMP, 75 + i);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_MIN_TEMP, 65 - i);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_SHORT_DESC, "Asteroids");
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_WIND_SPEED, 5.5 + 0.2 * (float) i);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_WEATHER_ID, 321);
returnContentValues[i] = weatherValues;
}
return returnContentValues;
}


I have one other test that uses the
InstrumentationRegistry
:

public class TestFetchWeatherTask {
static final String ADD_LOCATION_SETTING = "Sunnydale, CA";
static final String ADD_LOCATION_CITY = "Sunnydale";
static final Double ADD_LOCATION_LAT = 34.425833;
static final Double ADD_LOCATION_LON = -119.714167;

@Test
public void testAddLocation() {
// start from a clean state
InstrumentationRegistry.getTargetContext()
.getContentResolver()
.delete(WeatherContract.LocationEntry.CONTENT_URI,
WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ?",
new String[]{ADD_LOCATION_SETTING});

FetchWeatherTask fwt = new FetchWeatherTask(InstrumentationRegistry.getTargetContext(), null);
long locationId = fwt.addLocation(ADD_LOCATION_SETTING, ADD_LOCATION_CITY,
ADD_LOCATION_LAT, ADD_LOCATION_LON);

// does addLocation return a valid record ID?
assertFalse("Error: addLocation returned an invalid ID on insert", locationId == -1);

// test all this twice
for ( int i = 0; i < 2; i++ ) {

// does the ID point to our location?
Cursor locationCursor = InstrumentationRegistry.getTargetContext()
.getContentResolver().query(WeatherContract.LocationEntry.CONTENT_URI,
new String[]{
WeatherContract.LocationEntry._ID,
WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING,
WeatherContract.LocationEntry.COLUMN_CITY_NAME,
WeatherContract.LocationEntry.COLUMN_COORD_LAT,
WeatherContract.LocationEntry.COLUMN_COORD_LONG
},
WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ?",
new String[]{ADD_LOCATION_SETTING},
null);

// these match the indices of the projection
if (locationCursor.moveToFirst()) {
assertEquals("Error: the queried value of locationId does not match the returned value" +
"from addLocation", locationCursor.getLong(0), locationId);
assertEquals("Error: the queried value of location setting is incorrect",
locationCursor.getString(1), ADD_LOCATION_SETTING);
assertEquals("Error: the queried value of location city is incorrect",
locationCursor.getString(2), ADD_LOCATION_CITY);
assertEquals("Error: the queried value of latitude is incorrect",
Double.valueOf(locationCursor.getDouble(3)), ADD_LOCATION_LAT);
assertEquals("Error: the queried value of longitude is incorrect",
Double.valueOf(locationCursor.getDouble(4)), ADD_LOCATION_LON);
} else {
fail("Error: the id you used to query returned an empty cursor");
}

// there should be no more records
assertFalse("Error: there should be only one record returned from a location query",
locationCursor.moveToNext());

// add the location again
long newLocationId = fwt.addLocation(ADD_LOCATION_SETTING, ADD_LOCATION_CITY,
ADD_LOCATION_LAT, ADD_LOCATION_LON);

assertEquals("Error: inserting a location again should return the same ID",
locationId, newLocationId);
locationCursor.close();
}
// reset our state back to normal
InstrumentationRegistry.getTargetContext()
.getContentResolver()
.delete(WeatherContract.LocationEntry.CONTENT_URI,
WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ?",
new String[]{ADD_LOCATION_SETTING});

InstrumentationRegistry.getTargetContext()
.getContentResolver()
.acquireContentProviderClient(WeatherContract.LocationEntry.CONTENT_URI)
.getLocalContentProvider()
.shutdown();
}
}


If I comment out this test, the
TestProvider
class passes. If I don't, all of the
TestProvider
tests fail with the same error:


I/TestRunner: android.database.sqlite.SQLiteReadOnlyDatabaseException:
attempt to write a readonly database (code 1032)


Can anyone help me figure out what is going on with my tests? Why is my database magically becoming read-only? I have googled around with no luck.

Answer

To test a ContentProvider you should create a test that extends ProviderTestCase2, add the @RunWith(AndroidJUnit4.class) annotation at the beginning of the test class definition, specify the test runner as AndroidJUnitRunner and annotate every test with @Test.

Then, inject the Context

@Override
protected void setUp() throws Exception {
    setContext(InstrumentationRegistry.getTargetContext());
    super.setUp();
}

and run your tests from Studio.

You can learn more in this lesson.