Complete reference for Android Testing with examples.
- Introduction
- Local Tests
- Android
- References
- Testing forces you to think in a different way and implicitly makes your code cleaner in the process.
- You feel more confident about your code if it has tests.
- Shiny green status bars and cool reports detailing how much of your code is covered are both consequences of writing tests.
- Regression testing is made a lot easier, as automated tests would pick up the bugs first.
A unit test generally exercises the functionality of the smallest possible unit of code (which could be a method, class, or component) in a repeatable way.
Tools that are used to do this testing:
- JUnit – normal test assertions.
- Mockito – mocking out other classes that are not under test.
- PowerMock – mocking out static classes such as Android Environment class etc.
A UI Test or Instrumentation Test mocks typical user interactions with your app. Clicking on buttons, typing in text are some of the things UI Tests can complete.
- Espresso – Used for testing within your app, selecting items, making sure something is visible.
- UIAutomator – Used for testing interaction between different apps.
There are other tools that are available for this kind of testing such as Robotium, Appium, Calabash, Robolectric.
public class Calculator {
public int add(int op1, int op2) {
return op1 + op2;
}
public int diff(int op1, int op2) {
return op1 - op2;
}
public double div(int op1, int op2) {
// if (op2 == 0) return 0;
return op1 / op2;
}
}
public class CalculatorTest {
private Calculator calculator;
@Before
public void setup() {
calculator = new Calculator();
System.out.println("Ready for testing!");
}
@After
public void cleanup() {
System.out.println("Done with unit test!");
}
@BeforeClass
public static void testClassSetup() {
System.out.println("Getting test class ready");
}
@AfterClass
public static void testClassCleanup() {
System.out.println("Done with tests");
}
@Test
public void testAdd() {
calculator = new Calculator();
int total = calculator.add(4, 5);
assertEquals("Calculator is not adding correctly", 9, total);
}
@Test
public void testDiff() {
calculator = new Calculator();
int total = calculator.diff(9, 2);
assertEquals("Calculator is not subtracting correctly", 7, total);
}
@Test
public void testDiv() {
calculator = new Calculator();
double total = calculator.div(9, 3);
assertEquals("Calculator is not dividing correctly", 3.0, total, 0.0);
}
}
@Ignore
@Test(expected = java.lang.ArithmeticException.class)
public void testDivWithZeroDivisor() {
calculator = new Calculator();
double total = calculator.div(9, 0);
assertEquals("Calculator is not handling division by zero correctly", 0.0, total, 0.0);
}
public class HamcrestTest {
@Test
public void testWithAsserts() {
List<String> list = generateStingList();
assertTrue(list.contains("android"));
assertTrue(list.contains("context"));
assertTrue(list.size() > 4);
assertTrue(list.size() < 13);
}
@Test
public void testWithBigAssert() {
List<String> list = generateStingList();
assertTrue(list.contains("android") && list.contains("context") && list.size() > 3 && list.size() < 12);
}
@Test
public void testWithHamcrest() {
List<String> list = generateStingList();
assertThat(list, (hasItems("android", "context")));
assertThat(list, allOf(hasSize(greaterThan(3)), hasSize(lessThan(12))));
}
@Test
public void testFailureWithAsserts() {
List<String> list = generateStingList();
assertTrue(list.contains("android"));
assertTrue(list.contains("service"));
assertTrue(list.size() > 3);
assertTrue(list.size() < 12);
}
@Test
public void testFailureWithHamcrest() {
List<String> list = generateStingList();
assertThat(list, (hasItems("android", "service")));
assertThat(list, allOf(hasSize(greaterThan(3)), hasSize(lessThan(12))));
}
@Test
public void testTypeSafety() {
// assertThat("123", equalTo(123));
// assertThat(123, equalTo("123"));
}
private List<String> generateStingList() {
String[] sentence = {"android", "context", "service", "manifest", "layout", "resource", "broadcast", "receiver", "gradle"};
return Arrays.asList(sentence);
}
}
public class CalculatorWithTestName {
@Rule
public TestName name = new TestName();
@Test
public void testAdd() {
Calculator calculator = new Calculator();
int total = calculator.add(4, 5);
assertEquals(name.getMethodName() + " adding incorrectly", 9, total);
}
@Test
public void testDiff() {
Calculator calculator = new Calculator();
int total = calculator.diff(12, 7);
assertEquals(name.getMethodName() + " subtracting incorrectly", 5, total);
}
}
RESTMock is a library working on top of Square's okhttp/MockWebServer. It allows you to specify Hamcrest matchers to match HTTP requests and specify what response to return. It is as easy as:
RESTMockServer.whenGET(pathContains("users/defunkt"))
.thenReturnFile(200, "users/defunkt.json");
It's good to start server before the tested application starts, there are few methods:
To make it simple you can just use the predefined RESTMockTestRunner
in your UI tests. It extends AndroidJUnitRunner
:
defaultConfig {
...
testInstrumentationRunner 'io.appflate.restmock.android.RESTMockTestRunner'
}
If you have your custom test runner and you can't extend RESTMockTestRunner
, you can always just call the RESTMockServerStarter
. Actually RESTMockTestRunner
is doing exactly the same thing:
public class MyAppTestRunner extends AndroidJUnitRunner {
...
@Override
public void onCreate(Bundle arguments) {
super.onCreate(arguments);
RESTMockServerStarter.startSync(new AndroidAssetsFileParser(getContext()),new AndroidLogger());
...
}
...
}
####Step 2: Specify Mocks
By default, the RESTMockTestRunner
uses AndroidAssetsFileParser
as a mocks file parser, which reads the files from the assets folder. To make them visible for the RESTMock you have to put them in the correct folder in your project, for example:
.../src/androidTest/assets/users/defunkt.json
This can be accessed like this:
RESTMockServer.whenGET(pathContains("users/defunkt"))
.thenReturnFile(200, "users/defunkt.json");
If the response You wish to return is simple, you can just specify a string:
RESTMockServer.whenGET(pathContains("users/defunkt"))
.thenReturnString(200, "{}");
If you wish to have a greater control over the response, you can pass the MockResponse
RESTMockServer.whenGET(pathContains("users/defunkt")).thenReturn(new MockResponse().setBody("").setResponseCode(401).addHeader("Header","Value"));
You can either use some of the predefined matchers from RequestMatchers
util class, or create your own. remember to extend from RequestMatcher
The most important step, in order for your app to communicate with the testServer, you have to specify it as an endpoint for all your API calls. For that, you can use the RESTMockServer.getUrl()
. If you use Retrofit, it is as easy as:
RestAdapter adapter = new RestAdapter.Builder()
.baseUrl(RESTMockServer.getUrl())
...
.build();
It is possible to verify which requests were called and how many times thanks to RequestsVerifier
. All you have to do is call one of these:
//cheks if the GET request was invoked exactly 2 times
RequestsVerifier.verifyGET(pathEndsWith("users")).exactly(2);
//cheks if the GET request was invoked at least 3 times
RequestsVerifier.verifyGET(pathEndsWith("users")).atLeast(3);
//cheks if the GET request was invoked exactly 1 time
RequestsVerifier.verifyGET(pathEndsWith("users")).invoked();
//cheks if the GET request was never invoked
RequestsVerifier.verifyGET(pathEndsWith("users")).never();
RESTMock supports logging events. You just have to provide the RESTMock with the implementation of RESTMockLogger
. For Android there is an AndroidLogger
implemented already. All you have to do is use the RESTMockTestRunner
or call
RESTMockServerStarter.startSync(new AndroidAssetsFileParser(getContext()),new AndroidLogger());
or
RESTMockServer.enableLogging(RESTMockLogger)
RESTMockServer.disableLogging()
public class MainActivityTestRule<A extends Activity> extends ActivityTestRule<A> {
public MainActivityTestRule(Class<A> activityClass) {
super(activityClass);
}
@Override
protected Intent getActivityIntent() {
Log.e("MainActivityTestRule", "Prepare the activity's intent");
return super.getActivityIntent();
}
@Override
protected void beforeActivityLaunched() {
Log.e("MainActivityTestRule", "Execute before the activity is launched");
super.beforeActivityLaunched();
}
@Override
protected void afterActivityLaunched() {
Log.e("MainActivityTestRule", "Execute after the activity has been launched");
super.afterActivityLaunched();
}
@Override
protected void afterActivityFinished() {
Log.e("MainActivityTestRule", "Cleanup after it has finished");
super.afterActivityFinished();
}
@Override
public A launchActivity(Intent startIntent) {
Log.e("MainActivityTestRule", "Launching the activity");
return super.launchActivity(startIntent);
}
}
public class SampleServiceTestRule extends ServiceTestRule {
@Override
public void startService(Intent intent) throws TimeoutException {
Log.e("SampleServiceTestRule", "start the service");
super.startService(intent);
}
@Override
public IBinder bindService(Intent intent) throws TimeoutException {
Log.e("SampleServiceTestRule", "binding the service");
return super.bindService(intent);
}
@Override
protected void beforeService() {
Log.e("SampleServiceTestRule", "work before the service starts");
super.beforeService();
}
@Override
protected void afterService() {
Log.e("SampleServiceTestRule", "work after the service has started");
super.afterService();
}
}
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
@Rule
public MainActivityTestRule<MainActivity> mainActivityActivityTestRule = new MainActivityTestRule<MainActivity>(MainActivity.class);
@Test
public void testUI() {
Activity activity = mainActivityActivityTestRule.getActivity();
assertNotNull(activity.findViewById(R.id.text_hello));
TextView helloView = (TextView) activity.findViewById(R.id.text_hello);
assertTrue(helloView.isShown());
assertEquals("Hello World!", helloView.getText());
assertEquals(InstrumentationRegistry.getTargetContext().getString(R.string.hello_world), helloView.getText());
assertNull(activity.findViewById(android.R.id.button1));
}
}
@RunWith(AndroidJUnit4.class)
public class SampleServiceTest {
@Rule
public SampleServiceTestRule myServiceRule = new SampleServiceTestRule();
@Test
public void testService() throws TimeoutException {
myServiceRule.startService(new Intent(InstrumentationRegistry.getTargetContext(), SampleService.class));
}
@Test
public void testBoundService() throws TimeoutException {
IBinder binder = myServiceRule.bindService(
new Intent(InstrumentationRegistry.getTargetContext(), SampleService.class));
SampleService service = ((SampleService.LocalBinder) binder).getService();
// Do work with the service
assertNotNull("Bound service is null", service);
}
}
@Test
@RequiresDevice
public void testRequiresDevice() {
Log.d("Test Filters", "This test requires a device");
Activity activity = activityTestRule.getActivity();
assertNotNull("MainActivity is not available", activity);
}
@Test
@SdkSuppress(minSdkVersion = 30)
public void testMinSdkVersion() {
Log.d("Test Filters", "Checking for min sdk >= 30");
Activity activity = activityTestRule.getActivity();
assertNotNull("MainActivity is not available", activity);
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
public void testMinBuild() {
Log.d("Test Filters", "Checking for min build > Lollipop");
Activity activity = activityTestRule.getActivity();
assertNotNull("MainActivity is not available", activity);
}
@Test
@SmallTest
public void testSmallTest() {
Log.d("Test Filters", "this is a small test");
Activity activity = activityTestRule.getActivity();
assertNotNull("MainActivity is not available", activity);
}
@Test
@LargeTest
public void testLargeTest() {
Log.d("Test Filters", "This is a large test");
Activity activity = activityTestRule.getActivity();
assertNotNull("MainActivity is not available", activity);
}
@Test
public void testEspresso() {
ViewInteraction interaction =
onView(allOf(withId(R.id.editText),
withText("this is a test"),
hasFocus()));
interaction.perform(replaceText("how about some new text"));
ViewInteraction interaction2 =
onView(allOf(withId(R.id.editText),
withText("how about some new text")));
interaction2.check(matches(hasFocus()));
}
@Test
public void testEspressoSimplified() {
onView(allOf(withId(R.id.editText),
withText("this is a test"),
hasFocus())).perform(replaceText("how about some new text"));
onView(allOf(withId(R.id.editText),
withText("how about some new text"))).check(matches(hasFocus()));
}
MainActivityRoboelectricTest.java
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class MainActivityRoboelectricTest {
private MainActivity activity;
@Before
public void setup() {
activity = Robolectric.setupActivity(MainActivity.class);
}
@Test
public void clickButton() {
Button button = (Button) activity.findViewById(R.id.button);
assertNotNull("test button could not be found", button);
assertTrue("button does not contain text 'Click Me!'", "Click Me".equals(button.getText()));
}
}
public class MainActivityRobotiumTest {
private Solo solo;
@Rule
public MainActivityRobotiumTestRule<MainActivity> mActivityRule = new MainActivityRobotiumTestRule<>(MainActivity.class);
public void setUp() {
solo = new Solo(mActivityRule.getInstrumentation(), mActivityRule.getActivity());
}
public void tearDown() {
solo.finishOpenedActivities();
}
@Test
public void testPushClickMe() {
solo.waitForActivity(MainActivity.class);
solo.assertCurrentActivity("MainActivity is not displayed", MainActivity.class);
assertTrue("This is a test in EditText is not displayed",
solo.searchText("this is a test"));
solo.clickOnButton("Click Me");
assertTrue("You clicked me text is not displayed in the EditText",
solo.searchText("you clicked me!"));
}
}
MainActivityRobotiumTestRule.java
@Beta
public class MainActivityRobotiumTestRule<T extends Activity> extends UiThreadTestRule {
private static final String TAG = "InstrumentationRule";
private final Class<T> mActivityClass;
public Instrumentation getInstrumentation() {
return mInstrumentation;
}
private Instrumentation mInstrumentation;
private boolean mInitialTouchMode = false;
private boolean mLaunchActivity = false;
private T mActivity;
/**
* Similar to {@link #MainActivityRobotiumTestRule(Class, boolean, boolean)} but with "touch mode" disabled.
*
* @param activityClass The activity under test. This must be a class in the instrumentation
* targetPackage specified in the AndroidManifest.xml
* @see MainActivityRobotiumTestRule#MainActivityRobotiumTestRule(Class, boolean, boolean)
*/
public MainActivityRobotiumTestRule(Class<T> activityClass) {
this(activityClass, false);
}
/**
* Similar to {@link #MainActivityRobotiumTestRule(Class, boolean, boolean)} but defaults to launch the
* activity under test once per
* <a href="http://junit.org/javadoc/latest/org/junit/Test.html"><code>Test</code></a> method.
* It is launched before the first
* <a href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before</code></a>
* method, and terminated after the last
* <a href="http://junit.sourceforge.net/javadoc/org/junit/After.html"><code>After</code></a>
* method.
*
* @param activityClass The activity under test. This must be a class in the instrumentation
* targetPackage specified in the AndroidManifest.xml
* @param initialTouchMode true if the Activity should be placed into "touch mode" when started
* @see MainActivityRobotiumTestRule#MainActivityRobotiumTestRule(Class, boolean, boolean)
*/
public MainActivityRobotiumTestRule(Class<T> activityClass, boolean initialTouchMode) {
this(activityClass, initialTouchMode, true);
}
/**
* Creates an {@link MainActivityRobotiumTestRule} for the Activity under test.
*
* @param activityClass The activity under test. This must be a class in the instrumentation
* targetPackage specified in the AndroidManifest.xml
* @param initialTouchMode true if the Activity should be placed into "touch mode" when started
* @param launchActivity true if the Activity should be launched once per
* <a href="http://junit.org/javadoc/latest/org/junit/Test.html">
* <code>Test</code></a> method. It will be launched before the first
* <a href="http://junit.sourceforge.net/javadoc/org/junit/Before.html">
* <code>Before</code></a> method, and terminated after the last
* <a href="http://junit.sourceforge.net/javadoc/org/junit/After.html">
* <code>After</code></a> method.
*/
public MainActivityRobotiumTestRule(Class<T> activityClass, boolean initialTouchMode,
boolean launchActivity) {
mActivityClass = activityClass;
mInitialTouchMode = initialTouchMode;
mLaunchActivity = launchActivity;
mInstrumentation = InstrumentationRegistry.getInstrumentation();
}
/**
* Override this method to set up Intent as if supplied to
* {@link android.content.Context#startActivity}.
* <p>
* The default Intent (if this method returns null or is not overwritten) is:
* action = {@link Intent#ACTION_MAIN}
* flags = {@link Intent#FLAG_ACTIVITY_NEW_TASK}
* All other intent fields are null or empty.
*
* @return The Intent as if supplied to {@link android.content.Context#startActivity}.
*/
protected Intent getActivityIntent() {
return new Intent(Intent.ACTION_MAIN);
}
/**
* Override this method to execute any code that should run before your {@link Activity} is
* created and launched.
* This method is called before each test method, including any method annotated with
* <a href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before</code></a>.
*/
protected void beforeActivityLaunched() {
// empty by default
}
/**
* Override this method to execute any code that should run after your {@link Activity} is
* launched, but before any test code is run including any method annotated with
* <a href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before</code></a>.
* <p>
* Prefer
* <a href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before</code></a>
* over this method. This method should usually not be overwritten directly in tests and only be
* used by subclasses of MainActivityRobotiumTestRule to get notified when the activity is created and
* visible but test runs.
*/
protected void afterActivityLaunched() {
// empty by default
}
/**
* Override this method to execute any code that should run after your {@link Activity} is
* finished.
* This method is called after each test method, including any method annotated with
* <a href="http://junit.sourceforge.net/javadoc/org/junit/After.html"><code>After</code></a>.
*/
protected void afterActivityFinished() {
// empty by default
}
/**
* @return The activity under test.
*/
public T getActivity() {
if (mActivity == null) {
Log.w(TAG, "Activity wasn't created yet");
}
return mActivity;
}
@Override
public Statement apply(final Statement base, Description description) {
return new ActivityStatement(super.apply(base, description));
}
/**
* Launches the Activity under test.
* <p>
* Don't call this method directly, unless you explicitly requested not to launch the Activity
* manually using the launchActivity flag in
* {@link MainActivityRobotiumTestRule#MainActivityRobotiumTestRule(Class, boolean, boolean)}.
* <p>
* Usage:
* <pre>
* @Test
* public void customIntentToStartActivity() {
* Intent intent = new Intent(Intent.ACTION_PICK);
* mActivity = mActivityRule.launchActivity(intent);
* }
* </pre>
* @param startIntent The Intent that will be used to start the Activity under test. If
* {@code startIntent} is null, the Intent returned by
* {@link MainActivityRobotiumTestRule#getActivityIntent()} is used.
* @return the Activity launched by this rule.
* @see MainActivityRobotiumTestRule#getActivityIntent()
*/
public T launchActivity(@Nullable Intent startIntent) {
// set initial touch mode
mInstrumentation.setInTouchMode(mInitialTouchMode);
final String targetPackage = mInstrumentation.getTargetContext().getPackageName();
// inject custom intent, if provided
if (null == startIntent) {
startIntent = getActivityIntent();
if (null == startIntent) {
Log.w(TAG, "getActivityIntent() returned null using default: " +
"Intent(Intent.ACTION_MAIN)");
startIntent = new Intent(Intent.ACTION_MAIN);
}
}
startIntent.setClassName(targetPackage, mActivityClass.getName());
startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Log.d(TAG, String.format("Launching activity %s",
mActivityClass.getName()));
beforeActivityLaunched();
// The following cast is correct because the activity we're creating is of the same type as
// the one passed in
mActivity = mActivityClass.cast(mInstrumentation.startActivitySync(startIntent));
mInstrumentation.waitForIdleSync();
afterActivityLaunched();
return mActivity;
}
// Visible for testing
void setInstrumentation(Instrumentation instrumentation) {
mInstrumentation = checkNotNull(instrumentation, "instrumentation cannot be null!");
}
void finishActivity() {
if (mActivity != null) {
mActivity.finish();
mActivity = null;
}
}
/**
* <a href="http://junit.org/apidocs/org/junit/runners/model/Statement.html">
* <code>Statement</code></a> that finishes the activity after the test was executed
*/
private class ActivityStatement extends Statement {
private final Statement mBase;
public ActivityStatement(Statement base) {
mBase = base;
}
@Override
public void evaluate() throws Throwable {
try {
if (mLaunchActivity) {
mActivity = launchActivity(getActivityIntent());
}
mBase.evaluate();
} finally {
finishActivity();
afterActivityFinished();
}
}
}
}
@Test
public void testPressBackButton() {
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).pressBack();
}
@Test
@Ignore
public void testUiDevice() throws RemoteException {
UiDevice device = UiDevice.getInstance(
InstrumentationRegistry.getInstrumentation());
if (device.isScreenOn()) {
device.setOrientationLeft();
device.openNotification();
}
}
@Test
public void testUiAutomatorAPI() throws UiObjectNotFoundException, InterruptedException {
UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
UiSelector editTextSelector = new UiSelector().className("android.widget.EditText").text("this is a test").focusable(true);
UiObject editTextWidget = device.findObject(editTextSelector);
editTextWidget.setText("this is new text");
Thread.sleep(2000);
UiSelector buttonSelector = new UiSelector().className("android.widget.Button").text("Click Me").clickable(true);
UiObject buttonWidget = device.findObject(buttonSelector);
buttonWidget.click();
Thread.sleep(2000);
}
# Imports the monkeyrunner modules
from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice, MonkeyImage
# Alert the user a MonkeyRunner script is about to execute
MonkeyRunner.alert("Monkeyrunner about to execute","Monkeyrunner","OK")
# Connects to the current emulator
emulator= MonkeyRunner.waitForConnection()
# Install the Android app package and test package
emulator.installPackage('./app/build/outputs/apk/app-debug-unaligned.apk')
emulator.installPackage('./app/build/outputs/apk/app-debug-androidTest-unaligned.apk')
# sets a variable with the package's internal name
package = 'in.ravidsrk.sample'
# sets a variable with the name of an Activity in the package
activity = 'in.ravidsrk.sample.MainActivity'
# sets the name of the component to start
runComponent = package + '/' + activity
# Runs the component
emulator.startActivity(runComponent)
# wait for the screen to fully come up
MonkeyRunner.sleep(2.0)
# Takes a screenshot
snapshot = emulator.takeSnapshot()
# Writes the screenshot to a file
snapshot.writeToFile('mainactivity.png','png')
# Alert the user a testing is about to be run by MonkeyRunner
MonkeyRunner.alert("Instrumented test about to execute","Monkeyrunner","OK")
#kick off the instrumented test
emulator.shell('am instrument -w in.ravidsrk.sample.test/android.support.test.runner.AndroidJUnitRunner')
# return to the emulator home screen
emulator.press('KEYCODE_HOME','DOWN_AND_UP')
- https://github.com/googlesamples/android-testing
- https://riggaroo.co.za/introduction-automated-android-testing/
- http://robolectric.org
- https://github.com/robolectric/robolectric
- https://www.bignerdranch.com/blog/triumph-android-studio-1-2-sneaks-in-full-testing-support
- https://github.com/mutexkid/android-studio-robolectric-example
- http://blog.nikhaldimann.com/2013/10/10/robolectric-2-2-some-pages-from-the-missing-manual
- https://corner.squareup.com/2013/04/the-resurrection-of-testing-for-android.html
- http://simpleprogrammer.com/2010/07/27/the-best-way-to-unit-test-in-android/
- https://youtu.be/f7ihSQ44WO0?t=15m11s
- https://code.google.com/p/android-test-kit
- https://developer.android.com/training/testing/ui-testing/espresso-testing.html
- https://github.com/vgrec/EspressoExamples
- https://github.com/designatednerd/Wino
- http://chiuki.github.io/advanced-android-espresso/#/
- http://www.vogella.com/tutorials/AndroidTestingEspresso/article.html