Lesson Weekend

Testing is an important aspect of development, especially in Android. There are a number of factors that affect the behavior of a mobile app that we haven't had to consider when writing web apps. Android applications run on devices with limited memory, CPU power, and power supply and can be negatively impacted by external factors like connectivity, general system utilization, etc.

Testing in Android can be broken down into two categories:

  • Local tests which run on the JVM without an emulator or device
  • Instrumentation tests which require an android device or emulator to simulate a user

Local Testing

To run local tests we will use the Robolectric unit testing framework. With Robolectric, we will test the behaviors of a particular component in isolation of other components.

Configuration

Adding Libraries and Dependencies

Let’s start with a simple test using our MyRestaurants app. To get started, configure your app to use the 'org.apache.http.legacy' library (necessary for testing shadows, which we will cover momentarily) and add the following repositories and dependencies to your build.gradle file:

build.gradle(Module: app)

android {
  ...
    useLibrary 'org.apache.http.legacy'
  ...

  repositories {
      mavenCentral()
      jcenter()
  }
  ...
}

dependencies {
    ...
    testCompile 'org.robolectric:robolectric:3.0'
    testCompile 'org.robolectric:shadows-support-v4:3.0'
}

Creating Test Classes

Navigate to java/com.epicodus.myrestaurants(test), right click on the test package name, and create a new class called MainActivityTest:

create-new-test-class-android

Add the following annotation to our test class to allow our code to be run natively in the JVM instead of on an Android device:

com.epicodus.myrestaurants(test)/MainActivityTest.java
@Config(constants = BuildConfig.class, sdk = Build.VERSION_CODES.LOLLIPOP)
@RunWith(RobolectricGradleTestRunner.class)
public class MainActivityTest {

}

Remember, each time a class name appears red, it means the class needs to be imported. You can do this by clicking on the red and underlined class name, and pressing Option + Enter. After importing all classes, your MainActivityTest file should look like this:

com.epicodus.myrestaurants(test)/MainActivityTest.java
package com.epicodus.myrestaurants;

import android.os.Build;

import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;

@Config(constants = BuildConfig.class, sdk = Build.VERSION_CODES.LOLLIPOP)
@RunWith(RobolectricGradleTestRunner.class)

public class MainActivityTest {
}

Before we write our first test, we'll also want to configure our test class so it knows which Activity we will use to write our tests:

com.epicodus.myrestaurants(test)/MainActivityTest.java
package com.epicodus.myrestaurants;

import android.os.Build;

import org.junit.Before;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;

@Config(constants = BuildConfig.class, sdk = Build.VERSION_CODES.LOLLIPOP)
@RunWith(RobolectricGradleTestRunner.class)

public class MainActivityTest {
    private MainActivity activity;

    @Before
    public void setup() {
        activity = Robolectric.setupActivity(MainActivity.class);
    }
}

Writing Tests

For our first test, we will assert that the text in our MainActivity's TextView is equal to “MyRestaurants”. To do this, we need to give our TextView an ID:

activity_main.xml
<TextView
        ...
        android:id="@+id/appNameTextView" />

We are finally ready to write our first test:

MainActivityTest.java
package com.epicodus.myrestaurants;

import android.os.Build;
import android.widget.TextView;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;

@Config(constants = BuildConfig.class, sdk = Build.VERSION_CODES.LOLLIPOP)
@RunWith(RobolectricGradleTestRunner.class)

public class MainActivityTest {
    private MainActivity activity;

    @Before
    public void setup() {
        activity = Robolectric.setupActivity(MainActivity.class);
    }

    @Test
    public void validateTextViewContent() {
        TextView appNameTextView = (TextView) activity.findViewById(R.id.appNameTextView);
        assertTrue("MyRestaurants".equals(appNameTextView.getText().toString()));
    }
}

  • With your cursor over ‘assertTrue’, click Option + Enter, select Static import method... and select the first option to import it. This should add the line import static junit.framework.Assert.assertTrue; to the top of your file.
  • All JUnit tests require the @Test annotation before the method declaration.
  • activity.findViewById returns the appNameTextView which we will cast as type TextView.
  • Inside of assertTrue() we test to see that the text from our appNameTextView is equal to “MyRestaurants”

Running Tests

Let’s run this test and see if it passes. Right click on class name in the file structure view and select Run ‘MainActivityTest’:

running-unit-tests-android

The console should automatically open and display the progress of the running test. The progress bar indicator should turn green and read ‘1 test passed’. We just passed our first Android unit test -- nice work!

passing-test-in-console

Writing Tests with Shadows

Let's write one more test to verify the RestaurantsActivity is started when findRestaurantsButton is clicked. To do this, we will need to use something called shadows. Shadows are classes that modify or extend behavior of a class in the Android SDK. Robolectric looks for Shadow classes that correspond with any Android classes that are run as part of a Robolectric test.

Required Reading

Read about using shadows in Robolectric here and then check out the test below that asserts that the correct activity is launched when we click on our findRestaurantsButton:

Example Test Using Shadows

MainActivityTest.java
...
@Test
    public void secondActivityStarted() {
        activity.findViewById(R.id.findRestaurantsButton).performClick();
        Intent expectedIntent = new Intent(activity, RestaurantsActivity.class);
        ShadowActivity shadowActivity = org.robolectric.Shadows.shadowOf(activity);
        Intent actualIntent = shadowActivity.getNextStartedActivity();
        assertTrue(actualIntent.filterEquals(expectedIntent));
    }
...


Example GitHub Repo for MyRestaurants

Terminology


  • Local Testing: Tests that run locally on the JVM, without an emulator or device.

  • Instrumentation Testing: Tests that require an Android device or emulator to simulate a user.

  • Robolectric: A unit testing framework for Android applications.

  • Shadows: Classes that modify or extend behavior of a class in the Android SDK. Robolectric looks for Shadow classes that correspond with any Android classes that are run as part of a Robolectric test.

Tips


  • In order to use shadows, applications must be configured to use the 'org.apache.http.legacy' library.

  • Unit test classes should reflect the activity whose logic they're testing. (ie: Tests meant to cover logic present in MainActivity will reside in a MainActivityTest class. Tests covering logic in a WelcomeActivity will reside in a WelcomeActivityTest class.)

Example


Example GitHub Repo for MyRestaurants

Additional Resources