web-dev-qa-db-fra.com

Salle de tests unitaires et LiveData

Je développe actuellement une application à l'aide de la nouvelle Android Architecture Components . Plus précisément, j'implémente une base de données de pièces qui renvoie un objet LiveData à l'une de ses requêtes. L'insertion et l'interrogation fonctionnent comme prévu, mais j'ai un problème à tester la méthode d'interrogation à l'aide du test unitaire.

Voici le DAO que j'essaye 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 constater, la fonction de requête retourne un objet LiveData. Si je le modifie comme étant simplement un List, Cursor ou quoi que ce soit d'autre, j'obtiens le résultat attendu qui correspond aux données insérées dans la base de données. 

Le problème est que le test suivant échouera toujours car la value de l'objet LiveData 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 la suivante: Quelqu'un connaît-il une meilleure façon de réaliser des tests unitaires impliquant des objets LiveData?

33

Room calcule la valeur de LiveData paresseusement lorsqu'il y a un observateur.

Vous pouvez vérifier le exemple d'application .

Il utilise une méthode utilitaire getValue 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 fonction d’extensions :).

39
yigit

Lorsque vous renvoyez une LiveData d'une Dao dans Room, la requête de manière asynchrone et, comme @yigit, Room définit le LiveData#value paresseusement après avoir lancé la requête en observant la LiveData. Ce modèle est réactif .

Pour les tests unitaires, vous voulez que le comportement soit synchrone, vous devez donc bloquer le fil de test et attendre que la valeur soit transmise à l'observateur, puis le récupérer à partir de là et ensuite vous pouvez affirmer dessus.

Voici une fonction d'extension Kotlin pour cela:

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()
23
Christopher Perry

J'ai trouvé que Mockito était très utile dans un tel 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));
    }
}

J'espère que cette aide!

10
Hemant Kaushik

Comme @Hemant Kaushik l'a dit, dans ce cas, vousDEVRIEZutiliser InstantTaskExecutorRule

De developer.Android.com:

Une règle de test JUnit qui échange l'exécuteur en arrière-plan utilisé par les composants d'architecture avec un autre qui exécute chaque tâche de manière synchrone.

Ça marche vraiment!

5
kissed

Une approche légèrement différente des autres réponses pourrait être d’utiliser https://github.com/jraska/livedata-testing .

Vous évitez les moqueries et le test peut utiliser une API similaire au test RxJava et vous pouvez également tirer parti des fonctions d'extension 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 }
0
Josef Raška