简体   繁体   English

单元测试室和 LiveData

[英]Unit testing Room and LiveData

I'm currently developing an app using the newly Android Architecture Components .我目前正在使用新的Android Architecture Components开发应用程序。 Specifically, I'm implementing a Room Database that returns a LiveData object on one of its queries.具体来说,我正在实现一个 Room 数据库,它在其中一个查询中返回一个LiveData对象。 Insertion and querying work as expected, however I have an issue testing the query method using a unit test.插入和查询按预期工作,但是我在使用单元测试测试查询方法时遇到问题。

Here is the DAO I'm trying to test:这是我要测试的 DAO:

NotificationDao.kt NotificationDao.kt

@Dao
interface NotificationDao {

    @Insert
    fun insertNotifications(vararg notifications: Notification): List<Long>

    @Query("SELECT * FROM notifications")
    fun getNotifications(): LiveData<List<Notification>>
}

As you can tell, the query function returns a LiveData object, if I change this to be just a List , Cursor , or basically whatever then I get the expected result, which is the data inserted in the Database.如您所知,查询函数返回一个LiveData对象,如果我将其更改为ListCursor或基本上任何内容,那么我会得到预期的结果,即插入数据库中的数据。

The issue is that the following test will always fail because the value of the LiveData object is always null :问题是,下面的测试将始终失败,因为value的的LiveData对象始终是null

NotificationDaoTest.kt 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 
}

So the question is: Does anyone knows of a better way to perform unit tests that involve LiveData objects?所以问题是:有没有人知道执行涉及 LiveData 对象的单元测试的更好方法?

Room calculates the LiveData 's value lazily when there is an observer.当有观察者时,Room 会LiveData地计算LiveData的值。

You can check the sample app .您可以查看示例应用程序

It uses a getValue utility method which adds an observer to get the value:它使用一个getValue实用方法,该方法添加了一个观察者来获取值:

public static <T> T getOrAwaitValue(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];
}

Better w/ kotlin, you can make it an extensions function :).使用 kotlin 更好,您可以将其设为扩展功能 :)。

When you return a LiveData from a Dao in Room it makes the query asynchronously , and as @yigit said Room sets the LiveData#value lazily after you kick off the query by observing the LiveData .当您从 Room 中的Dao返回LiveData ,它会异步执行查询,正如@yigit 所说,Room 在您通过观察LiveData启动查询后懒惰地设置LiveData#value This pattern is reactive .这种模式是被动的

For unit tests you want the behavior to be synchronous , so you must block the test thread and wait for the value to be passed to the observer, then grab it from there and then you can assert on it.对于单元测试,您希望行为是同步的,因此您必须阻塞测试线程并等待将值传递给观察者,然后从那里获取它,然后您可以对其进行断言。

Here's a Kotlin extension function for doing this:这是用于执行此操作的 Kotlin 扩展函数:

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
}

You can use it like this:你可以这样使用它:

val someValue = someDao.getSomeLiveData().blockingObserve()

I found Mockito is very helpful in such case.我发现 Mockito 在这种情况下非常有用。 Here is an example:下面是一个例子:

1.Dependencies 1.依赖

testImplementation "org.mockito:mockito-core:2.11.0"
androidTestImplementation "org.mockito:mockito-android:2.11.0"

2.Database 2.数据库

@Database(
        version = 1,
        exportSchema = false,
        entities = {Todo.class}
)
public abstract class AppDatabase extends RoomDatabase {
    public abstract TodoDao todoDao();
}

3.Dao 3.道

@Dao
public interface TodoDao {
    @Insert(onConflict = REPLACE)
    void insert(Todo todo);

    @Query("SELECT * FROM todo")
    LiveData<List<Todo>> selectAll();
}

4.Test 4.测试

@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));
    }
}

Hope this help!希望这有帮助!

As @Hemant Kaushik said, in this case you SHOULD use InstantTaskExecutorRule .正如@Hemant Kaushik 所说,在这种情况下,您应该使用InstantTaskExecutorRule

From developer.android.com:来自 developer.android.com:

A JUnit Test Rule that swaps the background executor used by the Architecture Components with a different one which executes each task synchronously.一种 JUnit 测试规则,它将架构组件使用的后台执行程序与同步执行每个任务的不同的后台执行程序交换。

It really works!真的行!

Slightly different approach than other answers might be to use https://github.com/jraska/livedata-testing .与其他答案略有不同的方法可能是使用https://github.com/jraska/livedata-testing

You avoid mocking and the test can use API similar to RxJava testing and also you can get advantage from Kotlin extension functions.您可以避免模拟,并且测试可以使用类似于 RxJava 测试的 API,并且您还可以从 Kotlin 扩展功能中获益。

NotificationDaoTest.kt 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 }

If you are using JUnit 5, since rules are not applicable to it, thanks to this article you can manually create the extension:如果您使用的是 JUnit 5,由于规则不适用于它,感谢这篇文章,您可以手动创建扩展:

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)
    }
}

and then in your test class use it like this:然后在您的测试类中像这样使用它:

@ExtendWith(InstantExecutorExtension::class /* , Other extensions */)
class ItemDaoTests {
    ...
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM