单元测试室和 LiveData

Unit testing Room and LiveData

我目前正在使用新 Android Architecture Components 开发应用程序。具体来说,我正在实施一个房间数据库,该数据库在其查询之一上 returns 一个 LiveData 对象。插入和查询按预期工作,但是我在使用单元测试测试查询方法时遇到问题。

这是我要测试的 DAO:

NotificationDao.kt

@Dao
interface NotificationDao {

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

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

如您所知,查询函数 returns 是一个 LiveData 对象,如果我将其更改为只是一个 ListCursor 或基本上任何内容,那么我得到了预期的结果,这是数据库中插入的数据。

问题是以下测试总是会失败,因为 LiveData 对象的 value 总是 null:

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 
}

所以问题是:有没有人知道执行涉及 LiveData 对象的单元测试的更好方法?

当有观察者时,Room 会延迟计算 LiveData 的值。

您可以检查sample app

它使用 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];
}

使用 kotlin 更好,您可以将其作为扩展函数:)。

当你从 Room 中的 Dao return LiveData 时,它会进行 异步查询 ,正如@yigit 所说,在您通过观察 LiveData 开始查询后,Room 会延迟设置 LiveData#value。这个模式是 reactive.

对于单元测试,您希望行为同步,因此您必须阻塞测试线程并等待值是传递给观察者,然后从那里获取它,然后你可以断言它。

这是一个用于执行此操作的 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
}

你可以这样使用它:

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

我发现 Mockito 在这种情况下非常有用。这是一个例子:

1.Dependencies

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

2.Database

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

希望对您有所帮助!

正如@Hemant Kaushik 所说,在这种情况下你应该 使用InstantTaskExecutorRule

来自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.

真的有用!

与其他答案略有不同的方法可能是使用 https://github.com/jraska/livedata-testing

您可以避免模拟,测试可以使用 API 类似于 RxJava 测试,您还可以从 Kotlin 扩展函数中获益。

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 }

如果您使用的是 JUnit 5,由于规则不适用于它,感谢 this article 您可以手动创建扩展:

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

然后在你的测试中 class 像这样使用它:

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