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?
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 :).
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()
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!
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!
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 }