TODOs: Add Instrumentation Test for MainActivity and DataList Unit Test

Added a new flavor called Mock that is meant to run this Instrumentation
By getting rid of a Firebase/Syncbase dependency, we can verify that
- the UI triggers the correct Persistence actions.
- calls to the Listener correctly manipulate the UI

In other words, it's just up to the Persistence implementations to get
the logic between the two pieces correct.

The Instrumentation Test uses Mockito and Espresso.

The DataList got a few unit tests to confirm that it works as expected.

Change-Id: I6e50883e60ee5854054819c47fe03bce4207ea1d
diff --git a/app/build.gradle b/app/build.gradle
index 91d201b..89a014d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -12,6 +12,8 @@
         targetSdkVersion 23
         versionCode 1
         versionName "1.0"
+
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
     }
     productFlavors {
         firebase {
@@ -20,6 +22,9 @@
         syncbase {
             applicationId "io.v.todos.syncbase"
         }
+        mock {
+            applicationId "io.v.todos.mock"
+        }
     }
     buildTypes {
         release {
@@ -39,15 +44,36 @@
 dependencies {
     compile fileTree(dir: 'libs', include: ['*.jar'])
     testCompile 'junit:junit:4.12'
+
+    // Additional testing libraries (mockito and espresso) for the mock flavor.
+    androidTestMockCompile (
+            'org.mockito:mockito-core:1.10.19',
+            'com.google.dexmaker:dexmaker:1.2',
+            'com.google.dexmaker:dexmaker-mockito:1.2'
+    )
+
+    androidTestMockCompile ('com.android.support.test.espresso:espresso-intents:2.2') {
+            // Necessary to avoid version conflicts
+            exclude group: 'com.android.support', module: 'appcompat'
+            exclude group: 'com.android.support', module: 'support-v4'
+            exclude group: 'com.android.support', module: 'support-annotations'
+            exclude module: 'recyclerview-v7'
+    }
+    androidTestMockCompile('com.android.support.test.espresso:espresso-contrib:2.2') {
+            // Necessary to avoid version conflicts
+            exclude group: 'com.android.support', module: 'appcompat'
+            exclude group: 'com.android.support', module: 'support-v4'
+            exclude group: 'com.android.support', module: 'support-annotations'
+            exclude module: 'recyclerview-v7'
+    }
+
     compile (
             'com.android.support:appcompat-v7:23.1.1',
             'com.android.support:design:23.1.1',
             'com.android.support:cardview-v7:23.1.1',
-            'com.android.support:recyclerview-v7:23.1.1'
-    )
-    firebaseCompile(
-            'com.firebase:firebase-client-android:2.5.2',
+            'com.android.support:recyclerview-v7:23.1.1',
             'com.google.guava:guava:19.0'
     )
+    firebaseCompile 'com.firebase:firebase-client-android:2.5.2'
     syncbaseCompile 'io.v:vanadium-android:2.0'
 }
diff --git a/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java b/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java
new file mode 100644
index 0000000..b2a5497
--- /dev/null
+++ b/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java
@@ -0,0 +1,304 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.todos;
+
+import android.content.DialogInterface;
+import android.support.test.espresso.contrib.RecyclerViewActions;
+import android.support.test.espresso.intent.Intents;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.RecyclerView;
+import android.test.ActivityInstrumentationTestCase2;
+import android.widget.EditText;
+
+import io.v.todos.model.ListMetadata;
+import io.v.todos.model.ListSpec;
+import io.v.todos.persistence.ListEventListener;
+import io.v.todos.persistence.MainPersistence;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.click;
+import static android.support.test.espresso.action.ViewActions.swipeLeft;
+import static android.support.test.espresso.action.ViewActions.swipeRight;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.intent.Intents.intended;
+import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent;
+import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.test.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.not;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Contains UI tests for the MainActivity that isolate the MainPersistence's behavior.
+ * <p/>
+ * Note: These tests will fail if the device's screen is off.
+ * TODO(alexfandrianto): There do seem to be ways to force the screen to turn on though.
+ * <p/>
+ * Most sleeps are present in order to make the tests easier to follow when running.
+ * <p/>
+ * Created by alexfandrianto on 4/21/16.
+ */
+public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {
+    private static final String KEY1 = "asdlkjweriousdf";
+    private static final String KEY2 = "woeiuflskjeroius";
+    private static final String NAME1 = "weoislkjdflkejroif";
+    private static final String NAME2 = "weurlksdnoielrkmlsd";
+    private static final String NAME3 = "oisdlkwejrllisdfelkejr";
+    private static final long UI_DELAY = 500; // Tweak this value to adjust the test speed.
+
+    private MainActivity mActivity;
+
+    public MainActivityTest() {
+        super(MainActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        // Certain methods must be called before getActivity. Surprise!
+        setActivityInitialTouchMode(true);
+
+        mActivity = getActivity();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+
+        if (mActivity != null) {
+            mActivity.finish();
+            mActivity = null;
+        }
+    }
+
+    private void pause() {
+        try {
+            Thread.sleep(UI_DELAY);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private MainPersistence mockPersistence() {
+        MainPersistence mocked = mock(MainPersistence.class);
+        mActivity.setMainPersistence(mocked);
+        return mocked;
+    }
+
+    private AlertDialog beginAddItem() {
+        onView(withId(R.id.fab)).perform(click());
+        pause();
+
+        final AlertDialog dialog = UIUtil.getLastDialog();
+
+        assertTrue(dialog.isShowing());
+
+        mActivity.runOnUiThread(new Runnable() {
+            public void run() {
+                EditText et = (EditText) dialog.getCurrentFocus();
+                et.setText("HELLO WORLD!");
+            }
+        });
+
+        pause();
+
+        return dialog;
+    }
+
+    // Press the fab to attempt to add an item.
+    public void testAttemptAddItem() {
+        MainPersistence mocked = mockPersistence();
+        AlertDialog dialog = beginAddItem();
+
+        dialog.getButton(DialogInterface.BUTTON_POSITIVE).callOnClick();
+
+        pause();
+
+        assertFalse(dialog.isShowing());
+
+        verify(mocked).addTodoList(any(ListSpec.class));
+        verify(mocked, never()).deleteTodoList(anyString());
+        verify(mocked, never()).completeAllTasks(any(ListMetadata.class));
+    }
+
+    // Press the fab but don't actually add the item.
+    public void testAttemptAddItemFail() {
+        MainPersistence mocked = mockPersistence();
+        AlertDialog dialog = beginAddItem();
+
+        dialog.getButton(DialogInterface.BUTTON_NEGATIVE).callOnClick();
+
+        pause();
+
+        assertFalse(dialog.isShowing());
+
+        verify(mocked, never()).addTodoList(any(ListSpec.class));
+        verify(mocked, never()).deleteTodoList(anyString());
+        verify(mocked, never()).completeAllTasks(any(ListMetadata.class));
+    }
+
+    // Press the fab but don't actually add the item.
+    public void testAttemptAddItemCancel() {
+        MainPersistence mocked = mockPersistence();
+        AlertDialog dialog = beginAddItem();
+
+        dialog.dismiss();
+
+        pause();
+
+        assertFalse(dialog.isShowing());
+
+        verify(mocked, never()).addTodoList(any(ListSpec.class));
+        verify(mocked, never()).deleteTodoList(anyString());
+        verify(mocked, never()).completeAllTasks(any(ListMetadata.class));
+    }
+
+    // Add some default items so that we can interact with them with swipes.
+    private void addInitialData() {
+        mActivity.runOnUiThread(new Runnable() {
+            public void run() {
+                ListEventListener<ListMetadata> listener = mActivity.createMainListener();
+                listener.onItemAdd(new ListMetadata(KEY1, NAME1, 0, 0, 0));
+                listener.onItemAdd(new ListMetadata(KEY2, NAME2, 1, 1, 2));
+            }
+        });
+    }
+
+    // Call the listener directly to add an item.
+    public void testDirectlyAddItem() {
+        final ListEventListener<ListMetadata> listener = mActivity.createMainListener();
+
+        RecyclerView recycler = (RecyclerView) mActivity.findViewById(R.id.recycler);
+        pause();
+
+        final ListMetadata data = new ListMetadata(KEY1, NAME1, 0, 0, 0);
+        mActivity.runOnUiThread(new Runnable() {
+            public void run() {
+                listener.onItemAdd(data);
+            }
+        });
+
+        pause();
+        assertEquals(recycler.getAdapter().getItemCount(), 1);
+
+        final ListMetadata data2 = new ListMetadata(KEY2, NAME2, 1, 1, 2);
+        mActivity.runOnUiThread(new Runnable() {
+            public void run() {
+                listener.onItemAdd(data2);
+            }
+        });
+
+        pause();
+        assertEquals(recycler.getAdapter().getItemCount(), 2);
+    }
+
+    public void testDirectlyEditItem() {
+        addInitialData();
+
+        pause();
+
+        RecyclerView recycler = (RecyclerView) mActivity.findViewById(R.id.recycler);
+
+        onView(withId(R.id.recycler)).check(matches(hasDescendant(withText(NAME1))));
+        onView(withId(R.id.recycler)).check(matches(hasDescendant(withText(NAME2))));
+        onView(withId(R.id.recycler)).check(matches(not(hasDescendant(withText(NAME3)))));
+
+        final ListEventListener<ListMetadata> listener = mActivity.createMainListener();
+        mActivity.runOnUiThread(new Runnable() {
+            public void run() {
+                listener.onItemUpdate(new ListMetadata(KEY1, NAME3, 2, 0, 0));
+            }
+        });
+
+        pause();
+
+        onView(withId(R.id.recycler)).check(matches(not(hasDescendant(withText(NAME1)))));
+        onView(withId(R.id.recycler)).check(matches(hasDescendant(withText(NAME2))));
+        onView(withId(R.id.recycler)).check(matches(hasDescendant(withText(NAME3))));
+        assertEquals(recycler.getAdapter().getItemCount(), 2);
+    }
+
+    public void testDirectlyDeleteItem() {
+        addInitialData();
+
+        pause();
+
+        RecyclerView recycler = (RecyclerView) mActivity.findViewById(R.id.recycler);
+
+        onView(withId(R.id.recycler)).check(matches(hasDescendant(withText(NAME1))));
+        onView(withId(R.id.recycler)).check(matches(hasDescendant(withText(NAME2))));
+        onView(withId(R.id.recycler)).check(matches(not(hasDescendant(withText(NAME3)))));
+
+        final ListEventListener<ListMetadata> listener = mActivity.createMainListener();
+        mActivity.runOnUiThread(new Runnable() {
+            public void run() {
+                listener.onItemDelete(KEY2);
+            }
+        });
+
+        pause();
+
+        onView(withId(R.id.recycler)).check(matches(hasDescendant(withText(NAME1))));
+        onView(withId(R.id.recycler)).check(matches(not(hasDescendant(withText(NAME2)))));
+        onView(withId(R.id.recycler)).check(matches(not(hasDescendant(withText(NAME3)))));
+        assertEquals(recycler.getAdapter().getItemCount(), 1);
+    }
+
+    // Swipe a todo list item to the right to mark all of its children as done.
+    public void testAttemptCompleteAllTasks() {
+        MainPersistence mocked = mockPersistence();
+        addInitialData();
+
+        pause();
+
+        // DO UI INTERACTION
+        onView(withId(R.id.recycler)).perform(RecyclerViewActions.actionOnItemAtPosition(1, swipeRight()));
+
+        pause();
+
+        verify(mocked, never()).addTodoList(any(ListSpec.class));
+        verify(mocked, never()).deleteTodoList(anyString());
+        verify(mocked).completeAllTasks(any(ListMetadata.class));
+    }
+
+    // Swipe a todo list item to the left to attempt to delete it.
+    public void testAttemptDeleteItem() {
+        MainPersistence mocked = mockPersistence();
+        addInitialData();
+
+        pause();
+
+        // DO UI INTERACTION
+        onView(withId(R.id.recycler)).perform(RecyclerViewActions.actionOnItemAtPosition(1, swipeLeft()));
+
+        pause();
+
+        verify(mocked, never()).addTodoList(any(ListSpec.class));
+        verify(mocked).deleteTodoList(anyString());
+        verify(mocked, never()).completeAllTasks(any(ListMetadata.class));
+    }
+
+    // Tap a todo list item to launch its corresponding TodoListActivity
+    public void testTapItem() {
+        Intents.init();
+        addInitialData();
+
+        pause();
+
+        // DO UI INTERACTION
+        onView(withId(R.id.recycler)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click()));
+
+        pause();
+
+        // Confirm that the TodoListActivity was started!
+        intended(hasComponent(TodoListActivity.class.getName()));
+        Intents.release();
+    }
+}
diff --git a/app/src/main/java/io/v/todos/MainActivity.java b/app/src/main/java/io/v/todos/MainActivity.java
index 4d39028..8c1e5e3 100644
--- a/app/src/main/java/io/v/todos/MainActivity.java
+++ b/app/src/main/java/io/v/todos/MainActivity.java
@@ -7,6 +7,7 @@
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
+import android.support.annotation.VisibleForTesting;
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.helper.ItemTouchHelper;
 import android.view.Menu;
@@ -40,7 +41,7 @@
     // These todos are backed up at the SNACKOOS child of the Firebase URL.
     // We use the snackoosList to track a custom sorted list of the stored values.
     static final String INTENT_SNACKOO_KEY = "snackoo key";
-    private DataList<ListMetadata> snackoosList = new DataList<ListMetadata>();
+    private DataList<ListMetadata> snackoosList = new DataList<>();
 
     // This adapter handle mirrors the firebase list values and generates the corresponding todo
     // item View children for a list view.
@@ -107,32 +108,7 @@
             @Override
             protected MainPersistence initPersistence() throws Exception {
                 return PersistenceFactory.getMainPersistence(mActivity,
-                        new ListEventListener<ListMetadata>() {
-                            @Override
-                            public void onItemAdd(ListMetadata item) {
-                                int position = snackoosList.insertInOrder(item);
-
-                                adapter.notifyItemInserted(position);
-                                setEmptyVisiblity();
-                            }
-
-                            @Override
-                            public void onItemUpdate(ListMetadata item) {
-                                int start = snackoosList.findIndexByKey(item.key);
-                                int end = snackoosList.updateInOrder(item);
-
-                                adapter.notifyItemMoved(start, end);
-                                adapter.notifyItemChanged(end);
-                            }
-
-                            @Override
-                            public void onItemDelete(String key) {
-                                int position = snackoosList.removeByKey(key);
-
-                                adapter.notifyItemRemoved(position);
-                                setEmptyVisiblity();
-                            }
-                        });
+                        createMainListener());
             }
 
             @Override
@@ -142,6 +118,44 @@
         }.execute(PersistenceFactory.mightGetMainPersistenceBlock());
     }
 
+    // Creates a listener for this activity. Visible to tests to allow them to invoke the listener
+    // methods directly.
+    @VisibleForTesting
+    ListEventListener<ListMetadata> createMainListener() {
+        return new ListEventListener<ListMetadata>() {
+            @Override
+            public void onItemAdd(ListMetadata item) {
+                int position = snackoosList.insertInOrder(item);
+
+                adapter.notifyItemInserted(position);
+                setEmptyVisiblity();
+            }
+
+            @Override
+            public void onItemUpdate(ListMetadata item) {
+                int start = snackoosList.findIndexByKey(item.key);
+                int end = snackoosList.updateInOrder(item);
+
+                adapter.notifyItemMoved(start, end);
+                adapter.notifyItemChanged(end);
+            }
+
+            @Override
+            public void onItemDelete(String key) {
+                int position = snackoosList.removeByKey(key);
+
+                adapter.notifyItemRemoved(position);
+                setEmptyVisiblity();
+            }
+        };
+    }
+
+    // Allow the tests to mock out the main persistence.
+    @VisibleForTesting
+    void setMainPersistence(MainPersistence p) {
+        mPersistence = p;
+    }
+
     // Set the visibility based on what the adapter thinks is the visible item count.
     private void setEmptyVisiblity() {
         View v = findViewById(R.id.empty);
diff --git a/app/src/main/java/io/v/todos/UIUtil.java b/app/src/main/java/io/v/todos/UIUtil.java
index d17b822..779d093 100644
--- a/app/src/main/java/io/v/todos/UIUtil.java
+++ b/app/src/main/java/io/v/todos/UIUtil.java
@@ -19,6 +19,11 @@
         return prefix + ": " + DateUtils.getRelativeTimeSpanString(startTime);
     }
 
+    private static AlertDialog lastDialog;
+    public static AlertDialog getLastDialog() {
+        return lastDialog;
+    }
+
     public static void showAddDialog(Context context, String title,
                                      final DialogResponseListener addListener) {
         final EditText todoItem = new EditText(context);
@@ -37,6 +42,7 @@
                 }).show();
         dialog.getWindow().setSoftInputMode(
                 WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+        lastDialog = dialog;
     }
 
     public static void showEditDialog(Context context, String title, String defaultValue,
@@ -63,6 +69,7 @@
                 }).show();
         dialog.getWindow().setSoftInputMode(
                 WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+        lastDialog = dialog;
     }
 
     public static abstract class DialogResponseListener {
diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml
index db458e2..f1a47c2 100644
--- a/app/src/main/res/layout/content_main.xml
+++ b/app/src/main/res/layout/content_main.xml
@@ -27,7 +27,7 @@
             android:background="#FF0000"
             android:text="No data"/>
     </LinearLayout>
-    <android.support.design.widget.FloatingActionButton
+    <android.support.design.widget.FloatingActionButton android:id="@+id/fab"
         android:layout_height="wrap_content"
         android:layout_width="wrap_content"
         android:layout_alignParentBottom="true"
diff --git a/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java b/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java
new file mode 100644
index 0000000..d6fe975
--- /dev/null
+++ b/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java
@@ -0,0 +1,87 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.todos.persistence;
+
+import android.app.Activity;
+import android.content.Context;
+
+import io.v.todos.model.ListMetadata;
+import io.v.todos.model.ListSpec;
+import io.v.todos.model.Task;
+import io.v.todos.model.TaskSpec;
+
+public final class PersistenceFactory {
+    private PersistenceFactory(){}
+
+    /**
+     * Indicates whether {@link #getMainPersistence(Activity, ListEventListener)} may block. This
+     * can affect whether a progress indicator is shown and whether a worker thread is used.
+     */
+    public static boolean mightGetMainPersistenceBlock() {
+        return false;
+    }
+
+    /**
+     * Instantiates a persistence object that can be used to manipulate todo lists.
+     */
+    public static MainPersistence getMainPersistence(Activity activity,
+                                                     ListEventListener<ListMetadata> listener) {
+        return new MockMainPersistence();
+    }
+
+    /**
+     * Indicates whether {@link #getTodoListPersistence(Activity, String, TodoListListener)} may
+     * block. This can affect whether a progress indicator is shown and whether a worker thread is
+     * used.
+     */
+    public static boolean mightGetTodoListPersistenceBlock() {
+        return false;
+    }
+
+    /**
+     * Instantiates a persistence object that can be used to manipulate a todo list.
+     */
+    public static TodoListPersistence getTodoListPersistence(Activity activity, String key,
+                                                             TodoListListener listener) {
+        return new MockTodoListPersistence();
+    }
+
+    static class MockMainPersistence implements MainPersistence {
+        @Override
+        public void addTodoList(ListSpec listSpec) {}
+
+        @Override
+        public void deleteTodoList(String key) {}
+
+        @Override
+        public void completeAllTasks(ListMetadata listMetadata) {}
+
+        @Override
+        public void close() {}
+    }
+
+    static class MockTodoListPersistence implements TodoListPersistence {
+        @Override
+        public void updateTodoList(ListSpec listSpec) {}
+
+        @Override
+        public void deleteTodoList() {}
+
+        @Override
+        public void addTask(TaskSpec task) {}
+
+        @Override
+        public void updateTask(Task task) {}
+
+        @Override
+        public void deleteTask(String key) {}
+
+        @Override
+        public void setShowDone(boolean showDone) {}
+
+        @Override
+        public void close() {}
+    }
+}
diff --git a/app/src/test/java/io/v/todos/DataListTest.java b/app/src/test/java/io/v/todos/DataListTest.java
new file mode 100644
index 0000000..b8f5512
--- /dev/null
+++ b/app/src/test/java/io/v/todos/DataListTest.java
@@ -0,0 +1,89 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.todos;
+
+import org.junit.Test;
+
+import io.v.todos.model.DataList;
+import io.v.todos.model.KeyedData;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * A unit test for the DataList. Confirms that keyed data is inserted in the correct order.
+ */
+public class DataListTest {
+    @Test
+    public void dataListTest() throws Exception {
+        DataList<TestKeyedData> dataList = new DataList<>();
+
+        // Insert a few.
+        assertEquals(0, dataList.insertInOrder(new TestKeyedData("b")));
+        assertEquals(1, dataList.insertInOrder(new TestKeyedData("d")));
+        assertEquals(0, dataList.insertInOrder(new TestKeyedData("a")));
+        assertEquals(2, dataList.insertInOrder(new TestKeyedData("c")));
+        assertEquals(0, dataList.insertInOrder(new TestKeyedData("e", -1)));
+        assertEquals(5, dataList.insertInOrder(new TestKeyedData("g", 1)));
+        assertEquals(5, dataList.insertInOrder(new TestKeyedData("f", 1)));
+
+        // We should be at e, a, b, c, d, f, g now.
+        assertEquals(1, dataList.findIndexByKey("a"));
+        assertEquals(2, dataList.findIndexByKey("b"));
+        assertEquals(3, dataList.findIndexByKey("c"));
+        assertEquals(4, dataList.findIndexByKey("d"));
+        assertEquals(0, dataList.findIndexByKey("e"));
+        assertEquals(5, dataList.findIndexByKey("f"));
+        assertEquals(6, dataList.findIndexByKey("g"));
+        assertEquals(-1, dataList.findIndexByKey("h"));
+
+        // Update a few.
+        assertEquals(6, dataList.updateInOrder(new TestKeyedData("b", 2))); // Move b to the back.
+        assertEquals(5, dataList.updateInOrder(new TestKeyedData("a", 2))); // Move a too.
+
+        // Confirm priorities. We should be at e, c, d, f, g, a, b now.
+        assertEquals(2, dataList.findByKey("a").priority);
+        assertEquals(2, dataList.findByKey("b").priority);
+        assertEquals(0, dataList.findByKey("c").priority);
+        assertEquals(0, dataList.findByKey("d").priority);
+        assertEquals(-1, dataList.findByKey("e").priority);
+        assertEquals(1, dataList.findByKey("f").priority);
+        assertEquals(1, dataList.findByKey("g").priority);
+
+        // And now, remove a few.
+        assertEquals(1, dataList.removeByKey("c"));
+        assertEquals(4, dataList.removeByKey("a"));
+
+        // Confirm indexes. e, d, f, g, b
+        assertEquals(-1, dataList.findIndexByKey("a"));
+        assertEquals(4, dataList.findIndexByKey("b"));
+        assertEquals(-1, dataList.findIndexByKey("c"));
+        assertEquals(1, dataList.findIndexByKey("d"));
+        assertEquals(0, dataList.findIndexByKey("e"));
+        assertEquals(2, dataList.findIndexByKey("f"));
+        assertEquals(3, dataList.findIndexByKey("g"));
+        assertEquals(-1, dataList.findIndexByKey("h"));
+    }
+
+    private class TestKeyedData extends KeyedData<TestKeyedData> {
+        int priority;
+
+        TestKeyedData(String key) {
+            super(key);
+        }
+
+        TestKeyedData(String key, int priority) {
+            super(key);
+            this.priority = priority;
+        }
+
+        @Override
+        public int compareTo(TestKeyedData testKeyedData) {
+            if (priority != testKeyedData.priority) {
+                return priority - testKeyedData.priority;
+            }
+            return key.compareTo(testKeyedData.key);
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/test/java/io/v/todos/ExampleUnitTest.java b/app/src/test/java/io/v/todos/ExampleUnitTest.java
deleted file mode 100644
index 07340b3..0000000
--- a/app/src/test/java/io/v/todos/ExampleUnitTest.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright 2016 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package io.v.todos;
-
-import org.junit.Test;
-
-import static org.junit.Assert.*;
-
-/**
- * To work on unit tests, switch the Test Artifact in the Build Variants view.
- */
-public class ExampleUnitTest {
-    @Test
-    public void addition_isCorrect() throws Exception {
-        assertEquals(4, 2 + 2);
-    }
-}
\ No newline at end of file