Room Testing - mariamaged/Java-Android-Kotlin GitHub Wiki

Testing Room

Writing Instrumentation Tests

  • On the whole, writing instrumented tests for Room - where the tests run on an Android device or emulator - is unremarkable.
  • You get an instance of RoomDatabase of your RoomDatabase subclass and exercise it from there.
  • Java Example:
package com.mariamaged.android.room.room1;  
  
import androidx.test.ext.junit.runners.AndroidJUnit4;  
import androidx.test.platform.app.InstrumentationRegistry;  
  
import org.junit.After;  
import org.junit.Before;  
import org.junit.Test;  
import org.junit.runner.RunWith;  
  
import java.util.List;  
  
import static junit.framework.TestCase.assertTrue;  
import static org.junit.Assert.assertEquals;  
import static org.junit.Assert.assertNotEquals;  
import static org.junit.Assert.assertNotNull;  
  
@RunWith(AndroidJUnit4.class)  
public class TripTests {  
    private TripDatabase db;  
    private TripStore store;  
  
  @Before  
  public void setUp() {  
        db = TripDatabase.create(InstrumentationRegistry.getInstrumentation().getTargetContext(), true);  
        store = db.tripStore();  
    }  
  
  @After  
  public void tearDown() {  
        db.close();  
    }  
  
  @Test  
  public void basics() {  
        assertEquals(0, store.selectAll().size());  
  
        final Trip first = new Trip("Foo", 2880);  
  
        assertNotNull(first.id);  
        assertNotEquals(0, first.id.length());  
        store.insert(first);  
  
        assertTrip(store, first);  
  
        final Trip updated = new Trip(first.id, "Foo!!", 1440);  
        store.update(updated);  
        assertTrip(store, updated);  
  
        store.delete(updated);  
        assertEquals(0, store.selectAll().size());  
  
    }  
  
    private void assertTrip(TripStore store, Trip trip) {  
        List<Trip> results = store.selectAll();  
  
        assertNotNull(results);  
        assertEquals(1, results.size());  
        assertTrue(areIdentical(trip, results.get(0)));  
  
        Trip result = store.findById(trip.id);  
  
        assertNotNull(result);  
        assertTrue(areIdentical(trip, result));  
    }  
  
    private boolean areIdentical(Trip one, Trip two) {  
        return (one.id.equals(two.id)) &&  
                (one.title.equals(two.id)) &&  
                (one.duration == two.duration);  
    }  
}
  • Here, we:
    • Create an empty database.
    • Get the DAO (TripStore).
    • Confirm that there are no trips in the database.
    • Create a Trip object and insert() it into the database, then confirm that the database was properly inserted.
    • Create a new Trip object with the same ID as the first, update() the database using it, then confirm that the database was properly modified.
    • Delete the Trip object, then confirm that the database has no trips once again.
       
       
  • Kotlin example:
package com.mariamaged.android.roomKotlin  
  
import androidx.room.Room  
import androidx.test.ext.junit.runners.AndroidJUnit4  
import androidx.test.platform.app.InstrumentationRegistry  
import com.natpryce.hamkrest.assertion.assertThat  
import com.natpryce.hamkrest.equalTo  
import com.natpryce.hamkrest.hasSize  
import com.natpryce.hamkrest.isEmpty  
import org.junit.Test  
import org.junit.runner.RunWith  
import java.util.*  
  
  
@RunWith(AndroidJUnit4::class)  
class NoteStoreTest {  
    private val db = Room.inMemoryDatabaseBuilder(  
        InstrumentationRegistry.getInstrumentation().targetContext,  
        NoteDatabase::class.java  
  )  
        .build()  
  
    private val underTest = db.notes()  
  
  @Test  
  fun insertAndDelete() {  
        assertThat(underTest.loadAll(), isEmpty)  
  
        val entity = NoteEntity (  
            id = UUID.randomUUID().toString(),  
            title = "This is a title",  
            text = "This is some text",  
            version = 1  
  )  
  
        underTest.insert(entity)  
        underTest.loadAll().let {  
	   assertThat(it, hasSize(equalTo(1)))  
           assertThat(it[0], equalTo(entity))  
        }  
  
	underTest.delete(entity)  
        assertThat(underTest.loadAll(), isEmpty)  
    }  
  
  @Test  
  fun update() {  
        val entity = NoteEntity(  
            id = UUID.randomUUID().toString(),  
            title = "This is a title",  
            text = "This is some text",  
            version = 1  
  )  
  
        underTest.insert(entity)  
  
        val updated = entity.copy(title = "This is new", text = "So is this")  
  
        underTest.update(updated)  
        underTest.loadAll().let {  
	    assertThat(it, hasSize(equalTo(1)))  
            assertThat(it[0], equalTo(updated))  
        }  
  }  
}

Using In-Memory Databases

When testing a database, though, one of the challenges is in making these tests hermetic, or self-contained.

  • One test method should not depend upon another test method.
  • And one test method should not affect the results of another test method accidentally.

This means that we want to start with a known starting point before each test, and we have to consider how to do that.

  • One approach- is to use an in-memory database.
  • The static create() method on TripDatabase, if you pass true for the second parameter, creates a TripDatabase backed by memory and not disk.
     
  • There are two key advantages of using an in-memory database for instrumentation testing:
    1. It is intrinsically self-contained.
      • Once the TripDatabase is closed or garbage-collected, its memory is released, and if separate tests use separate TripDatabase instances, one will not affect the other.
    2. Reading and writing to and from memory is much faster than is reading and writing to and from disk, so the tests run much faster.
       
  • On the other hand, this means that instrumented tests are useless for performance tuning, as (presumably) your production app will actually store its database on disk.

Importing Starter Data

  • The one downside to having an empty starter database, such as fresh-in memory, is that you have no data.
  • Eventually, you need some data to test.
     
  • Alternatives include:
    1. Loading the data from some neutral format (e.g., JSON) via some utility method.
    2. Packaging one or more starter databases as assests in the instrumentation tests (e.g., src/androidTest/assest/), then using ATTACH DATABASE ... and INSERT INTO ... SELECT FROM ... SQLite to copy from the starter database to the database to be used in testing.

Writing Unit Tests via MOCKs

package com.mariamaged.android.room.room1;  
  
import androidx.room.Dao;  
import androidx.room.Delete;  
import androidx.room.Insert;  
import androidx.room.Query;  
import androidx.room.Update;  
  
import java.util.List;  
  
@Dao  
public interface TripStore {  
    @Query("Select * FROM trips ORDER BY title")  
    List<Trip> selectAll();  
  
    @Query("SELECT * from trips WHERE id = :id")  
    Trip findById(String id);  
  
    @Insert  
    void insert(Trip... trips);  
  
    @Update  
    void update(Trip... trips);  
  
    @Delete  
    void delete(Trip... trips);  
  
}
  • This is a pure interface.
  • More importantly, other than annotations, its API is purely domain-specific.
  • Everything revolves around our Trip entity and other business logic (e.g., String values as identifiers).
     
  • Room DAOs are designed to be mocked, using a mocking library like Mockito.
    • So that you can write units tests (tests that run on your development machine) in addition to - or perhaps instead of - instrumentation tests.
⚠️ **GitHub.com Fallback** ⚠️