Chapter 5. Testing

5.1 Unit Testing

Problem

You want to test the non-Android parts of your app.

Solution

Use the experimental unit testing support added in version 1.1 of Android Studio and the Gradle plug-in for Android.

Discussion

The Eclipse Android Development Tools (ADT) plug-in only supported integration tests, and required developers to create a separate project just for the tests themselves. One of the advantages of the switch to Android Studio and Gradle was support for tests inside the Android project itself.

Prior to version 1.1 of Android Studio and the associated Gradle plug-in, however, those tests were still restricted to integration tests, meaning you needed either an emulator or a connected device in order to run the tests. Integration tests can be very powerful and useful, and are the subject of Recipes 5.3 and  5.4.

This recipe discusses true unit tests, which run on a local JVM on a development machine. Unlike the integration tests that use an androidTest source set, the unit tests reside in the src/test/java directory of your app.

When you generate a new Android app in Android Studio, a sample unit test is provided for you. It resides in the src/test/java tree, but is not currently in the classpath, as Figure 5-1 shows.

rega 0501
Figure 5-1. Sample unit test generated by Android Studio, under app/src

The generated test is shown in Example 5-1.

Example 5-1. Generated sample unit test
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);
    }
}

This type of test should look familiar to anyone who has used JUnit in the past, which should be virtually every Java developer. The @Test annotation from JUnit 4 indicates that the addition_isCorrect method is a test method. The assertEquals method is a static method in the Assert class (note the static import of all static methods in that class), whose first argument is the correct answer and whose second argument is the actual test.

In order to run the test, you need to do what the comment says, which is to select the Test Artifact in the Build Variants view, as shown in Figure 5-2.

rega 0502
Figure 5-2. Selecting the “Unit Tests” artifact in Build Variants

Note that by selecting “Unit Tests,” the directory tree under src/test/java is now understood by Android Studio to contain test sources (because the folder is shown in green) and the com/oreilly/helloworld tree is now interpreted as a package.

One last step is required before executing the unit test. You need to make sure JUnit is included as a testCompile dependency in your project. As shown in Recipe 1.5, this is already the case for the default project. The dependencies section of the module build file is repeated in Example 5-2.

Example 5-2. JUnit dependency in the module build.gradle file
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'  1
    compile 'com.android.support:appcompat-v7:23.0.1'
}
1

JUnit dependency added during testCompile

You can now run the tests from Gradle using the test target, but be prepared for a lot of effort (see Example 5-3).

Example 5-3. Executing the unit test
> ./gradlew test
Starting a new Gradle Daemon for this build (subsequent builds will be faster).
:app:preBuild UP-TO-DATE
:app:preArrogantStarkDebugBuild UP-TO-DATE
:app:checkArrogantStarkDebugManifest
:app:preArrogantStarkReleaseBuild UP-TO-DATE
:app:preArrogantWayneDebugBuild UP-TO-DATE
:app:preArrogantWayneReleaseBuild UP-TO-DATE
:app:preFriendlyStarkDebugBuild UP-TO-DATE
:app:preFriendlyStarkReleaseBuild UP-TO-DATE
:app:preFriendlyWayneDebugBuild UP-TO-DATE
:app:preFriendlyWayneReleaseBuild UP-TO-DATE
// ... all the stages for all the variants ...
:app:compileObsequiousWayneReleaseUnitTestJavaWithJavac
:app:compileObsequiousWayneReleaseUnitTestSources
:app:assembleObsequiousWayneReleaseUnitTest
:app:testObsequiousWayneReleaseUnitTest
:app:test

BUILD SUCCESSFUL

The single test ran for every variant, generating HTML outputs in the app/build/reports/tests folder, shown in Example 5-4.

Example 5-4. Output folders for the tests
> ls -F app/build/reports/tests/
arrogantStarkDebug/     arrogantWayneRelease/
friendlyWayneDebug/     obsequiousStarkRelease/
arrogantStarkRelease/   friendlyStarkDebug/
friendlyWayneRelease/   obsequiousWayneDebug/
arrogantWayneDebug/     friendlyStarkRelease/
obsequiousStarkDebug/   obsequiousWayneRelease/

Opening the index.html file in any of those folders shows the test report in Figure 5-3.

rega 0503
Figure 5-3. Test report in HTML

You can drill down to the ExampleUnitTest class and see the specific results (Figure 5-4).

To restrict the tests to a single variant and even a single test class, use the --tests flag, as in Example 5-5.

