2 years later...
Android Engineer - Scalable Capital
An guide to testing on Android
opinionated
Architecture
- MV*
Unit tests
- As little as Powermock as possible
Instrumentation tests
- AndroidJUnitRunner
E2E
- Espresso test recorder
- Espresso-intents
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
- Fastest
- Most robust
public static void main(String [] args) {
HomeActivity activity = new HomeActivity();
activity.onCreate();
activity.onStart();
activity.onResume();
assertNotNull("Activity was null", activity);
}
You can't instantiate Activity*, the system does it for you
*Or service, content provider, broadcast receiver
public class HomeActivity extends Activity {
private HomeState state;
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
state = new HomeState();
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { state.buttonClicked(); }
});
}
}
public class HomeState {
public buttonClicked() {
//Decide what to do with the button click
}
}
Decouple the state of the UI from the UI
public class HomeState {
private HomeActivity activity;
public HomeState(HomeActivity activity) { this.activity = activity; }
public buttonClicked() {
activity.showData(new String {"Item"});
}
}
public class HomeActivity extends Activity {
private HomeState state;
private Button button;
protected void onCreate(Bundle savedInstanceState) {
//
state = new HomeState(this);
}
public void showData(String [] items) {
//Populate
}
}
public class HomePresenter {
private WeakReference<HomeView> view;
public HomePresenter(HomeView v) {
this.view = new WeakReference(v);
}
public buttonClicked() {
view.showData(new String[]{"Item"});
}
}
public interface HomeView {
void showData(String [] items);
}
public class HomeActivity extends Activity implements HomeView {
public void showData(String [] items) {
//Populate
}
}
public static void main(String [] args) {
TestHomeView view = new TestHomeView();
HomePresenter presenter = new HomePresenter(view);
presenter.buttonClicked();
assertEquals(1, view.getItems().size()); //Tests all the logic
}
public class TestHomeView implements HomeView {
private List<String> items = new ArrayList();
@Override
public void populateWithData(String[] items) {
this.items = new ArrayList(Arrays.asList(items));
}
}
@RunWith(MockitoJUnitRunner.class)
public class HomePresenterUnitTest {
@Mock TestHomeView view;
@Test
public void viewShowsAllItems() throws Exception {
HomePresenter presenter = new HomePresenter(view);
presenter.buttonClicked();
assertEquals(1, view.getItems().size());
}
}
Mockito 2.1
- Mocking final classes
Mockito 2.3
- Strict Stubbing
Mockito 2.6
- Mockito 2 inside instrumentation tests
@RunWith(MockitoJUnitRunner.class)
public class PaymentPresenterUnitTest {
@Mock AuthProvider authProvider;
@Mock PaymentView view;
@Mock RequestQueue requestQueue;
@Test
public void testInstructPaymentSuccessful() {
doReturn(true).when(authProvider).isAuthenticated();
presenter.onDepositRequest(portfolio, 1000);
presenter.instructPayment();
verify(view).showLoading();
verify(requestQueue, mode).enqueueRequest(any(Request.class), any());
presenter.getPaymentHandler().onErrorResponse();
verify(view).hideLoading();
ArgumentCaptor<Instruction> captor = ArgumentCaptor.forClass(Instruction.class);
verify(view).showInstructionFailed(captor.capture());
assertThat(captor.getValue().getType(), is(Type.DEPOSIT));
assertThat(captor.getValue().getAmount(), is(1000));
}
}
Please note that PowerMock is mainly intended for people with expert knowledge in unit testing. Putting it in the hands of junior developers may cause more harm than good.
From PowerMock's github page [5]
Please note that PowerMock is mainly intended for people with expert knowledge in unit testing. Putting it in the hands of
juniordevelopers may cause more harm than good.
From PowerMock's github page [5]
busy
Allows mocking:
- Static classes (Uri, TextUtils, Patterns, etc.)
- private methods
Uses black magic (bytecode manipulation)
KISS
For 95% of cases, PowerMock is not needed
Keep it simple, stupid!
aircraft engineer Kelly Johnson, US Navy, 1960
Breaks test coverage
public class Uri {
private String url;
private Uri(String url) {
this.url = url;
}
@Override
public String toString() {
return url;
}
public static Uri parse(String url) {
return new Uri(url);
}
//...
}
public class SystemClockInstance {
public long elapsedRealtime() {
return SystemClock.elapsedRealtime();
}
}
public class AuthProvider {
public AuthProvider(@NonNull SystemClockInstance clockInstance) {
clockInstance = clockInstance;
}
void setToken(Token token) {
this.token = token.getAccess_token();
this.authTokenExpireMs = clockInstance.elapsedRealtime() + token.getExpiry();
}
}
public class AuthProviderUnitTest {
@Mock
SystemClockInstance clockInstance;
}
@RunWith(PowerMockRunner.class)
@PrepareOnlyThisForTest({Realm.class})
public class DbUnitTest {
@Mock
Realm realm;
@Before
public void setupMock() {
PowerMockito.spy(Realm.class);
}
@Test
public void testManagedCopyManagedObject() throws Exception {
PowerMockito.doReturn(realm).when(Realm.class, "getDefaultInstance");
Db db = new Db();
RealmObject managed = mock(RealmObject.class);
doReturn(true).when(managed).isManaged();
assertThat(db.managedCopy(managed), is(managed));
}
}
*
90% of bugs have nothing to do with fragmentation
Stephan Linzner, Droidcon Italy 2014
- No shared state
- Isolated crashes
- Isolated debugging
@Test
public void testComputeKeyConsistent() {
final int KEY_LEN_BITS = 128;
final String pin = "1234";
final byte[] salt = "hello salt".getBytes();
SecretKey secretKey1 = Secure.computeKey(pin, salt, KEY_LEN_BITS);
SecretKey secretKey2 = Secure.computeKey(pin, salt, KEY_LEN_BITS);
assertThat(KEY_LEN_BITS / 8, equalTo(secretKey1.getEncoded().length));
assertThat(secretKey1.getEncoded(), equalTo(secretKey2.getEncoded()));
}
SecretKey secretKey = computeKey(pin, iv, KEY_LENGTH_BITS_AES_CBC_PKCS5);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(opmode,
new SecretKeySpec(secretKey.getEncoded(), "AES"),
new IvParameterSpec(iv)
);
SecretKey secretKey = computeKey(pin, iv, KEY_LENGTH_BITS_AES_CBC_PKCS5);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(opmode,
secretKey,
new IvParameterSpec(iv)
);
You are not testing against the SDK, so what's the point?
- SDK Support is late (6 months to release API 24)
- If it is related to the system, use the system
Unit tests cover the rest
@RunWith(AndroidJUnit4.class)
public class MainActivityTest5 {
@Rule
public ActivityTestRule<MainActivity> mActivityTestRule = new ActivityTestRule<>(MainActivity.class);
@Test
public void mainActivityTest5() {
ViewInteraction appCompatButton = onView(
allOf(withId(R.id.button1), withText("launch ShowDialogActivity"),
withParent(allOf(withId(R.id.activity_main),
withParent(withId(android.R.id.content)))),
isDisplayed()));
appCompatButton.perform(click());
ViewInteraction appCompatButton2 = onView(
allOf(withId(R.id.button), withText("show dialog"), isDisplayed()));
appCompatButton2.perform(click());
ViewInteraction appCompatButton3 = onView(
allOf(withId(android.R.id.button2), withText("Cancel"),
withParent(allOf(withClassName(is("com.android.internal.widget.ButtonBarLayout")),
withParent(withClassName(is("android.widget.LinearLayout"))))),
isDisplayed()));
appCompatButton3.perform(click());
ViewInteraction appCompatButton4 = onView(
allOf(withId(R.id.button), withText("show dialog"), isDisplayed()));
appCompatButton4.perform(click());
ViewInteraction appCompatButton5 = onView(
allOf(withId(android.R.id.button1), withText("OK"),
withParent(allOf(withClassName(is("com.android.internal.widget.ButtonBarLayout")),
withParent(withClassName(is("android.widget.LinearLayout"))))),
isDisplayed()));
appCompatButton5.perform(click());
}
}
@Test
public void testAlertDialogAppear() throws Throwable {
rule.runOnUiThread(() -> rule.getActivity().showUpdateMandatory());
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
onView(withText(R.string.UpdateNecessary))
.check(matches(isDisplayed()));
onView(withText(R.string.Update))
.perform(click());
intended(hasData(new CustomMatcher<Uri>("Matches if app market link") {
public boolean matches(Object item) {
if (!(item instanceof Uri)) {
return false;
}
String url = item.toString();
return (url.contains("market://")
|| url.contains("play.google.com/store"))
&& url.contains("capital.scalable.droid");
}
}));
}
Run instrumentation tests against minified build
android {
//...
testBuildType 'instrumentation'
buildTypes {
instrumentation.initWith(buildTypes.release)
instrumentation {
applicationIdSuffix ".instrumentation"
testProguardFiles 'proguard-test-rules.pro'
}
}
}
testProguardFiles should be copied from [5]
Shared test utils folder
//Sharing utils between test and androidTest
sourceSets {
test {
java.srcDir 'src/sharedTest/java'
}
androidTest {
java.srcDir 'src/sharedTest/java'
}
}
Start using MockitoJUnitRunner.StrictStubs
Will become default in Mockito 3.0
testOptions {
unitTests.returnDefaultValues = true
}
Return defaults = true
Error: "Method ... not mocked"
Debug instrumentation tests from cmd line
Use Robots [6]
class PaymentViewRobot {
PaymentViewRobot failedInstructionShown() {
instructionCaptor = ArgumentCaptor.forClass(PaymentInstruction.class);
verify(view).showInstructionFailed(instructionCaptor.capture());
return this;
}
PaymentViewRobot instructionTypeMatches(
Matcher<PaymentInstruction.Type> matcher) {
assertThat(instructionCaptor.getValue().getType(), matcher);
return this;
}
}
@Test
public void testInstructPaymentFailed() {
doReturn(true).when(authProvider).isAuthenticated();
presenter.onDepositRequest(portfolio, transactionAmount);
presenter.instructPayment();
presenter.getPaymentHandler().onErrorResponse(new VolleyError());
viewRobot.failedInstructionShown()
.instructionTypeMatches(is(PaymentInstruction.Type.DEPOSIT));
}
Avoid android.* imports in business logic
package android.os;
/**
* Run AsyncTask synchronously.
*/
public abstract class AsyncTask<Params, Progress, Result> {
protected abstract Result doInBackground(Params... params);
protected void onPostExecute(Result result) {}
protected void onProgressUpdate(Progress... values) {}
public AsyncTask<Params, Progress, Result> execute(Params... params) {
Result result = doInBackground(params);
onPostExecute(result);
return this;
}
}
[1]: https://www.youtube.com/watch?v=pK7W5npkhho
[2]: https://medium.com/android-testing-daily/the-3-tiers-of-the-android-test-pyramid-c1211b359acd
[X]: https://github.com/powermock/powermock
[4]: http://engineer.recruit-lifestyle.co.jp/techblog/2016-06-20-android-try-espressotestrecorder/
[5]: https://github.com/googlesamples/android-testing-templates/blob/master/AndroidTestingBlueprint/app/proguard-test-rules.pro
[6]: http://jakewharton.com/testing-robots/