65

I'm currently developing an app using the newly Android Architecture Components. Specifically, I'm implementing a Room Database that returns a LiveData object on one of its queries. Insertion and querying work as expected, however I have an issue testing the query method using a unit test.

Here is the DAO I'm trying to test:

NotificationDao.kt

@Dao
interface NotificationDao {

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

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

As you can tell, the query function returns a LiveData object, if I change this to be just a List, Cursor, or basically whatever then I get the expected result, which is the data inserted in the Database.

The issue is that the following test will always fail because the value of the LiveData object is always 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 
}

So the question is: Does anyone knows of a better way to perform unit tests that involve LiveData objects?

iknow
  • 4,796
  • 9
  • 19
  • 37

6 Answers6

59

Room calculates the LiveData's value lazily when there is an observer.

You can check the sample app.

It uses a getValue utility method which adds an observer to get the value:

public static <T> T getOrAwaitValue(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];
}

Better w/ kotlin, you can make it an extensions function :).

Nicolas
  • 5,182
  • 2
  • 22
  • 58
yigit
  • 34,323
  • 13
  • 70
  • 58
  • 1
    The links are dead. – 最白目 Oct 11 '18 at 08:15
  • 1
    @最白目, it's here https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/test-common/java/com/android/example/github/util/LiveDataTestUtil.kt it's popular example of android-architecture-components "GithubBrowserSample" – Marica Nov 25 '18 at 14:30
  • Example how it can look like with extension function: https://github.com/jraska/livedata-testing if you would like to avoid writing latches on your own. – Josef Raška Dec 09 '18 at 10:59
34

When you return a LiveData from a Dao in Room it makes the query asynchronously, and as @yigit said Room sets the LiveData#value lazily after you kick off the query by observing the LiveData. This pattern is reactive.

For unit tests you want the behavior to be synchronous, so you must block the test thread and wait for the value to be passed to the observer, then grab it from there and then you can assert on it.

Here's a Kotlin extension function for doing this:

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
}

You can use it like this:

val someValue = someDao.getSomeLiveData().blockingObserve()
AKA
  • 307
  • 5
  • 16
Christopher Perry
  • 36,832
  • 42
  • 136
  • 182
  • One could also make it an extension property instead, inline the observer using a lambda, and use `await` return value to differentiate between null data and data not set within the time limit: https://gist.github.com/arekolek/e9e0d050cdd6ed16cd7dd9183eee62c0 – arekolek Jan 14 '18 at 00:00
  • 8
    I tried following this solution but I get the following error, "IllegalStateException: Cannot invoke observeForever on a background thread" – Rabbit Jun 21 '19 at 21:09
  • 6
    @Rabbit I had the same problem and solved it by adding `@Rule @JvmField public val rule = InstantTaskExecutorRule()` to my test class which requires the `androidTestImplementation "android.arch.core:core-testing:1.1.1"` dependency. It works now! – Jeehut Dec 08 '19 at 09:36
19

I found Mockito is very helpful in such case. Here is an example:

1.Dependencies

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

Hope this help!

Hemant Kaushik
  • 1,470
  • 13
  • 20
  • But aren't you are testing both selectAll and insert at the same time, here? – Rasive Feb 04 '18 at 20:19
  • 1
    @Rasive Its true, the test covers two methods of the DAO and it borders being an integration test. But overall still a comprehensive example how to set up testing room using Mockito. – sunadorer Apr 02 '18 at 23:15
  • 3
    The trick is to use `InstantTaskExecutorRule` in order to force LiveData instant updates. I was missing that, thanks! – Ioane Sharvadze May 03 '18 at 11:54
  • thats not work, I can't import mockito in android tests folder – umni4ek Mar 15 '19 at 15:28
  • 1
    @HemantKaushik, my fault, I've tried to run junit tests in androidTest folder – umni4ek Mar 18 '19 at 08:42
13

As @Hemant Kaushik said, in this case you SHOULD use InstantTaskExecutorRule.

From developer.android.com:

A JUnit Test Rule that swaps the background executor used by the Architecture Components with a different one which executes each task synchronously.

It really works!

kissed
  • 575
  • 5
  • 7
5

Slightly different approach than other answers might be to use https://github.com/jraska/livedata-testing.

You avoid mocking and the test can use API similar to RxJava testing and also you can get advantage from Kotlin extension functions.

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 }
Josef Raška
  • 1,251
  • 10
  • 7
  • 3
    For anyone looking for a more efficient/quick way of fixing this issue, look no further than this answer. Just import his package (don't forget to mark the implementation on which test folder you're using it on) and follow the above code. Trust me, this solves a lot. – Altus Nov 24 '19 at 17:20
2

If you are using JUnit 5, since rules are not applicable to it, thanks to this article you can manually create the extension:

class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {

    override fun beforeEach(context: ExtensionContext?) {
        ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
            override fun executeOnDiskIO(runnable: Runnable) = runnable.run()

            override fun postToMainThread(runnable: Runnable) = runnable.run()

            override fun isMainThread(): Boolean = true
        })
    }

    override fun afterEach(context: ExtensionContext?) {
        ArchTaskExecutor.getInstance().setDelegate(null)
    }
}

and then in your test class use it like this:

@ExtendWith(InstantExecutorExtension::class /* , Other extensions */)
class ItemDaoTests {
    ...
}
Mahozad
  • 6,430
  • 9
  • 43
  • 70