wake-up-neo.net

Unit Testing Room und LiveData

Ich entwickle derzeit eine App, die die neuen Android-Architekturkomponenten verwendet. Im Besonderen implementiere ich eine Raumdatenbank, die ein LiveDataobject 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?

33

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 :).

39
yigit

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()
23

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!

10
Hemant Kaushik

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!

5
kissed

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 {
    ...
}
0
Mahozad

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 }
0
Josef Raška