Ich entwickle derzeit eine App, die die neuen Android-Architekturkomponenten verwendet. Im Besonderen implementiere ich eine Raumdatenbank, die ein LiveData
object in einer ihrer Abfragen zurückgibt. Das Einfügen und Abfragen funktioniert wie erwartet, jedoch ist beim Testen der Abfragemethode mit dem Gerätetest ein Problem aufgetreten.
Hier ist die DAO, die ich testen möchte:
NotificationDao.kt
@Dao
interface NotificationDao {
@Insert
fun insertNotifications(vararg notifications: Notification): List<Long>
@Query("SELECT * FROM notifications")
fun getNotifications(): LiveData<List<Notification>>
}
Wie Sie feststellen können, gibt die Query-Funktion ein LiveData
-Objekt zurück. Wenn ich das ändern, um nur eine List
, Cursor
zu sein, oder was auch immer, dann erhalte ich das erwartete Ergebnis, dh die in die Datenbank eingefügten Daten.
Das Problem ist, dass der folgende Test immer fehlschlägt, da die value
des LiveData
-Objekts immer null
ist:
NotificationDaoTest.kt
lateinit var db: SosafeDatabase
lateinit var notificationDao: NotificationDao
@Before
fun setUp() {
val context = InstrumentationRegistry.getTargetContext()
db = Room.inMemoryDatabaseBuilder(context, SosafeDatabase::class.Java).build()
notificationDao = db.notificationDao()
}
@After
@Throws(IOException::class)
fun tearDown() {
db.close()
}
@Test
fun getNotifications_IfNotificationsInserted_ReturnsAListOfNotifications() {
val NUMBER_OF_NOTIFICATIONS = 5
val notifications = Array(NUMBER_OF_NOTIFICATIONS, { i -> createTestNotification(i) })
notificationDao.insertNotifications(*notifications)
val liveData = notificationDao.getNotifications()
val queriedNotifications = liveData.value
if (queriedNotifications != null) {
assertEquals(queriedNotifications.size, NUMBER_OF_NOTIFICATIONS)
} else {
fail()
}
}
private fun createTestNotification(id: Int): Notification {
//method omitted for brevity
}
Die Frage ist also: Weiß jemand, wie man Unit-Tests mit LiveData-Objekten besser durchführt?
Der Raum berechnet den Wert von LiveData
, wenn ein Beobachter vorhanden ist.
Sie können die Beispiel-App überprüfen.
Es verwendet eine getValue utility-Methode, die einen Beobachter hinzufügt, um den Wert zu erhalten:
public static <T> T getValue(final LiveData<T> liveData) throws InterruptedException {
final Object[] data = new Object[1];
final CountDownLatch latch = new CountDownLatch(1);
Observer<T> observer = new Observer<T>() {
@Override
public void onChanged(@Nullable T o) {
data[0] = o;
latch.countDown();
liveData.removeObserver(this);
}
};
liveData.observeForever(observer);
latch.await(2, TimeUnit.SECONDS);
//noinspection unchecked
return (T) data[0];
}
Besser w/kotlin, können Sie es eine Erweiterungsfunktion machen :).
Wenn Sie einen LiveData
von einem Dao
in Room zurückgeben, führt er die Abfrage asynchron aus. Als @yigit besagt, dass Room den LiveData#value
faul nach dem Start der Abfrage setzt, indem Sie den LiveData
beobachten. Dieses Muster ist reaktiv .
Für Unit-Tests möchten Sie, dass das Verhalten synchron ist. Sie müssen den Test-Thread blockieren und warten, bis der Wert an den Beobachter übergeben wird. Dann holen Sie ihn von dort ab, und Sie können dies bestätigen darauf.
Hier ist eine Kotlin-Erweiterungsfunktion:
private fun <T> LiveData<T>.blockingObserve(): T? {
var value: T? = null
val latch = CountDownLatch(1)
val observer = Observer<T> { t ->
value = t
latch.countDown()
}
observeForever(observer)
latch.await(2, TimeUnit.SECONDS)
return value
}
Sie können es so verwenden:
val someValue = someDao.getSomeLiveData().blockingObserve()
Ich fand, dass Mockito in solchen Fällen sehr hilfreich ist. Hier ist ein Beispiel:
1. Abhängigkeiten
testImplementation "org.mockito:mockito-core:2.11.0"
androidTestImplementation "org.mockito:mockito-Android:2.11.0"
2.Datenbank
@Database(
version = 1,
exportSchema = false,
entities = {Todo.class}
)
public abstract class AppDatabase extends RoomDatabase {
public abstract TodoDao todoDao();
}
3.Dao
@Dao
public interface TodoDao {
@Insert(onConflict = REPLACE)
void insert(Todo todo);
@Query("SELECT * FROM todo")
LiveData<List<Todo>> selectAll();
}
4.Test
@RunWith(AndroidJUnit4.class)
public class TodoDaoTest {
@Rule
public TestRule rule = new InstantTaskExecutorRule();
private AppDatabase database;
private TodoDao dao;
@Mock
private Observer<List<Todo>> observer;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
Context context = InstrumentationRegistry.getTargetContext();
database = Room.inMemoryDatabaseBuilder(context, AppDatabase.class)
.allowMainThreadQueries().build();
dao = database.todoDao();
}
@After
public void tearDown() throws Exception {
database.close();
}
@Test
public void insert() throws Exception {
// given
Todo todo = new Todo("12345", "Mockito", "Time to learn something new");
dao.selectAll().observeForever(observer);
// when
dao.insert(todo);
// then
verify(observer).onChanged(Collections.singletonList(todo));
}
}
Ich hoffe das hilft!
Wie @Hemant Kaushik sagte, sollten Sie in diesem Fall InstantTaskExecutorRule
verwenden.
Von developer.Android.com:
Eine JUnit-Testregel, die den Hintergrund-Executor, der von den Architekturkomponenten verwendet wird, durch einen anderen ersetzt, der die einzelnen Aufgaben synchron ausführt.
Es funktioniert wirklich!
Wenn Sie JUnit 5 verwenden, können Sie dank in diesem Artikel die Erweiterung manuell erstellen, da Regeln nicht auf JUnit 5 anwendbar sind:
class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
override fun beforeEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
override fun postToMainThread(runnable: Runnable) = runnable.run()
override fun isMainThread(): Boolean = true
})
}
override fun afterEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance().setDelegate(null)
}
}
und dann in Ihrer Testklasse verwenden Sie es wie folgt:
@ExtendWith(InstantExecutorExtension::class /* , Other extensions */)
class ItemDaoTests {
...
}
Ein etwas anderer Ansatz als bei anderen Antworten könnte die Verwendung von https://github.com/jraska/livedata-testing sein.
Sie vermeiden das Spotteln und der Test kann eine API ähnlich dem RxJava-Test verwenden. Außerdem können Sie die Kotlin-Erweiterungsfunktionen nutzen.
NotificationDaoTest.kt
val liveData = notificationDao.getNotifications()
liveData.test()
.awaitValue() // for the case where we need to wait for async data
.assertValue { it.size == NUMBER_OF_NOTIFICATIONS }