Example 5-5. Running the tests in only one test class
> ./gradlew testFriendlyWayneDebug --tests='*.ExampleUnitTest'

The variant is still constructed, but only that one, and only the tests in the ExampleUnitTest class are run.

As an alternative, if you right-click in the test itself and run it inside Android Studio, it runs for the current variant only and provides a nice view showing the results (Figure 5-5).

rega 0504
Figure 5-4. Result of ExampleUnitTest tests
rega 0505
Figure 5-5. Test results in Android Studio

The only problem is, this didn’t actually test anything significant. That’s the point, actually. When using the JUnit support, you can’t test anything that relies on the Android SDK. Unit testing is only for the purely Java parts of your application.

Note

Unit testing support is only for the non-Android parts of your application.

In Recipe 4.5, the library accessed a web service, downloaded JSON data, parsed it, and updated a TextView with an included value. If you like, you can test just the parsing part of that process, as in Example 5-6.

Example 5-6. Test the Gson parser
import com.google.gson.Gson;

import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class IcndbJokeTest {
    private String jsonTxt = "{"type": "success", "value": {"id": 451,
        "joke": "Xav Ducrohet writes code that optimizes itself.",
        "categories": ["nerdy"]}}";  1

    @Test
    public void testGetJoke() throws Exception {
        Gson gson = new Gson();
        IcndbJoke icndbJoke = gson.fromJson(jsonTxt, IcndbJoke.class);
        String correct = "Xav Ducrohet writes code that optimizes itself.";

        assertNotNull(icndbJoke);  2
        assertEquals(correct, icndbJoke.getJoke()); 3
    }
}
1

String should be all on one line

2

Check that parsing yielded a non-null result

3

Check that the retrieved joke is correct

The good news is that unit tests are fast, at least relative to integration tests, because they don’t require deployment to an actual device or an emulator. If you have Java classes that are not dependent on Android classes, unit tests are great way to make sure they’re working properly. Test Driven Development (TDD) has not yet been adopted in the mobile world the way it has in the regular Java world, but this is a good way to get started.

See Also

Recipe 5.3 illustrates Activity tests using the Robotium library. Recipe 5.4 does the same using the Espresso framework from Google. JUnit information can be found at http://junit.org.

5.2 Testing with the Android Testing Support Library

Problem

You want to test the Android components of your app.

Solution

Use the new testing classes to implement JUnit-style tests of your app.

Discussion

First, a meta-note on terminology: testing Android components, like activities or services, requires deployment of the app to a connected device or emulator. The testing library is based on JUnit, but these are not unit tests in the strictest sense. They’re either integration tests or functional tests, depending on how you use those terms.

Since the approach here is to drive a deployed app programmatically and check that the UI changes correctly, the term “functional” will be preferred here. You will see the term integration used frequently in the documentation, however.

Note

Despite the word “unit” in AndroidJUnitRunner and other test classes, Android tests are inherently functional. They require either an emulator or a connected device in order to run.

The Android Testing Support Library is added as an optional dependency through the SDK Manager, as shown in Figure 5-6.

Testing is part of the “Android Support Repository” download, as Figure 5-6 illustrates. The testing classes reside in the android.support.test package.

The documentation shows that to add all the relevant classes to your Gradle build file, use the dependencies in Example 5-7.

Example 5-7. Gradle dependencies for the Android Testing Support Library
dependencies {
    androidTestCompile 'com.android.support.test:runner:0.3'
    // Set this dependency to use JUnit 4 rules
    androidTestCompile 'com.android.support.test:rules:0.3'
}
rega 0506
Figure 5-6. Adding the Android Testing Support Library using the SDK Manager

The AndroidJUnitRunner class has support for JUnit 4 annotations. To use it, you can add the @RunWith annotation from JUnit to your test class, or you can add a setting to the defaultConfig block of your Gradle build file.

Example 5-8. Using AndroidJUnitRunner by default
android {
    defaultConfig {
        // ... other settings ...
        testInstrumentationRunner
            "android.support.test.runner.AndroidJUnitRunner"
    }
}

It’s particularly easy to test a labels on a layout using the test support classes. An example is shown in Example 5-9.

