Use the experimental unit testing support added in version 1.1 of Android Studio and the Gradle plug-in for Android.
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.
The generated test is shown in Example 5-1.
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.
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.
dependencies
{
compile
fileTree
(
dir:
'libs'
,
include:
[
'*.jar'
]
)
testCompile
'junit:junit:4.12'
compile
'com.android.support:appcompat-v7:23.0.1'
}
You can now run the tests from Gradle using the test
target, but be prepared for a lot of effort (see Example 5-3).
> ./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.
> 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.
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.
> ./gradlew testFriendlyWayneDebug --tests='*.ExampleUnitTest'
The variant is still constructed, but only that one, and only the tests in the Example
UnitTest
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).
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.
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.
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"]}}"
;
@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
)
;
assertEquals
(
correct
,
icndbJoke
.
getJoke
(
)
)
;
}
}
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.
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.
Use the new testing classes to implement JUnit-style tests of your app.
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.
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.
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'
}
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.
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.
@MediumTest
@RunWith
(
AndroidJUnit4
.
class
)
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
(
)
)
;
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
)
;
}
}
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.
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.
> ./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.
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.
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.
Add the Robotium dependency and script your tests.
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.
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.
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.
public
class
MyActivityRobotiumTest
extends
ActivityInstrumentationTestCase2
<
MyActivity
>
{
private
Solo
solo
;
public
MyActivityRobotiumTest
(
)
{
super
(
MyActivity
.
class
)
;
}
public
void
setUp
(
)
{
solo
=
new
Solo
(
getInstrumentation
(
)
,
getActivity
(
)
)
;
}
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
(
)
;
}
}
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).
> ./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.
Clicking the “Devices” button shows the same results, organized by device (Figure 5-10).
The full Robotium JavaDocs offer additional details and sample projects.
Activity testing using the Android Support Library is covered in Recipe 5.2. Testing with Espresso is covered in Recipe 5.4.
Add the Espresso dependencies to your Gradle build and write tests to use it.
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.
To use Espresso in your project, add two androidTestCompile
dependencies, as shown in Example 5-15.
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.
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.
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.
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.
Once again, clicking the “Devices” button shows the results organized by device rather than test, as in Figure 5-13.
Espresso is an interesting DSL approach to writing functional tests. It is likely to be a recommended API for the future.
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.
allprojects
{
repositories
{
jcenter
(
)
}
configurations
.
all
{
resolutionStrategy
.
force
'com.android.support:support-annotations:23.3.0'
}
}
apply
plugin:
'android-reporting'
Now if you run the mergeAndroidReports
task, everything will be collected into a single file.
> ./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.
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.
3.22.41.235