Search code examples
androidandroid-testingsqlbritesqldelight

Errors when testing database repository - SQLBrite, SQLDelight


When testing my DB I get the following errors:

  • SQLiteDiskIOException: disk I/O error (HTC Desire 620)
  • SQLiteReadOnlyDatabaseException: attempt to write a read-only database (Moto g2)

Depending apparently on the device I test it on. The error doesn't happen when I run the app. Still, if I can't test the app there is probably something wrong with my code.

The app uses two libraries which are supposed to go well together SQLDelight and SQLBrite, which might make this question somewhat specific.

For a better understanding of what's going on I'm going to give short description of the files in my data package.

-+-data-+
 |      |-manager-+
 |      |         |-LocationManager
 |      |         |-RunManager
 |      |-model-+
 |      |       |-Location
 |      |       |-Run
 |-DatabaseContract
 |-DataRepository
 |-MyDBHelper

The files Location and Run are row models generated by SQLDelight. LocationManager and RunManager enable generating sqlStatements to insert or remove data from the corresponding table. Below the RunManager the LocationMangager looks similar.

public class RunManager {

    public final Run.InsertRun insertRace;
    public final Run.DeleteRun deleteRun;
    public final Run.DeleteRunWhereTimeSmallerThan deleteRunWhereTimeSmallerThan;

    public RunManager(SQLiteDatabase db) {
        insertRace = new Run.InsertRun(db);
        deleteRun = new Run.DeleteRun(db);
        deleteRunWhereTimeSmallerThan = new Run.DeleteRunWhereTimeSmallerThan(db);
    }

}

The MyDBHelper extends the SQLiteOpenHelper in a standard manner.

public class MyDbHelper extends SQLiteOpenHelper {

    private static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "runner.db";

    private static MyDbHelper INSTANCE = null;

    private MyDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    public static MyDbHelper getInstance(Context context) {
        if (INSTANCE == null) {
            INSTANCE = new MyDbHelper(context);
        }
        return INSTANCE;
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        // create table (omitted)
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        // upgrade table (omitted)
    }
}

The data repository aggregates the various insert and query operations.

public class DataRepository implements DatabaseContract {

    private static DataRepository INSTANCE = null;

    BriteDatabase briteDatabase;
    LocationManger locationManger;
    RunManager runManager;

    private DataRepository(Context context) {
        SqlBrite sqlBrite = new SqlBrite.Builder().build();
        MyDbHelper helper = MyDbHelper.getInstance(context);
        briteDatabase = sqlBrite.wrapDatabaseHelper(helper, Schedulers.io());
        locationManger = new LocationManger(briteDatabase.getWritableDatabase());
        runManager = new RunManager(briteDatabase.getWritableDatabase());
    }

    public static DataRepository getInstance(Context context) {
        if (null == INSTANCE) {
            INSTANCE = new DataRepository(context);
        }
        return INSTANCE;
    }

    @Override
    public Observable<List<Run>> getAllRun() {
        return briteDatabase.createQuery(
                Run.TABLE_NAME,
                Run.SELECT_ALL
        ).mapToList(Run.MAPPER::map);
    }

    @Override
    public Observable<List<Location>> getLocationsForRun(long id) {
        return briteDatabase.createQuery(
                Location.TABLE_NAME, Location.FACTORY.selectAllByRace(id).statement
        ).mapToList(Location.MAPPER::map);
    }

    @Override
    public long insertRun(double distance, long duration, double avgSpeed, long timestamp) {
        runManager.insertRace.bind(distance, duration, avgSpeed, timestamp);
        return briteDatabase.executeInsert(Run.TABLE_NAME, runManager.insertRace.program);
    }

    @Override
    public void deleteRun(long id) {
        runManager.deleteRun.bind(id);
        briteDatabase.executeUpdateDelete(Run.TABLE_NAME, runManager.deleteRun.program);
    }

    @Override
    public void deleteRunWhereTimestampSmallerThan(long timestamp) {
        runManager.deleteRunWhereTimeSmallerThan.bind(timestamp);
        briteDatabase.executeUpdateDelete(Run.TABLE_NAME, runManager.deleteRunWhereTimeSmallerThan.program);
    }

    @Override
    public long insertLocation(long raceId, double lat, double lng, double alt, long timestamp) {
        locationManger.insertLocation.bind(raceId, lat, lng, alt, timestamp);
        return briteDatabase.executeInsert(Location.TABLE_NAME, locationManger.insertLocation.program);
    }

