70 votes

Les tests unitaires de Chambre et de LiveData

Je suis en train de développer une application en utilisant le tout nouveau Android des Composants de l'Architecture. Plus précisément, je suis à la mise en œuvre d'une Chambre de Base de données qui renvoie un LiveDataobjet sur un de ses requêtes. De l'Insertion et de l'interrogation fonctionne comme prévu, mais j'ai un problème de test de la méthode de requête à l'aide du test de l'unité.

Ici, c'est le DAO, je suis en train de tester:

NotificationDao.kt

@Dao
interface NotificationDao {

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

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

}

Comme vous pouvez le dire, la fonction de requête renvoie un LiveData objet, si je change ce sera juste un List, Cursor ou essentiellement n'importe quel puis-je obtenir le résultat escompté, qui est l'insertion de données dans la Base de données.

Le problème est que le test suivant, toujours voués à l'échec parce que l' value de la LiveData objet est toujours 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 
}

La question est donc: est-ce quelqu'un connaît une meilleure façon d'effectuer des tests unitaires qui impliquent LiveData objets?

63voto

yigit Points 634

Chambre calcule le LiveDatas'valeur langoureusement quand il y a un observateur.

Vous pouvez le vérifier l' application d'exemple.

Il utilise un getValue méthode utilitaire qui ajoute un observateur pour obtenir la valeur:

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

Mieux w/ kotlin, vous pouvez en faire une des extensions de fonction :).

37voto

Christopher Perry Points 7972

Lorsque vous revenez à un LiveData d'un Dao dans la Chambre il fait la requête de manière asynchrone, et comme @yigit dit ensembles de Salle à l' LiveData#value paresseusement après le coup d'envoi de la requête par l'observation de l' LiveData. Ce modèle est réactif.

Pour les tests unitaires vous voulez le comportement synchrone, de sorte que vous devez bloquer le test thread et d'attendre pour la valeur à transmettre à l'observateur, puis saisir à partir de là et puis, vous pouvez faire valoir sur elle.

Voici un Kotlin fonction d'extension pour ce faire:

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
}

Vous pouvez l'utiliser comme ceci:

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

20voto

Hemant Kaushik Points 702

J'ai trouvé Mockito est très utile dans de tels cas. Voici un exemple:

1.Dépendances

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

2.Base de données

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

Espérons que cette aide!

14voto

kissed Points 161

Comme @Hemant Kaushik dit, dans ce cas, vous DEVEZ utiliser InstantTaskExecutorRule.

À partir de developer.android.com:

Un Test JUnit Règle que les swaps sur le fond exécuteur utilisés par les Composants de l'Architecture avec un autre qui exécute chaque tâche de manière synchrone.

Il fonctionne vraiment!

6voto

Josef Raška Points 1094

Approche légèrement différente que les autres réponses pourrait être d'utiliser https://github.com/jraska/livedata-testing.

Vous éviter les moqueries et le test peut utiliser l'API similaire à RxJava tests, et vous pouvez obtenir l'avantage de Kotlin fonctions d'extension.

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 }

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X