CodeNewbie Community

Cover image for Testing in Android. Testing LiveData
Tristan
Tristan

Posted on

Testing in Android. Testing LiveData

Introduction

  • Now that I have officially released my first app, which can be found HERE on the Google Play store. I want to add more features to my app, however, I think the next best step is to add some tests. So, this series is going to be a practical series dedicated to understanding testing in the Android framework.

Source code

  • You can find my source code HERE

Video

  • You can find the video version HERE

LiveData

  • So, what is LiveData? Well, as the documentation so elegantly puts it,LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state. Confused yet? Welcome to the club! Basically, for the purpose of this tutorial, we can define LiveData as being a fancy class that implements the observer pattern. You can learn more about the fancy details of LiveData HERE.

Testing LiveData

  • Before we go into testing, there are a few things that we must be aware of. Behind the scenes Room and LiveData implement a lot of code without our knowledge. First off, all LiveData queries to the Room database are asynchronous and run on a background thread. Which provides us with our first problem, how do we write code that will block the asynchronous query so we can test it?. The answer to this problem, is two parts:

1)observeForever/observer
2)CountDownLatch

observeForever/observer

  • The Android documentation states, Room persistence library supports observable queries, which return LiveData objects. Which means we can observe these queries and have them run code when they return a value. This observation is done with the observeForever() method. This method takes in a class that implements the Observer interface and will enact the onChanged() method when the LiveData query returns. We can do so like this:
//LiveData query
LiveData<List<Calf>> calfLiveDataList = getCalfDao().getAllCalves();

Observer<List<Calf>> observer = new Observer<List<Calf>>() {
            @Override
            public void onChanged(List<Calf> listLiveData) {
              //code to run when query returns
            }
        };

calfLiveDataList.observeForever(observer);
Enter fullscreen mode Exit fullscreen mode
  • This code still has one major problem and that problem is, it is not blocking. This means that we can still not run any tests. To fix this problem we will implement a CountDownLatch.

CountDownLatch

  • This class is a synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes. We will initialize a CountDownLatch with a given count, we can then use the await() method to block our code until the countDown() method is called, which releases all the threads and unblocks our code.
final CountDownLatch latch = new CountDownLatch(1);

Observer<List<Calf>> observer = new Observer<List<Calf>>() {
            @Override
            public void onChanged(List<Calf> listLiveData) {
                latch.countDown(); // this releases all the threads

            }
        };

latch.await(2, TimeUnit.SECONDS);
Enter fullscreen mode Exit fullscreen mode
  • So our code above will block until onChange is triggered and latch.countDown() is called. Now that we have the blocking code, we just need to get the value from listLiveData

listLiveData

  • listLiveData represents the data that gets returned by the LiveData query and it will return a List containing Calf objects. This seems simple enough, just remove the data like you would any other List. However, the new Observer statement makes this a inner/nested class and there are a few special rules when dealing with variables in inner/nested classes. The two main ones that we will have to compile with are:

1) Any local variable used but not declared in an inner class must be declared final

2) From the inner class you can't assign a local variable but you can use a reference object.

  • To abide by these rules we will, create a final reference Array variable:
final Calf[] data = new Calf[1];
Enter fullscreen mode Exit fullscreen mode
  • This variable can now be accessed and manipulated but inner classes, like so:
 final CountDownLatch latch = new CountDownLatch(1);

        final Calf[] data = new Calf[1];
        Observer<List<Calf>> observer = new Observer<List<Calf>>() {
            @Override
            public void onChanged(List<Calf> listLiveData) {
                latch.countDown(); // this releases all the threads
                data[0] = listLiveData.get(0);
            }
        };
Enter fullscreen mode Exit fullscreen mode
  • data[0] = listLiveData.get(0); is simply us assigning the first value to our reference variable.

Cannot invoke observeForever on a background thread

  • Now we can observe the query:
calfLiveDataList.observeForever(observer);
Enter fullscreen mode Exit fullscreen mode
  • However, if you try to run this code, you will get an error stating, Cannot invoke observeForever on a background thread. This error occurs because the callback onChanged() which is on the observer interface often makes UI changes. When dealing with threads in Android the main rule is, NEVER MAKE UI CHANGES ON A BACKGROUND THREAD. To handle this new error, we need access to the main thread.

Accessing the main thread

  • To be able to access and assign tasks to the main thread we are going to use the Handler class.
Handler handler = new Handler(Looper.getMainLooper());
Enter fullscreen mode Exit fullscreen mode
  • Looper.getMainLooper() is what actually give us access to the main thread. handler gives access to the main thread's task queue, which we can assign a task to through the post() method.
handler.post(new Runnable() {
                @Override
                public void run() {                    
                       calfLiveDataList.observeForever(observer);
                        }
                    }
       );
Enter fullscreen mode Exit fullscreen mode
  • The Runnable allows us to create a new task to be run on the main thread.

Running the tests

  • With the observer properly implemented, we can now peacefully run out tests:
        Calf returnedCalf = data[0];
        int id = returnedCalf.getId();

        Assert.assertEquals(1,id);
Enter fullscreen mode Exit fullscreen mode
  • In this test I am just checking to make sure that the Calf object has the proper Id.

Conclusion

  • Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.

Discussion (0)