What’s new in Android Testing

github.com/googlesamples/android-testing

Developer Platforms Engineer @Google

+Stephan Linzner

@onlythoughtwork

The next generation of Android Testing Tools

Unit Test Support

AndroidJUnitRunner
Espresso

Espresso-Intents

Unit Test Support
a.k.a JVM Tests
a.k.a Local Tests

Before unit test support

Running tests on device/emulator was slow
Build, deploy, make an espresso, run tests

Stub implementations in android.jar

Error java.lang.RuntimeException: Stub!

Framework limitations

Final classes

Unit test support for the Android Gradle Plugin

com.android.tools.build:gradle:1.1+

New source set test/ for unit tests

Mockable android.jar

Mockito to stub dependencies into the Android framework

apply plugin: 'com.android.application'

android
{
...
testOptions {
unitTests
.returnDefaultValues = true // Caution!
}
}

dependencies
{
// Unit testing dependencies
testCompile
'junit:junit:4.12'
testCompile
'org.mockito:mockito-core:1.10.19'
}

app/build.gradle

Unit test sample

@RunWith(MockitoJUnitRunner.class)
@SmallTest

public class UnitTestSample {

private static final String FAKE_STRING = "HELLO WORLD";

@Mock
Context mMockContext;

@Test
public void readStringFromContext_LocalizedString() {
// Given a mocked Context injected into the object under test...
when(mMockContext.getString(R.string.hello_word)).thenReturn(FAKE_STRING);
ClassUnderTest myObjectUnderTest = new ClassUnderTest(mMockContext);

// ...when the string is returned from the object under test...
String result = myObjectUnderTest.getHelloWorldString();

// ...then the result should be the expected one.
assertThat
(result, is(FAKE_STRING));
}

}

Dependency

Injection

Use MockitoJUnitRunner for

easier initialization of @Mock fields.

Command-line

$ ./gradlew test

...

:app:mockableAndroidJar

...

:app:assembleDebugUnitTest

:app:testDebug

:app:test

BUILD SUCCESSFUL

Total time: 3.142 secs

Android Studio

Reports

app/build/reports/tests/debug/index.html

app/build/test-results/debug/TEST-com.example.android.testing.unittesting.BasicSample.EmailValidatorTest.xml

New in 1.1+,
XML Reports

Limitations

Tight coupling with Android

When Mockito is not enough

Stubbing static methods

TextUtils.*, Log.*, etc.

Workarounds

Tight coupling with Android

Revisit your design decisions
Run unit tests on device or emulator

Stubbing static methods

Wrapper

PowerMockito
unitTests.returnDefaultValues = true

Android Testing Support Library


a.k.a Instrumentation Tests

a.k.a Device or Emulator Tests

3

Instrumentation Tests

Run on Device or Emulator

Run With AndroidJUnitRunner
Located androidTest/ source set

Android SDK Manager

Open Sdk Manager
from Android Studio

Download latest

Support Repository

apply plugin: 'com.android.application'

android
{
...
defaultConfig
{

testInstrumentationRunner ‘android.support.test.runner.AndroidJUnitRunner’
}

}
dependencies
{
// AndroidJUnit Runner dependencies
androidTestCompile
'com.android.support.test:runner:0.2'
}

app/build.gradle

Command-line

$./gradlew connectedAndroidTest
...

:app:assembleDebug
...

:app:assembleDebugAndroidTest

:app:connectedAndroidTest

BUILD SUCCESSFUL

Total time: 59.152 secs

Android Studio

App Under Test

Android Test App

Reports

app/build/outputs/reports/androidTests/connected/index.html
app/build/outputs/androidTest-results/connected/TEST-Nexus-6-5.1-app-flavor.xml

New in 1.1+,
XML Reports

AndroidJUnitRunner

AndroidJUnitRunner

A new test runner for Android

JUnit3/JUnit4 Support
Instrumentation Registry
Test Filtering
Intent Monitoring/Stubbing

Activity/Application Lifecycle Monitoring

JUnit4

@RunWith(AndroidJUnit4.class)
@SmallTest
public class DroidconItalyTest {
Droidcon mDroidcon;

@Before
public void initDroidcon() {
mDroidcon
= Droidcons.get(Edition.ITALY);
mDroidcon
.init();
}

@Test
public void droidcon_IsAwesome_ReturnsTrue() {
assertThat
(mDroidcon.isAwesome(), is(true));
}

@After
public void releaseDroidcon() {
mDroidcon
.release();
}
}

Use @Before to

setup your test fixture

Annotate all tests with @Test

Use @After to

release any resource

JUnit4 test need to be

annotated with AndroidJUnit4.class

Instrumentation Registry