Example 5-9. Testing component labels
@MediumTest  1
@RunWith(AndroidJUnit4.class)  2
public class MyActivityLayoutTest
        extends ActivityInstrumentationTestCase2<MyActivity> {

    private MyActivity activity;
    private TextView textView;
    private EditText editText;
    private Button helloButton;

    public MyActivityLayoutTest() {
        super(MyActivity.class);
    }

    @Before
    public void setUp() throws Exception {
        super.setUp()
        injectInstrumentation(InstrumentationRegistry.getInstrumentation()); 3
        activity = getActivity();

        textView = (TextView) activity.findViewById(R.id.text_view);
        editText = (EditText) activity.findViewById(R.id.edit_text);
        helloButton = (Button) activity.findViewById(R.id.hello_button);
    }

    @After
    public void tearDown() throws Exception {
        super.tearDown();
    }

    @Test
    public void testPreconditions() {
        assertNotNull("Activity is null", activity);
        assertNotNull("TextView is null", textView);
        assertNotNull("EditText is null", editText);
        assertNotNull("HelloButton is null", helloButton);
    }

    @Test
    public void textView_label() {
        final String expected = activity.getString(R.string.hello_world);
        final String actual = textView.getText().toString();
        assertEquals(expected, actual);
    }

    @Test
    public void editText_hint() {
        final String expected = activity.getString(R.string.name_hint);
        final String actual = editText.getHint().toString();
        assertEquals(expected, actual);
    }

    @Test
    public void helloButton_label() {
        final String expected = activity.getString(R.string.hello_button_label);
        final String actual = helloButton.getText().toString();
        assertEquals(expected, actual);
    }
}
1

Expected durations are @SmallTest, @MediumTest, and @LargeTest

2

Use the JUnit 4 runner for Android

3

Needed for the new JUnit 4 runner

The new AndroidJUnitRunner is part of the Android Support Test Library. It adds JUnit 4 support, so that tests can be annotated rather that specified using the old JUnit 3 naming convention. It has other extra capabilities. See the Android Testing Support Library documentation for details.

In Example 5-9, the attributes represent widgets on the user interface. The @Before method looks them up and assigns them to the attributes. The docs recommend using a testPreconditions test like the one shown, just to demonstrate that the widgets were found. That test is no different from any of the others, but a failure there makes it easy to see what went wrong.

The other tests all look up strings from the string resources and compare them to the labels on the actual widgets. Note that nothing is being modified here—the test is essentially read-only.

Finally, the @MediumTest annotation is used to indicate the size of a test method. Tests that only take a few milliseconds are marked as @SmallTest, those that take on the order of 100 milliseconds are @MediumTest, and longer ones are marked @LargeTest.

From Gradle, running tests that require connected devices or emulators is done through the connectedCheck task.

Tip

Run the connectedCheck task to execute tests on all emulators and connected devices concurrently.

A sample execution is shown in Example 5-10. The sample test was run concurrently on two separate emulators.

Example 5-10. Executing the tests from Gradle
> ./gradlew connectedCheck
:app:preBuild UP-TO-DATE
:app:preDebugBuild UP-TO-DATE
:app:checkDebugManifest
:app:prepareDebugDependencies
// ... lots of tasks ...
:app:packageDebugAndroidTest UP-TO-DATE
:app:assembleDebugAndroidTest UP-TO-DATE
:app:connectedDebugAndroidTest
:app:connectedAndroidTest
:app:connectedCheck

BUILD SUCCESSFUL

The output report resides in the http://robolectric.orgapp/build/reports/androidTests/connected directory. A sample output report is shown in Figure 5-7.

rega 0507
Figure 5-7. Sample test output organized by test

The sample output shows the emulator names and the results of all the tests. Clicking the “Devices” button switches the output to organize it by device, as shown in Figure 5-8.

The classes in the Android Support Test Library can do much more than this, but the tests start getting complicated quickly. When you want to drive the UI by adding data, clicking buttons, and checking results, there are alternative libraries, like Robotium and Espresso, that make the process much easier. Recipes that use those libraries are referenced in the “See Also” section.

rega 0508
Figure 5-8. Sample test output organized by device

See Also

Recipe 5.3 shows how to use the Robotium library to drive the UI. Google now provides the Espresso library as part of the Android Test Kit project. Espresso tests are demonstrated in Recipe 5.4.

5.3 Functional Testing with Robotium

Problem

You want to test activities using the Robotium library.

Solution

Add the Robotium dependency and script your tests.

Discussion

The Android Test Support Library has classes for accessing widgets on activities, but there are easier ways to drive an Android UI. While this is not a book about testing, it’s easy to add the Robotium library dependency to Gradle and run tests that way.

The Robotium project is described as “like Selenium, but for Android.” It’s a test automation framework that makes it easy to write black-box UI tests for Android apps.

Just add the Robotium library as a dependency in the module Gradle build file, as in Example 5-11.

