The basics of Unit and Instrumentation Testing on Android

There has been a lot written about unit testing and instrumentation testing on the internet. However, I felt that there is a need for a very basic explanation of how to add Unit tests and Instrument tests to your android Application. In this article I’m going to give you the bare basics of writing both Unit and Instrumentation tests, no mocking, no injection patters, nothing.

Getting started

Take any existing project you have with some decent amount of code. You will need:

  • Android studio 1.4+
  • Android SDK 6 (API 23)

What is the difference between Unit and Instrumentation Tests

Often Unit tests are referred to as “local tests” or “local unit tests”. The main reason for this seems to be that you want to be able to run tests without a device or an emulator attached.

Unit tests cannot test the UI for your app without mocking objects such as an Activity.

Instrumentation tests run on a device or an emulator. In the background, your app will be installed and then a testing app will also be installed which will control your app, lunching it and running UI tests as needed.

Instrumentation tests can be used to test none UI logic as well. They are especially useful when you need to test code that has a dependency on a context.

Creating your first Unit test

Lets start with the unit tests as they are fairly easy to set-up and less prone to quirks.

Start by adding a dependency on JUnit 4 in your project. The dependency is of type `testCompile` which means that the dependencies is only required to compile the test source of the project. By default, also includes the compiled production classes and the compile time dependencies.

dependencies {
...
testCompile "junit:junit:4.12"
...
}

After you add the dependency, make sure to synchronize your project.

Android studio should have created the folders structure for unit tests by default, if not make sure the following directory structure exists. You can check if the directory structure exists or create it in Explorer or Finder.

<Project Dir>/app/src/test/java/com/example/appname

You should ideally place your unit tests code in the same package as the package in which your code reside in your source. This is not required, but it is considered a good practice.

If you want to test the following code:

package com.example.appname.util;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;


public class DateUtils {

public static final SimpleDateFormat DISPLAY;
public static final SimpleDateFormat DISPLAY_SHORT;

public static final long SECOND_MILLISECONDS = 1000l;
public static final long MINUTE_MILLISECONDS =
SECOND_MILLISECONDS * 60;
public static final long HOUR_MILLISECONDS =
MINUTE_MILLISECONDS * 60;
public static final long DAY_MILLISECONDS =
HOUR_MILLISECONDS * 24;

static {
//Use 12 or 24 hour time depending on device config.
DISPLAY = new SimpleDateFormat(
"EEEE, dd MMMM yyyy",
Locale.getDefault());
DISPLAY_SHORT = new SimpleDateFormat("EEE",
Locale.getDefault());
DISPLAY.setTimeZone(TimeZone.getDefault());
DISPLAY_SHORT.setTimeZone(TimeZone.getDefault());
}

public static Date epocSecondsToDate(long epocSeconds) {
Calendar c = Calendar.
getInstance(TimeZone.getTimeZone("UTC"));
c.setTimeInMillis(epocSeconds * 1000);
return c.getTime();
}

public static String dateToDayDateString(Date date,
boolean useShortFormat) {
if (useShortFormat) {
return DISPLAY_SHORT.format(date).toUpperCase();
} else {
return DISPLAY.format(date).toUpperCase();
}
}

public static String epocSecondsToDisplayDateTimeString(long epocSeconds) {
Date d = epocSecondsToDate(epocSeconds);
return dateToDayDateString(d, false);
}

}

We will begin by creating DateUtilsTest in our test directory under the package com.alimuzaffar.examplecode.util. The directory structure will look as follows:

// The DateUtils class under source
<Project Dir>/app/src/main/java/com/example/appname/util/DateUtils.java
// The DateUtilsTest class under the test directory
<Project Dir>/app/src/test/java/com/example/appname/util/DateUtilsTest.java

Our DateUtilsTest class will be as follows:

package com.example.appname.util;
import com.example.appname.util.DateUtils;
import org.junit.Test;
import java.util.Date;
import static org.junit.Assert.assertFalse;
public class DateUtilsTest {
    @Test
public void dateUtilsFormat_isCorrect() throws Exception {
long epoc = 1446885450; //7th Nov 2015
Date date = DateUtils.epocSecondsToDate(epoc);
assertEquals("Date time in millis is wrong",
epoc * 1000, date.getTime());
String day = DateUtils.dateToDayDateString(date, true);
assertEquals("Day is wrong", "SAT", day);
}
    @Test
public void dateUtilsFormat_anotherTest() throws Exception {
...
}
}

Running the Unit tests

In order to run the Unit tests, you need to make sure that Gradle is synchronized and then from the Build Variants, under Test Artifacts select “Unit Tests”

The project structure changes a bit and the test classes under the test directory should become activated. Your project structure should look something like this:

In order to run the unit test, select the unit test you want to run and select Run ‘<Unit Test Name>’.

When the unit tests are run, successfully or otherwise, you should be able to see this in the run menu at the bottom of the screen.

Create your first Instrumentation Test

To write instrumentation tests, we don’t need to add any new dependencies. By default, instrumentation tests are most suited for checking values of UI components when an activity is run. However, as I mentioned earlier, instrumentation tests work by using a test app to control your app. For this reason any actions you take on the UI thread will throw an exception. There are two ways around this. One would be to use @UiThreadTest annotation, or using espresso.

