单元测试室和 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
对象,如果我将其更改为只是一个 List
、Cursor
或基本上任何内容,那么我得到了预期的结果,这是数据库中插入的数据。
问题是以下测试总是会失败,因为 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 {
...
}
我目前正在使用新 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
对象,如果我将其更改为只是一个 List
、Cursor
或基本上任何内容,那么我得到了预期的结果,这是数据库中插入的数据。
问题是以下测试总是会失败,因为 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 {
...
}