Example 5-11. Add the Robotium dependency
dependencies {
    androidTestCompile 'com.jayway.android.robotium:robotium-solo:5.4.1'
}

Consider a simple activity called MyActivity, shown in Example 5-12, that prompts the user for a name, adds it to an Intent, and starts a WelcomeActivity that greets the user.

Example 5-12. The MyActivity class is a “Hello, World” app
public class MyActivity extends Activity {
    private TextView textView;
    private EditText editText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my);

        textView = (TextView) findViewById(R.id.text_view);
        editText = (EditText) findViewById(R.id.edit_text);
        Button helloButton = (Button) findViewById(R.id.hello_button);
        helloButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                sayHello(v);
            }
        });
    }

    public void sayHello(View view) {
        String name = editText.getText().toString();
        Intent intent = new Intent(this, WelcomeActivity.class);
        intent.putExtra("name", name);
        startActivity(intent);
    }
}

Robotium provides a class called com.robotium.solo.Solo, which wraps both the activity being tested and the Instrumentation object. It allows you to add text, click buttons, and more, without worrying about being on or off the UI thread. An example that tests the given activity is shown in Example 5-13.

Example 5-13. A Robotium test for MyActivity
public class MyActivityRobotiumTest
    extends ActivityInstrumentationTestCase2<MyActivity> {  1

    private Solo solo;  2

    public MyActivityRobotiumTest() {
        super(MyActivity.class);
    }

    public void setUp() {
        solo = new Solo(getInstrumentation(), getActivity());  3
    }

    public void testMyActivity() {
        solo.assertCurrentActivity("MyActivity", MyActivity.class);
    }

    public void testSayHello() {
        solo.enterText(0, "Dolly");
        solo.clickOnButton(
            getActivity().getString(R.string.hello_button_label));
        solo.assertCurrentActivity("WelcomeActivity", WelcomeActivity.class);
        solo.searchText("Hello, Dolly!");
    }

    public void tearDown() {
        solo.finishOpenedActivities();
    }
}
1

Activity tests all extend this class

2

The Solo reference from Robotium

3

Instantiate the Solo reference

Robotium tests extend ActivityInstrumentationTestCase2, as with all activity tests. The Solo instance is initialized with the activity and retrieved instrumentation instances. The tests themselves use methods from the Solo class, like enterText, clickOnButton, or searchText.

The only downside to using Robotium is that the tests use the old JUnit 3 structure, with predefined setUp and tearDown methods as shown, and all tests have to follow the pattern public void testXYZ(). Still, the ease of writing the tests is remarkable.

The test class is stored in the same androidTest hierarchy as other Android tests, and executed on all emulators and connected devices simultaneously through the connectedCheck task (Example 5-14).

Example 5-14. Executing the tests from Gradle
> ./gradlew connectedCheck
:app:preBuild UP-TO-DATE
:app:preDebugBuild UP-TO-DATE
:app:checkDebugManifest
:app:prepareDebugDependencies
// ... lots of tasks ...
:app:packageDebugAndroidTest UP-TO-DATE
:app:assembleDebugAndroidTest UP-TO-DATE
:app:connectedDebugAndroidTest
:app:connectedAndroidTest
:app:connectedCheck

BUILD SUCCESSFUL

The result is shown in Figure 5-9 after running on two emulators.

rega 0509
Figure 5-9. Robotium test output

Clicking the “Devices” button shows the same results, organized by device (Figure 5-10).

The full Robotium JavaDocs offer additional details and sample projects.

rega 0510
Figure 5-10. Robotium test output organized by device

See Also

Activity testing using the Android Support Library is covered in Recipe 5.2. Testing with Espresso is covered in Recipe 5.4.

5.4 Activity Testing with Espresso

Problem

You want to test Android activities using the Espresso library from Google.

Solution

Add the Espresso dependencies to your Gradle build and write tests to use it.

Discussion

The Espresso testing library has been added to the “Android Test Kit” project, part of Google’s testing tools for Android. Documentation for Espresso resides in a wiki. Since Espresso is a Google project and specifically designed for Android, it’s reasonable to assume that it will be the preferred mechanism for Android testing in the future.

While this is not a book on testing, setting up and running Espresso tests fits the normal Gradle practices, so a brief illustration is included here.

Espresso is included in the Android Support Repository, which is added under “Extras” in the SDK Manager. This process was illustrated in a figure in Recipe 5.2, repeated here in Figure 5-11.

rega 0511
Figure 5-11. Adding the Android Support Library using the SDK Manager

