Room Testing - mariamaged/Java-Android-Kotlin GitHub Wiki
- 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 wasproperly 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))
}
}
}
When testing a database, though, one of the challenges is in making these tests
hermetic
, orself-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 bymemory
andnot disk
.
- There are two key advantages of using an in-memory database for instrumentation testing:
- It is intrinsically self-contained.
- Once the TripDatabase is
closed
orgarbage-collected
, itsmemory is released
, and if separate tests use separate TripDatabase instances, one will not affect the other.
- Once the TripDatabase is
- 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.
- It is intrinsically self-contained.
- 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.
- 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:
- Loading the data from some
neutral format (e.g., JSON)
via some utility method. - Packaging one or more starter databases as
assests
in theinstrumentation 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.
- Loading the data from some
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
.
- So that you can write