Sala de pruebas Unitarias y LiveData


Actualmente estoy desarrollando una aplicación utilizando los nuevos componentes de la arquitectura Android. Específicamente estoy implementando una base de datos de habitaciones que devuelve un objeto LiveDataen una de sus consultas. La inserción y la consulta funcionan como se esperaba, sin embargo, tengo un problema al probar el método de consulta usando unit test.

Aquí está el DAO que estoy tratando de probar:

NotificationDao.kt

@Dao
interface NotificationDao {

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

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

}

Como puede ver, la función de consulta devuelve un objeto LiveData, si cambiar esto para ser solo un List, Cursor o básicamente lo que sea, entonces obtengo el resultado esperado, que son los datos insertados en la base de datos.

El problema es que la siguiente prueba siempre fallará porque el value del objeto LiveData siempre es null:

Prueba de notificación.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 
}

Entonces la pregunta es: ¿Alguien conoce una mejor manera de realizar pruebas unitarias que involucren objetos LiveData?

Author: Nicolás Carrasco, 2017-05-30

4 answers

Room calcula el valor de LiveData perezosamente cuando hay un observador.

Puedes comprobar la aplicación de ejemplo .

Utiliza un método de utilidad getValue que agrega un observador para obtener el valor:

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

Mejor con kotlin, puedes convertirlo en una función de extensiones :).

 33
Author: yigit,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-01 12:52:01

Cuando devuelve un LiveData desde un Dao en la Sala, realiza la consulta asíncrono, y como dijo @yigit Room establece el LiveData#value perezosamente después de iniciar la consulta observando el LiveData. Este patrón es reactivo .

Para las pruebas unitarias, desea que el comportamiento sea síncrono, por lo tanto, debe bloquear el hilo de prueba y esperar a que el valor se pase al observador, luego agarrarlo desde allí y luego puede afirmarlo.

Aquí hay un Función de extensión Kotlin para hacer esto:

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
}

Puedes usarlo así:

val someValue = someDao.getSomeLiveData().blockingObserve()
 17
Author: Christopher Perry,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-25 00:42:31

Me pareció que Mockito es muy útil en tal caso. He aquí un ejemplo:

1.Dependencias

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

2.Base de datos

@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.Prueba

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

Espero que esta ayuda!

 7
Author: Hemant Kaushik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-10 18:30:05

Como dijo @Hemant Kaushik, en este caso DEBERÍAS usar InstantTaskExecutorRule.

Desde developer.android.com:

Una Regla de prueba JUnit que intercambia el ejecutor en segundo plano utilizado por los Componentes de la Arquitectura con uno diferente que ejecuta cada tarea de forma sincrónica.

¡Realmente funciona!

 4
Author: kissed,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-02 14:35:13