Writing Unit Tests via Mocks

Let’s look again at the TripStore DAO:

package com.commonsware.android.room;

import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Delete;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Update;
import java.util.List;

@Dao
interface TripStore {
  @Query("SELECT * FROM trips ORDER BY title")
  List<Trip> selectAll();

  @Query("SELECT * FROM trips WHERE id=:id")
  Trip findById(String id);

  @Insert
  void insert(Trip... trips);

  @Update
  void update(Trip... trips);

  @Delete
  void delete(Trip... trips);
}

This is a pure interface. More importantly, other than annotations, its API is purely domain-specific. Everything revolves around our Trip entity and other business logic (e.g., String values as identifiers).

Room DAOs are designed to be mocked, using a mocking library like Mockito, so that you can write unit tests (tests that run on your development machine or CI server) in addition to — or perhaps instead of — instrumentation tests.

The primary advantage of unit tests is execution speed, as they do not have to be run on Android devices or emulators. On the other hand, setting up mocks can be tedious.

The RoomBasics project not only has the instrumentation tests shown earlier in this chapter, but an equivalent unit test in test/, embodied in a TripUnitTests class:

package com.commonsware.android.room;

import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;

public class TripUnitTests {
  private TripStore store;

  @Before
  public void setUp() {
    store=Mockito.mock(TripStore.class);

    final HashMap<String, Trip> trips=new HashMap<>();

    doAnswer(new Answer() {
      @Override
      public Object answer(InvocationOnMock invocation) throws Throwable {
        ArrayList<Trip> result=new ArrayList<>(trips.values());

        Collections.sort(result, new Comparator<Trip>() {
          @Override
          public int compare(Trip left, Trip right) {
            return(left.title.compareTo(right.title));
          }
        });

        return(result);
      }
    }).when(store).selectAll();

    doAnswer(new Answer() {
      @Override
      public Object answer(InvocationOnMock invocation) throws Throwable {
        String id=(String)invocation.getArguments()[0];

        return(trips.get(id));
      }
    }).when(store).findById(any(String.class));

    doAnswer(new Answer() {
      @Override
      public Object answer(InvocationOnMock invocation) throws Throwable {
        for (Object rawTrip : invocation.getArguments()) {
          Trip trip=(Trip)rawTrip;

          trips.put(trip.id, trip);
        }

        return(null);
      }
    }).when(store).insert(ArgumentMatchers.any());

    doAnswer(new Answer() {
      @Override
      public Object answer(InvocationOnMock invocation) throws Throwable {
        for (Object rawTrip : invocation.getArguments()) {
          Trip trip=(Trip)rawTrip;

          trips.put(trip.id, trip);
        }

        return(null);
      }
    }).when(store).update(ArgumentMatchers.any());

    doAnswer(new Answer() {
      @Override
      public Object answer(InvocationOnMock invocation) throws Throwable {
        for (Object rawTrip : invocation.getArguments()) {
          Trip trip=(Trip)rawTrip;

          trips.remove(trip.id);
        }

        return(null);
      }
    }).when(store).delete(ArgumentMatchers.any());
  }

  @Test
  public void basics() {
    assertEquals(0, store.selectAll().size());

    final Trip first=new Trip("Foo", 2880);

    assertNotNull(first.id);
    assertNotEquals(0, first.id.length());
    store.insert(first);

    assertTrip(store, first);

    final Trip updated=new Trip(first.id, "Foo!!!", 1440);

    store.update(updated);
    assertTrip(store, updated);

    store.delete(updated);
    assertEquals(0, store.selectAll().size());
  }

  private void assertTrip(TripStore store, Trip trip) {
    List<Trip> results=store.selectAll();

    assertNotNull(results);
    assertEquals(1, results.size());
    assertTrue(areIdentical(trip, results.get(0)));

    Trip result=store.findById(trip.id);

    assertNotNull(result);
    assertTrue(areIdentical(trip, result));
  }

  private boolean areIdentical(Trip one, Trip two) {
    return(one.id.equals(two.id) &&
      one.title.equals(two.title) &&
      one.duration==two.duration);
  }
}

The basics() test method, and its supporting utility methods, are the same as in the instrumentation tests. What differs is where the TripStore comes from. In the instrumentation tests, we created an in-memory TripDatabase and retrieved a TripStore from it. In the unit tests, we use Mockito to create a mock TripStore (via Mockito.mock(TripStore.class)), then teach the mock how to respond to its methods. In this case, we mock a database with a simple HashMap, with a roster of the trips, keyed by their ID values. Each of the doAnswer() calls mocks a specific method on the TripStore, down to the details of having selectAll() return the trips ordered by title.

Whether this is worth the effort is for you to decide. For many projects, instrumentation tests will suffice. For larger projects, where the speed difference between unit tests and instrumentation tests is substantial, it might be worth the engineering time to create the mocks. While mocking is also useful for scenarios that are difficult to reproduce, it is unlikely that your DAO will have any of those scenarios.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.