@Before
public void accessAllTheThings() {
mArgsBundle
= InstrumentationRegistry.getArguments();
mInstrumentation
= InstrumentationRegistry.getInstrumentation();
mTestAppContext
= InstrumentationRegistry.getContext();
mTargetContext
= InstrumentationRegistry.getTargetContext();
}

Test Filters

@SdkSuppress(minSdkVersion=15)
@Test
public void featureWithMinSdk15() {
...
}

@RequiresDevice
@Test
public void SomeDeviceSpecificFeature() {
...
}

Suppress test to run on certain
target Api levels

Filter tests that can only run on a (physical) device

JUnit4 Rules

apply plugin: 'com.android.application'

android
{
...
defaultConfig
{

testInstrumentationRunner ‘android.support.test.runner.AndroidJUnitRunner’
}

}
dependencies
{
// AndroidJUnit Runner dependencies
androidTestCompile 'com.android.support.test:runner:0.2'

androidTestCompile 'com.android.support.test:rules:0.2'

}

app/build.gradle

@Deprecated
public class ActivityInstrumentationTestCase2

After

Before

ActivityInstrumentationTestCase2 vs. ActivityTestRule

ActivityTestRule Sample

https://github.com/googlesamples/android-testing/tree/master/espresso/BasicSample

@RunWith(AndroidJUnit4.class)
@LargeTest
public class ChangeTextBehaviorTest {

...

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

@Test
public void changeText_sameActivity() {
// Type text and then press the button.
onView
(withId(R.id.editTextUserInput))
.perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
onView
(withId(R.id.changeTextBt)).perform(click());

// Check that the text was changed.
onView
(withId(R.id.textToBeChanged)).check(matches(withText(STRING_TO_BE_TYPED)));
}
}

Use @Rule annotation

Create an ActivityTestRule
for your Activity

ActivityTestRule

public class ActivityTestRule<T extends Activity> extends UiThreadTestRule {

public T getActivity() {}

public void launchActivity(Intent) {}

protected Intent getActivityIntent() {}

protected void beforeActivityLaunched() {}

protected void afterActivityFinished() {}

}

Lazy Launch of Activity
Custom Start Intent/Test

Access Activity instance

Override Activity Start Intent

ServiceTestRule Sample

@RunWith(AndroidJUnit4.class)
@MediumTest
public class MyServiceTest {

@Rule
public final ServiceTestRule mServiceRule = new ServiceTestRule();

@Test
public void testWithStartedService() {
mServiceRule
.startService(
new Intent(InstrumentationRegistry.getTargetContext(), MyService.class));
// test code
}

@Test
public void testWithBoundService() {
IBinder binder = mServiceRule.bindService(
new Intent(InstrumentationRegistry.getTargetContext(), MyService.class));
MyService service = ((MyService.LocalBinder) binder).getService();
assertTrue
("True wasn't returned", service.doSomethingToReturnTrue());
}
}

Use @Rule annotation

Create the
ServiceTestRule

Start Service under Test

Bind to Service under Test

Espresso

A new approach to UI Testing

An API from Developers for Developers

What would a user do?


Find a view

Perform an action
Check some state

onView(Matcher)

.perform(ViewAction)

.check(ViewAssertion);

Find, Perform, Check

apply plugin: 'com.android.application'

android
{
...
defaultConfig
{

testInstrumentationRunner ‘android.support.test.runner.AndroidJUnitRunner’
}

}
dependencies
{
androidTestCompile 'com.android.support.test:runner:0.2'

androidTestCompile 'com.android.support.test:rules:0.2'

// Espresso dependencies

androidTestCompile 'com.android.support.test.espresso:espresso-core:2.1'
}

app/build.gradle

Espresso BasicSample

https://github.com/googlesamples/android-testing/tree/master/espresso/BasicSample

@RunWith(AndroidJUnit4.class)
@LargeTest
public class ChangeTextBehaviorTest {
...
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(
MainActivity.class);

@Test
public void changeText_sameActivity() {
onView
(withId(R.id.editTextUserInput))
.perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
onView
(withId(R.id.changeTextBt)).perform(click());

onView
(withId(R.id.textToBeChanged)).check(matches(withText(STRING_TO_BE_TYPED)));
}
}

Type text and then press the button.

Check that text was changed

Find EditText view

Start Activity Under Test

Espresso APIs

onData() API for Adapter Views

Multi Window Support
Synchronization APIs

Espresso Contrib APIs

DrawerActions

RecyclerViewActions
[Time/Date]PickerActions

apply plugin: 'com.android.application'

android
{
...
defaultConfig
{

testInstrumentationRunner ‘android.support.test.runner.AndroidJUnitRunner’
}

}
dependencies
{
androidTestCompile 'com.android.support.test:runner:0.2'

androidTestCompile 'com.android.support.test:rules:0.2'

// Espresso dependencies

androidTestCompile 'com.android.support.test.espresso:espresso-core:2.1'

androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.1'
}

