Skip to main content

Android : Sharing code and resources between UI and unit tests

As an Android developer you are already familiar with writing Unit and UI tests. This post is about "How to share the code and resources between your UI and unit tests?". I'd used "Appointments List" as an example scenario for this post.


Existing Setup

For UI testing, I'd have list of appointments API response in JSON format as "appointments.json" in res directory. The .json file is is converted to AppointmentsListResponse using JsonParser.

androidTest/java/com/example/AppointmentScreenTest.java
AppointmentsListResponse getFakeAppointmentsResponse() {
  InputStream in = getInstrumentation().getContext()
      .getResources()
      .openRawResource(com.example.android.internal.test.R.raw.appointments);

  AppointmentsListResponse fakeResponse =
      new JsonParser(in).parseTo(AppointmentsListResponse.class);  
}
androidTest/java/com/example/JsonParser.java

public class JsonParser
  String mStringToParse;
  private static Gson mGson = new Gson();

  public JsonParser(String parseString) {
    mStringToParse = parseString;
  }

  public JsonParser(InputStream is) {
    try (java.util.Scanner s = new java.util.Scanner(is)) {
      mStringToParse = s.useDelimiter("\\A").hasNext() ? s.next() : "";
    }
  }

  public <T> T parseTo(Class<T> targetClass) {
    try {
      return targetClass.cast(mGson.fromJson(mStringToParse, targetClass));
    } catch (ClassCastException e) {
      return null;
    }
  }
}
For presenter tests, I'd created fake response manually like shown below:

test/java/com/example/AppointmentPresenterTest.java
public static AppointmentsListResponse getFakeAppointmentListResponse() {
  AppointmentsListResponse response = new AppointmentsListResponse();
  response.setAppointments(getFakeAppointmentsList());
  response.setPatients(getFakePatientList());
  response.setDoctors(getFakeDoctorsList());
  return response;
}

What's the problem?

  • UI tests use real world data for testing but for unit tests we need to construct the data manually.
  • Requires 'Setter' methods for response class to facilitate test data creation.
  • Code repetition in both UI and unit tests.

What's the solution?


Step 1: Create a shared test folder called testShared  under app.
Step 2: Add a new File | New | Folder | Java Folder
Step 3: Add a new File | New | Folder | Java Resources Folder
Step 4: Add the test resources in the newly created resources directory. 
             e.g., For this scenario I have added "appointments.json" file. 
Step 5: Create a SharedTestHelper class which provides the test data for both UI & unit tests. 
Step 6: In your app/build.gradle, add the following lines
android.sourceSets {
  test {
    java.srcDirs += "$projectDir/src/testShared/java"
    resources.srcDirs += "$projectDir/src/testShared/resources"
  }

  androidTest {
    java.srcDirs += "$projectDir/src/testShared/java"
    resources.srcDirs += "$projectDir/src/testShared/resources"
  }
}
testShared/java/com/example/SharedTestHelper.java
public final class SharedTestHelper {
  private final Context mContext;
  private final ClassLoader mClassLoader;

  public SharedTestHelper() {
    this(null);
  }

  public SharedTestHelper(Context context) {
    mContext = context;
    if (context == null) {
      mClassLoader = ClassLoader.getSystemClassLoader();
    } else {
      mClassLoader = context.getClassLoader();
    }
  }

  public AppointmentsListResponse getFakeAppointmentsResponse() {
    InputStream in = mClassLoader.getResourceAsStream("appointments.json");
    AppointmentsListResponse fakeResponse =
        new JsonParser(in).parseTo(AppointmentsListResponse.class);
    return fakeResponse;
  }
}
Unit Tests - Use JVM's Built-In classloader. UI Tests - Use Dalvik's classloader which can be accessed via instrumentation context.
Usage

UI Tests

Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
new SharedTestHelper(instrumentation.getTargetContext()).getFakeAppointmentsResponse();

Unit Tests

new SharedTestHelper().getFakeAppointmentsResponse();
As you see we are reusing the same test data for both UI and unit tests, no extra setter methods. The entire mock data generation is shared between both tests, so there is no code repetition.
Thanks for @PavelDudka's awesome post Android Test tricks - Sharing code between UI & unit tests 

Comments

Popular posts from this blog

Android Studio Ninja Tip: Custom try/catch code template

As an Android Studio user, you should be familiar with the “ surround-with try-catch” functionality. Surround with try-catch When you use Ctrl+Alt+T (Linux) / Cmd+Alt+T (Mac) shortcut, the built-in try-catch generator auto generates a call to the printStackTrace() method of class Exception in the catch block. After this, we manually add some code to log the exception. e.g., Log.e(TAG, “Oops! something gone wrong”, e); As a power user, you may have used some or all of the following built-in Live templates: Live Templates You can save yourself the pain of these 5-keystrokes or repetitive typing or copy pasting the code by becoming a Ninja who uses a custom built-in try-catch template. Steps for customization Step-1 : Go to Android Studio Preferences | Editor | File and Code Templates Step-2:  Select Code | Catch Statement Body Step-3:  Replace the existing template from: ${EXCEPTION}.printStackTrace(); with: Log.e(getClass().getSimpleName(), "Ex

BSNL HUAWEI WLL Modem Installation

Hi everyone, Do u experience problem in connecting to Internet using BSNL WLL HUAWEI ETS 1201 from Ubuntu. Here is my Solution for it: Step 1 : Install Wvdial (a program that makes it easy to connect your Linux workstation to the Internet.). In Ubuntu there is a Dependency hell problem U need to download “Other Packages related to Wvdial” before installing Wvdial. I hope U will do it! After installation just check it by:   developer@ubuntu:~$ which wvdial     /usr/bin/wvdial When the above message is received then Your Wvdial installation is fine and we move to the next step Step 2 : Just run developer@ubuntu:~$sudo wvdialconf In my case I got the error as shown below: ttyS0<*1>: ATQ0 V1 E1 -- failed with 2400 baud, next try: 9600 baud ttyS0<*1>: ATQ0 V1 E1 -- failed with 9600 baud, next try: 115200 baud ttyS0<*1>: ATQ0 V1 E1 -- and failed too at 115200, giving up. Modem Port Scan<*1>: S1 S2 S3 Sorry, no modem was detected! Is it in use by anothe