Since espresso seems to be the way forward, we’ll use espresso to take actions on the main thread such as button clicks, change text etc. Add a dependency on espresso:

// Espresso UI Testing
androidTestCompile "com.android.support.test.espresso:espresso-core:2.2.1"
// Optional if you need to detect intents.
androidTestCompile "com.android.support.test.espresso:espresso-intents:2.2.1"

Instrumentation tests are created in an androidTest folder. Just like unit tests, under androidTest, you should create the same package structure as that of your app.

<Project Dir>/app/src/androidTest/java/com/example/appname

If you want to test a simple activity, you should create you test class in the same package as your Activity.

Example:

// The activity to test
<Project Dir>/app/src/main/java/com/example/appname/activity/MainActivity.java
// The test
<Project Dir>/app/src/androidTest/java/com/example/appname/activity/MainActivityTest.java

In order to test the activity, we use an instance of the ActivityInstrumentationTestCase2<T> class. We can setup our test in the setUp method and then write a test to make sure our setup was successful.

public class MainActivityTest extends 
ActivityInstrumentationTestCase2<MainActivity> {

private MainActivity mTestActivity;
private TextView mTestEmptyText;
private FloatingActionButton mFab;

public MainActivityTest() {
super(MainActivity.class);
}

@Override
protected void setUp() throws Exception {
super.setUp();

// Starts the activity under test using
// the default Intent with:
// action = {@link Intent#ACTION_MAIN}
// flags = {@link Intent#FLAG_ACTIVITY_NEW_TASK}
// All other fields are null or empty.
mTestActivity = getActivity();
mTestEmptyText = (TextView) mTestActivity
.findViewById(R.id.empty);
mFab = (FloatingActionButton) mTestActivity
.findViewById(R.id.fab);
}

/**
* Test if your test fixture has been set up correctly.
* You should always implement a test that
* checks the correct setup of your test fixture.
* If this tests fails all other tests are
* likely to fail as well.
*/
public void testPreconditions() {
// Try to add a message to add context to your assertions.
// These messages will be shown if
// a tests fails and make it easy to
// understand why a test failed
assertNotNull("mTestActivity is null", mTestActivity);
assertNotNull("mTestEmptyText is null", mTestEmptyText);
assertNotNull("mFab is null", mFab);
}

}

Note: All test methods need to start with testXXX or be annotated with the @Test annotation.

Running the instrumentation test

In order to run the instrumentation test, you need to select Android Instrumentation Tests under Test Artifact in the Build Variants window.

You should see the project structure change and the classes under the androidTest folder should now be visible.

In order to run the test, do a clean build of your project (this is not required, but you can have some odd issues when you switch between Unit Tests and Instrumentation Tests and this just helps reduce issues). Then right click on the Instrumentation test you want to run and select Run ‘<InstrumentationTestName>’.

When the project is built, you should be prompted to run it on an emulator or a device. In my experience it should work on most versions of Android, however, with espresso, instrumentation tests seem to mostly work on Android 5.1+.

Running UI Tests

So far, we have been able to get a reference to the activity and from it, a reference to various UI components. If you want to read from UI components or do other operations that don’t require the UI thread, you should be able to do this like so:

/**
* Tests the correctness of the initial text.
*/
public void testEmptyView_labelText() {
// It is good practice to read the string
// from your resources in order to not break
// multiple tests when a string changes.
String expected = mTestActivity.getString(R.string.help_text);
String actual = mTestEmptyText.getText().toString();
assertEquals("mTestEmptyText contains wrong text",
expected, actual);
}

You don’t have to create a different method for each test, however, it is a good practice. If you want to read and write from the database, you can do this like so:

public void testDatabaseMethods_add() {
RecentLocationsHelper.addLocationToDB(123456, "Hello World");
RecentLocation r = RecentLocationsHelper.getLocation(123456);
assertEquals("Ids do not match", 123456, r.getId());
assertEquals("Names do not match", “Hello World”, r.getName());
// Clean up database
boolean result = RecentLocationsHelper.deleteLocation(123456);
assertTrue("Delete failed", result);
 }

If you need a reference to the context, then remember, we got one in our setUp() method. Or else, you can just use getActivity().

If you do try to change the value of a View or do any other operations that must be executed on the main thread, you’ll get an error:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

There are 2 solutions to this, you can either annotate your method with android.test.UiThreadTest annotation.

@UiThreadTest
public void testMainThread_operations() {
mTestEmptyText.setText("Hello World");
}

The second option is to use the new espresso framework.

public void testMainThread_operations() {
Espresso.onView(ViewMatchers.withId(R.id.txt_search))
.perform(ViewActions.typeText("Hi"));
}

The typeText action will only work on EditText and other Views that can be edited (cannot be used on TextView). There are other actions you can perform such as replaceText() and click(), closeSoftKeyboard() etc.

Finally

That in short are the basics of testing on Android. Using these simple techniques, you won’t be able to provide vast code coverage, but you should be able to do any where between 20% — 40% code coverage at lease depending on how complex your app is and how well the code is structured. In order to do more, you will start to look into refactoring your code and mocking objects.


Yay! you made it to the end! We should hang out! feel free to follow me on Medium, LinkedIn, Google+ or Twitter.