app/build.gradle

Espresso-Intents

Espresso-Intents is like Mockito but for Intents

Hermetic inter-app testing

Hermetic Testing

Intent

Activity Result

Process:
com.android.camera

?

Process:
your.package.name

Open Camera

intended(IntentMatcher);

Intent Validation

intending(IntentMatcher)

.respondWith(ActivityResult);

Intent Stubbing

apply plugin: 'com.android.application'

android
{
...
defaultConfig
{

testInstrumentationRunner ‘android.support.test.runner.AndroidJUnitRunner’
}

}
dependencies
{
androidTestCompile 'com.android.support.test:runner:0.2'

androidTestCompile 'com.android.support.test:rules:0.2'

// Espresso dependencies

androidTestCompile 'com.android.support.test.espresso:espresso-core:2.1'

androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.1'
}

app/build.gradle

IntentsBasicSample

https://github.com/googlesamples/android-testing/tree/master/espresso/IntentsBasicSample

@RunWith(AndroidJUnit4.class)
@LargeTest
public class DialerActivityTest {
...
@Rule
public IntentsTestRule<DialerActivity> mRule = new IntentsTestRule<>(DialerActivity.class);

@Test
public void typeNumber_ValidInput_InitiatesCall() {
intending
(not(isInternal())).respondWith(new ActivityResult(Activity.RESULT_OK, null));

onView(withId(R.id.edit_text_caller_number)).perform(typeText(VALID_PHONE_NUMBER),
closeSoftKeyboard
());
onView
(withId(R.id.button_call_number)).perform(click());


intended
(allOf(
hasAction
(Intent.ACTION_CALL)),
hasData
(INTENT_DATA_PHONE_NUMBER),
toPackage
(PACKAGE_ANDROID_DIALER)));
}
}

Type Number and press Call Button

Verify Intent was sent

Create IntentsTestRule

Stub all external
Intents

UI Automator

UI Automator 2.0

Black box testing

Inter-app behavior testing

Context Access

apply plugin: 'com.android.application'

android
{
...
defaultConfig
{

testInstrumentationRunner ‘android.support.test.runner.AndroidJUnitRunner’
}

}
dependencies
{
androidTestCompile 'com.android.support.test:runner:0.2'

// UiAutomator Dependencies

androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.0.0'


}

app/build.gradle

UIAutomator BasicSample

https://github.com/googlesamples/android-testing/tree/master/uiautomator/BasicSample

@Before
public void startMainActivityFromHomeScreen() {

mDevice
= UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());

mDevice
.pressHome();

final String launcherPackage = getLauncherPackageName();
assertThat
(launcherPackage, notNullValue());
mDevice
.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), LAUNCH_TIMEOUT);

Context context = InstrumentationRegistry.getContext();
final Intent intent = context.getPackageManager()
.getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);
intent
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
context
.startActivity(intent);

// Wait for the app to appear
mDevice
.wait(Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)), LAUNCH_TIMEOUT);
}

Initialize UiDevice

Launch Basic Sample

UIAutomator BasicSample

https://github.com/googlesamples/android-testing/tree/master/uiautomator/BasicSample

@Test
public void testChangeText_sameActivity() {

mDevice
.findObject(By.res(BASIC_SAMPLE_PACKAGE, "editTextUserInput"))
.setText(STRING_TO_BE_TYPED);
mDevice
.findObject(By.res(BASIC_SAMPLE_PACKAGE, "changeTextBt"))
.click();

UiObject2 changedText = mDevice
.wait(Until.findObject(By.res(BASIC_SAMPLE_PACKAGE, "textToBeChanged")),
500 /* wait 500ms */);
assertThat
(changedText.getText(), is(equalTo(STRING_TO_BE_TYPED)));
}

Type text and click Button

Verify text displayed
in UI

Contribute

Initialize your build environment

https://source.android.com/source/initializing.html

Install Repo

https://source.android.com/source/downloading.html

Checkout android-support-test branch

repo init -u https://android.googlesource.com/platform/manifest -g all -b android-support-test

Sync the source

repo sync -j24

Browse the source

cd frameworks/testing

Build and test

// Just build debug build type

./gradlew assembleDebug

// Run tests

./gradlew connectedCheck

Contribute to Android Testing Support Library

https://source.android.com/source/life-of-a-patch.html

Thank you

https://plus.google.com/+AndroidDevelopers/posts/jHXFkebKjEb
https://plus.google.com/+JoseAlcerreca/posts/3aU1J6EDGKd
https://github.com/googlesamples/android-testing

Q&A

#HappyTesting

What's new in Android Testing Droidcon Italy 2015 - Google Slides