18

Aktualnie pracuję nad aplikacją przy użyciu nowego Android Architecture Components. W szczególności implementuję bazę danych pokoju, która zwraca obiekt LiveData w jednym z zapytań. Wstawianie i zapytania działają zgodnie z oczekiwaniami, jednak mam problem testujący metodę zapytania przy użyciu testu jednostkowego.Urządzenie do testowania jednostek i LiveData

Oto DAO Próbuję testu:

NotificationDao.kt

@Dao 
interface NotificationDao { 

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

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

}

Jak widać, funkcja kwerenda zwraca LiveData obiekt, jeśli Zmieniam to na bycie tylko List, lub w zasadzie cokolwiek otrzymam oczekiwany wynik, który jest danymi wstawionymi do bazy danych.

Problem jest następujący test zawsze niepowodzeniem, ponieważ value obiektu LiveData jest zawsze 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 
} 

Więc pytanie brzmi: Czy ktoś wie lepszego sposobu na przeprowadzanie testów jednostkowych z obiektami LiveData?

Odpowiedz

25

Pokój oblicza wartość LiveData leniwie, gdy jest obserwator.

Możesz sprawdzić sample app.

Wykorzystuje metodę getValue narzędzie, które dodaje obserwatora, aby uzyskać wartość:

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]; 
} 

Lepiej w/Kotlin, można zrobić to funkcja rozszerzenia :).

+1

Czy jest jakaś aktualizacja w wersji 1.0.0? –

10

Po powrocie do LiveData z Dao w pokoju to sprawia, że ​​zapytanie asynchronicznie, a jako @yigit wspomnianych zestawów Pokój z LiveData#value leniwie po inauguracją zapytanie obserwując LiveData. Ten wzór to reactive.

Dla testów jednostkowych, które chcesz, aby zachowanie było synchroniczne, musisz zablokować wątek testowy i poczekać, aż wartość zostanie przekazana obserwatorowi, a następnie zabrać go stamtąd, a następnie można na nim potwierdzić.

Oto funkcja rozszerzenie Kotlin to robisz:

fun <T> LiveData<T>.blockingObserve(): T? { 
    var value: T? = null 
    val latch = CountDownLatch(1) 
    val innerObserver = Observer<T> { 
     value = it 
     latch.countDown() 
    } 
    observeForever(innerObserver) 
    latch.await(2, TimeUnit.SECONDS) 
    return value 
} 

Można go używać tak:

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

Można też użyć go jako właściwości rozszerzenia, wstawić obserwatora za pomocą lambda i użyć zwracanej wartości 'await' w celu rozróżnienia danych zerowych i danych ustawionych w określonym limicie czasowym: https://gist.github.com/arekolek/e9e0d050cdd6ed16cd7dd9183eee62c0 – arekolek

2

znalazłem Mockito jest bardzo pomocny w takim przypadku. Oto przykład:

1.Zależności

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

Mam nadzieję, że ta pomoc!

+0

Ale czy nie testujesz jednocześnie selectAll i wstawiania w tym samym czasie? – Rasive