To use Espresso in your project, add two androidTestCompile dependencies, as shown in Example 5-15.

Example 5-15. Adding the Espresso dependencies
dependencies {
    androidTestCompile 'com.android.support.test:runner:0.5'
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
}

This actually leads to a conflict in versions of the support annotations library, because Espresso relies on version 23.1.1, while SDK 23 includes version 23.3.0 of the same library. You get an error similar to:

WARNING: Error:Conflict with dependency
'com.android.support:support-annotations'. Resolved versions for app (23.3.0) and
test app (23.1.1) differ. See http://g.co/androIDstudio/app-test-app-conflict
for details.

While that may be resolved by the time you build your application, let’s make lemonade out of those lemons by showing how to fix it. In the top-level Gradle build file, simply force a resolution in the allProjects section, as shown in Example 5-16.

Example 5-16. Resolving a conflict in library versions
allprojects {
    repositories {
        jcenter()
    }

    configurations.all {
        resolutionStrategy.force
            'com.android.support:support-annotations:23.3.0'
    }
}

Espresso also requests that you set the testInstrumentationRunner in the defaultConfig block to use the AndroidJUnitRunner, as in Recipe 5.2. The complete module build file therefore looks like that shown in Example 5-17.

Example 5-17. The full module build.gradle file
apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"

    defaultConfig {
        applicationId "com.nfjs.helloworldas"
        minSdkVersion 16
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner
            'android.support.test.runner.AndroidJUnitRunner'
    }
}

dependencies {
    compile 'com.android.support:support-annotations:23.3.0'
    androidTestCompile 'com.android.support.test:runner:0.5'
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
}

Espresso tests love to use static methods, both in Espresso classes and in Hamcrest matchers. Consequently, the test shown in Example 5-18 includes the import statements for clarity.

Example 5-18. An Espresso test, with imports
package com.nfjs.helloworldas;

import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.test.ActivityInstrumentationTestCase2;
import android.test.suitebuilder.annotation.MediumTest;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

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.typeText;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.CoreMatchers.containsString;

@RunWith(AndroidJUnit4.class)
@MediumTest
public class MyActivityEspressoTest
    extends ActivityInstrumentationTestCase2<MyActivity> {

    public MyActivityEspressoTest() {
        super(MyActivity.class);
    }

    @Rule
    public ActivityTestRule<MyActivity> mActivityRule =
        new ActivityTestRule<>(MyActivity.class);

    @Test
    public void testHelloWorld() {
        onView(withId(R.id.edit_text))
                .perform(typeText("Dolly"));
        onView(withId(R.id.hello_button))
                .perform(click());
        onView(withId(R.id.greeting_text))
                .check(matches(withText(containsString("Dolly"))));
    }
}

The simple DSL focuses on user actions rather than activities. From this test, it is not obvious that clicking the button actually shifted from the MyActivity class to the WelcomeActivity class, but that did in fact happen. The results are shown in Figure 5-12.

rega 0512
Figure 5-12. Espresso test results

Once again, clicking the “Devices” button shows the results organized by device rather than test, as in Figure 5-13.

rega 0513
Figure 5-13. Espresso test results organized by device

Espresso is an interesting DSL approach to writing functional tests. It is likely to be a recommended API for the future.

Collecting Test Results

If your app includes multiple flavors or modules, the HTML test reports will be organized into separate subdirectories. This makes it tedious to examine each one individually.

Fortunately, there is a plug-in available to collect all the reports into a single build folder. In the top-level build file, after the buildscript block, include the android-reporting plug-in. See Example 5-19 for details.

Example 5-19. Adding the android-reporting plug-in
allprojects {
    repositories {
        jcenter()
    }

    configurations.all {
        resolutionStrategy.force
            'com.android.support:support-annotations:23.3.0'
    }
}

apply plugin: 'android-reporting' 1
1

The Android reporting plug-in collects test reports into a single file

Now if you run the mergeAndroidReports task, everything will be collected into a single file.

Example 5-20. Merging Android test reports
> ./gradlew deviceCheck mergeAndroidReports --continue

The --continue flag is a standard Gradle flag, telling the build to keep going even if there are failed tests. The result when running with multiple variants should be similar to that in Figure 5-14.

rega 0514
Figure 5-14. Merged test reports from app with multiple variants

See Also

Activity testing using the Android Support Library is covered in Recipe 5.2. Testing with the Robotium library is covered in Recipe 5.3. The technique listed here for merging test reports works with any tests, not just Espresso.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.41.235