現在、新たに Android Architecture Components を使用してアプリを開発しています。具体的には、クエリの1つでLiveData
objectを返すルームデータベースを実装しています。挿入とクエリは期待どおりに機能しますが、ユニットテストを使用してクエリメソッドをテストする際に問題があります。
ここに私がテストしようとしているDAOがあります:
NotificationDao.kt
@Dao
interface NotificationDao {
@Insert
fun insertNotifications(vararg notifications: Notification): List<Long>
@Query("SELECT * FROM notifications")
fun getNotifications(): LiveData<List<Notification>>
}
おわかりのように、クエリ関数はLiveData
オブジェクトを返します。これを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
}
質問は次のとおりです:LiveDataオブジェクトを含む単体テストを実行するより良い方法を知っている人はいますか?
観測者がいる場合、Roomは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];
}
より良いw/kotlin、あなたはそれを拡張機能にすることができます:)。
ルームのLiveData
からDao
を返すと、クエリasynchronouslyを作成し、@ yigitが言ったように部屋はLiveData#value
LiveData
を観察して、クエリを開始した後の遅延。このパターンは 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。依存関係
testImplementation "org.mockito:mockito-core:2.11.0"
androidTestImplementation "org.mockito:mockito-Android:2.11.0"
2。データベース
@Database(
version = 1,
exportSchema = false,
entities = {Todo.class}
)
public abstract class AppDatabase extends RoomDatabase {
public abstract TodoDao todoDao();
}
.Dao
@Dao
public interface TodoDao {
@Insert(onConflict = REPLACE)
void insert(Todo todo);
@Query("SELECT * FROM todo")
LiveData<List<Todo>> selectAll();
}
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));
}
}
このヘルプを願っています!
@Hemant Kaushikが言ったように、このケースでは[〜#〜] should [〜#〜] use InstantTaskExecutorRule
。
Developer.Android.comから:
アーキテクチャコンポーネントで使用されるバックグラウンドエグゼキュータを、各タスクを同期的に実行する別のエグゼキュータと交換するJUnitテストルール。
それは実際に動作します!
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 }
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)
}
}
そして、テストクラスで次のように使用します。
@ExtendWith(InstantExecutorExtension::class /* , Other extensions */)
class ItemDaoTests {
...
}