    public Observable<List<SingleRun>> getAllSingleRunModels() {
        return briteDatabase.createQuery(
                Run.TABLE_NAME,
                Run.SELECT_ALL
        ).mapToList(Run.MAPPER::map)
        // omitted

}

Now to the main part of the question. For now, I wrote the following test cases and run into the errors listed at the top. Interestingly enough when I'm running the tests separately I don't get any errors, all tests pass.

@RunWith(AndroidJUnit4.class)
@LargeTest
public class DataRepositoryTest {

    private Context context;
    private DataRepository mDataRepository;

    @Before
    public void setUp() {
        context = InstrumentationRegistry.getTargetContext();
        context.deleteDatabase(MyDbHelper.DATABASE_NAME);
        mDataRepository = DataRepository.getInstance(InstrumentationRegistry.getTargetContext());
    }

    @Test
    public void testPreConditions() {
        Assert.assertNotNull(context);
        Assert.assertNotNull(mDataRepository);
    }

    @Test
    public void testInsertRace() { // this test failes when all tests are run.
        long raceID1 = mDataRepository.insertRun(5.0, 35, 3.5, 1000);
        Assert.assertEquals(1, raceID1);
        long raceID2 = mDataRepository.insertRun(10.0, 70, 3.5, 2000);
        Assert.assertEquals(2, raceID2);
        long locationID1 = mDataRepository.insertLocation(raceID1, 0.5, 0.5, 0, 1000);
        Assert.assertEquals(1, locationID1);
        long locationID2 = mDataRepository.insertLocation(raceID1, 0.5, 0.5, 0, 1001);
        Assert.assertEquals(2, locationID2);
        long locationID3 = mDataRepository.insertLocation(raceID1, 0.5, 0.5, 0, 1002);
        Assert.assertEquals(3, locationID3);
        long locationID4 = mDataRepository.insertLocation(raceID1, 0.5, 0.5, 0, 1003);
        Assert.assertEquals(4, locationID4);
        long locationID5 = mDataRepository.insertLocation(raceID2, 0.5, 0.5, 0, 2000);
        Assert.assertEquals(5, locationID5);
        long locationID6 = mDataRepository.insertLocation(raceID2, 0.5, 0.5, 0, 2001);
        Assert.assertEquals(6, locationID6);
        long locationID7 = mDataRepository.insertLocation(raceID2, 0.5, 0.5, 0, 2002);
        Assert.assertEquals(7, locationID7);
        long locationID8 = mDataRepository.insertLocation(raceID2, 0.5, 0.5, 0, 2003);
        Assert.assertEquals(8, locationID8);
    }

    @Test
    public void testRaceObservable() {
        long raceID1 = mDataRepository.insertRun(5.0, 35, 3.5, 1000);
        Run run1 = Run.FACTORY.creator.create(raceID1, 5.0, 35l, 3.5, 1000l);
        Assert.assertEquals(1, raceID1);
        long raceID2 = mDataRepository.insertRun(10.0, 70, 3.5, 2000);
        Run run2 = Run.FACTORY.creator.create(raceID2, 10.0, 70l, 3.5, 2000l);
        Assert.assertEquals(2, raceID2);
        List<Run> expectedResult = Arrays.asList(run1, run2);
        Assert.assertEquals(expectedResult, mDataRepository.getAllRun().blockingFirst());
    }


}

I assume this has something to do with accessing the DB from different threads, but I don't know how to solve the problem.


Solution

  • Your problem is that when setUp runs the second time, DataRepository.getInstance returns the old data repository, meaning it does not create a new SQLiteOpenHelper. When you delete the database you also need to clean up your singletons for DataRepository and MyDbHelper.

    Alternatively dont use singletons at all:

    @Before
    public void setUp() {
        context = InstrumentationRegistry.getTargetContext();
        context.deleteDatabase(MyDbHelper.DATABASE_NAME);
        mDataRepository = new DataRepository(InstrumentationRegistry.getTargetContext());
    }
    
    // In DataRepository.java
    DataRepository(Context context) {
        SqlBrite sqlBrite = new SqlBrite.Builder().build();
        MyDbHelper helper = new MyDbHelper(context);
        briteDatabase = sqlBrite.wrapDatabaseHelper(helper, Schedulers.io());
        locationManger = new LocationManger(briteDatabase.getWritableDatabase());
        runManager = new RunManager(briteDatabase.getWritableDatabase());
    }
    
    // In MyDbHelper.java
    MyDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }