单元测试室和LiveData

Nic*_*sco 44 android unit-testing kotlin android-room android-architecture-components

我目前正在使用新的Android架构组件开发应用程序.具体来说,我正在实现一个房间数据库,LiveData它在其中一个查询上返回一个对象.插入和查询按预期工作,但是我在使用单元测试测试查询方法时遇到问题.

这是我试图测试的DAO:

NotificationDao.kt

@Dao
interface NotificationDao {

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

@Query("SELECT * FROM notifications")
fun getNotifications(): LiveData<List<Notification>>
Run Code Online (Sandbox Code Playgroud)

}

正如你所知,查询函数返回一个LiveData对象,如果我将其更改为a List,Cursor或者基本上是什么,然后我得到预期结果,即插入数据库中的数据.

问题是,下面的测试将始终失败,因为value该的LiveData对象始终是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 
}
Run Code Online (Sandbox Code Playgroud)

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

yig*_*git 48

LiveData当有观察者时,房间懒洋洋地计算它的值.

您可以查看示例应用.

它使用getValue实用程序方法,该方法添加一个观察者来获取值:

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];
}
Run Code Online (Sandbox Code Playgroud)

更好w/kotlin,你可以使它成为扩展功能:).


Chr*_*rry 27

当你LiveData从一个Dao房间里返回一个它时,它会异步地进行查询,并且@yigit表示LiveData#value在你通过观察开始查询后,房间懒得设置LiveData.这种模式是反应性的.

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

这是一个用于执行此操作的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
}
Run Code Online (Sandbox Code Playgroud)

你可以像这样使用它:

val someValue = someDao.getSomeLiveData().blockingObserve()
Run Code Online (Sandbox Code Playgroud)

  • @Rabbit我遇到了同样的问题,并通过在我的测试类中添加`@Rule @JvmField public valrule = InstantTaskExecutorRule()`来解决它,这需要`androidTestImplementation "android.arch.core:core-testing:1.1.1"`依赖性。现在可以了! (6认同)
  • 我尝试遵循此解决方案,但收到以下错误,“ IllegalStateException:无法在后台线程上调用observeForever” (3认同)

Hem*_*hik 13

我发现Mockito在这种情况下非常有帮助.这是一个例子:

1.Dependencies

testImplementation "org.mockito:mockito-core:2.11.0"
androidTestImplementation "org.mockito:mockito-android:2.11.0"
Run Code Online (Sandbox Code Playgroud)

2.Database

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

3.DAO

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

    @Query("SELECT * FROM todo")
    LiveData<List<Todo>> selectAll();
}
Run Code Online (Sandbox Code Playgroud)

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));
    }
}
Run Code Online (Sandbox Code Playgroud)

希望这有帮助!

  • 诀窍是使用`InstantTaskExecutorRule`来强制LiveData即时更新.我很想念,谢谢! (2认同)

kis*_*sed 7

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

来自developer.android.com:

一个JUnit测试规则,它将架构组件使用的后台执行程序与另一个同步执行每个任务的后台执行程序交换.

真的行!


小智 5

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

您可以避免模拟,并且测试可以使用类似于 RxJava 测试的 API,并且您还可以从 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 }
Run Code Online (Sandbox Code Playgroud)

  • 对于任何寻求更有效/快速的方法来解决此问题的人来说,只要看看这个答案即可。只需导入他的包(不要忘记标记您正在使用它的测试文件夹的实现)并按照上面的代码进行操作。相信我,这可以解决很多问题。 (3认同)