Unit testing loaders

Loaders are fantastic, and if you don’t believe me read Ian Lake’s post on Medium: Making loading data lifecycle aware.

Building a dynamic Android app requires dynamic data. But I hope we’ve all moved beyond loading data on the UI thread (#perfmatters or something like that). That discussion can go on for seasons and seasons, but let’s focus in on one case: loading data specifically for display in your Activity or Fragment with Loaders.

Ian recommends using LoaderTestCase, and that if you are using support loaders you should make a custom LoaderTestCase class:

Note: while there’s a LoaderTestCase designed for framework classes, you’ll need to make a Support Library equivalent from the LoaderTestCase source code if you want to do something similar with the Support v4 Loader. This also gives you a good idea of how to interact with a Loader without a LoaderManager.

That’s fine - I’ll bite. Here’s my custom LoaderTestCase class that supports the Support v4 Loader. (gist)

/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.v4.content.Loader;
import android.test.AndroidTestCase;

import java.util.concurrent.ArrayBlockingQueue;
/**
 * A convenience class for testing {@link Loader}s. This test case
 * provides a simple way to synchronously get the result from a Loader making
 * it easy to assert that the Loader returns the expected result.
 */
public class SupportLoaderTestCase extends AndroidTestCase {
    static {
        // Force class loading of AsyncTask on the main thread so that it's handlers are tied to
        // the main thread and responses from the worker thread get delivered on the main thread.
        // The tests are run on another thread, allowing them to block waiting on a response from
        // the code running on the main thread. The main thread can't block since the AysncTask
        // results come in via the event loop.
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... args) {
                return null;
            }

            @Override
            protected void onPostExecute(Void result) {
            }
        };
    }

    /**
     * Runs a Loader synchronously and returns the result of the load. The loader will
     * be started, stopped, and destroyed by this method so it cannot be reused.
     *
     * @param loader The loader to run synchronously
     * @return The result from the loader
     */
    public <T> T getLoaderResultSynchronously(final Loader<T> loader) {
        // The test thread blocks on this queue until the loader puts it's result in
        final ArrayBlockingQueue<T> queue = new ArrayBlockingQueue<T>(1);
        // This callback runs on the "main" thread and unblocks the test thread
        // when it puts the result into the blocking queue
        final Loader.OnLoadCompleteListener<T> listener = new Loader.OnLoadCompleteListener<T>() {
            @Override
            public void onLoadComplete(Loader<T> completedLoader, T data) {
                // Shut the loader down
                completedLoader.unregisterListener(this);
                completedLoader.stopLoading();
                completedLoader.reset();
                // Store the result, unblocking the test thread
                queue.add(data);
            }
        };
        // This handler runs on the "main" thread of the process since AsyncTask
        // is documented as needing to run on the main thread and many Loaders use
        // AsyncTask
        final Handler mainThreadHandler = new Handler(Looper.getMainLooper()) {
            @Override
            public void handleMessage(Message msg) {
                loader.registerListener(0, listener);
                loader.startLoading();
            }
        };
        // Ask the main thread to start the loading process
        mainThreadHandler.sendEmptyMessage(0);
        // Block on the queue waiting for the result of the load to be inserted
        T result;
        while (true) {
            try {
                result = queue.take();
                break;
            } catch (InterruptedException e) {
                throw new RuntimeException("waiting thread interrupted", e);
            }
        }
        return result;
    }
}

Now that we have a custom LoaderTestCase class, how exactly do we use it? Our first hint is in the Javadoc:

A convenience class for testing Loaders. This test case provides a simple way to synchronously get the result from a Loader making it easy to assert that the Loader returns the expected result.

Okay. Cool. Now what? Well, there’s really only one method you can interact with in the LoaderTestCase: getLoaderResultSynchronously(). Succinctly put, You give it a loader object and it runs it through the loader life cycle and returns the result that would otherwise have been passed to onLoadFinished()

Let’s walk through a complete example. Here’s a heavily contrived custom loader that returns a list of Objects from an imaginary third party synchronous REST API.

public class BuildingLoader extends Loader<ArrayList<BuildingModel>> {
  private final String[] mBuildingNames;
  private ArrayList<BuildingModel> mBuildingArrayList;

  public BuildingLoader(Context context, String[] buildingNames) {
    super(context);
    mBuildingNames = buildingNames;
  }

  @Override
    protected void onStartLoading() {
        if (mBuildingArrayList != null) {
            deliverResult(mBuildingArrayList);
        } else {
            forceLoad();
        }

    }

    @Override
    protected void onForceLoad() {
        if (mBuildingArrayList != null) {
            deliverResult(mBuildingArrayList);
        }

        loadInBackground();
    }

    @Override
    protected void onReset() {
        mBuildingArrayList = null;
    }

    private void loadInBackground() {
      for (int x = 0; x < mBuildingNames.length; x++) {
        onGetDataForBuilding(mBuildingNames[x]);
      }

      deliverResult(mBuildingArrayList);
    }

    @VisibleForTesting
    protected void onGetDataForBuilding(String buildingName) {
      // Pretend this is a blocking call to a third party REST API that
      // accepts a building name and returns the building address.
      BuildingModel resultBuilding = null;

      onSuccess(resultBuilding);
    }

    public void onSuccess(final BuildingModel buildingModel) {
      if (mBuildingArrayList == null) {
            mBuildingArrayList = new ArrayList<>();
        }
        mBuildingArrayList.add(buildingModel);
    }
}

Now let’s write a simple unit test (using SupperLoaderTestCase) that verifies our custom loader returns an ArrayList of BuildingModel objects when given an array of building names.

(Since its A Bad Idea™ to perform network requests in a unit test we will also utilize Mockito to stub out the actual interaction to our imaginary third party REST API. We will also rely on Roboelectric here, although probably not strictly necessary.)


import junit.framework.Assert;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Matchers;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;

import java.util.ArrayList;

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class BuildingLoaderTest extends LoaderTestCase {


    /**
     * Test that given a single building name, our custom Loader returns an ArrayList
     * with a single building object.
     */
    @Test
    public void testSingleBuilding() {
        String[] buildingsToLoad = {"Home"};

        BuildingLoader spyBuildingLoader = setupBuildingLoaderForTest(
                new BuildingLoader(RuntimeEnvironment.application,
                        buildingsToLoad));

        ArrayList<BuildingModel> buildings = getLoaderResultSynchronously(spyBuildingLoader);

        Assert.assertNotNull(buildings);
        Assert.assertEquals(1, buildings.size());
    }

    private BuildingLoader setupBuildingLoaderForTest(BuildingLoader loader) {
        // Prevent the loader from making request to imaginary REST API to get building info
        // instead return mocked BuildingModel objects based on the requested building name
        BuildingLoader spyBuildingLoader = Mockito.spy(
                loader
        );

        // Immediately fire the onSuccess callback with a fake BuildingModel
        // object, just like a real request would.
        Mockito.doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                String buildingName = (String) invocation.getArguments()[0];

                BuildingModel building = new BuildingModel(buildingName
                        "Street Address for " + buildingName);

                ((BuildingLoader) invocation.getMock()).onSuccess(building);
                return null;
            }
        }).when(spyBuildingLoader).onGetDataForBuilding(Matchers.anyString());

        return spyBuildingLoader;
    }
}

There ya go! Your imagination should be positively running wild now with the things you can test using LoaderTestCase.