Target Audience: Android developers
Android has historically been a tricky beast for writing tests. Things have changed, however, and testing Android apps has never been easier.
developers.android.com has some great documentation for getting started. With this post, we want to demonstrate testing in real world scenarios.
In most of our applications, we use Retrofit for network calls and the MVP pattern. From our experience, MVP helps us write code which is better, and inherently easier to test. For this demo, we have written a simple app that shows a login form and makes an API call.
Typically, there’s four categories of tests,
- Local Unit tests
- Instrumented Unit tests
- User interface tests
- Integration tests
Local Unit Testing
Use this for parts of your code that have no dependency on the Android framework, or depend on something small that can be mocked.
Methods as below are ideal candidates for unit testing
// Checks for valid email address
boolean isValidUsername(String username) {
Pattern pattern = Pattern.compile("\\A[^@]+@([^@\\.]+\\.)+[^@\\.]+\\z");
Matcher matcher = pattern.matcher(username);
return matcher.matches();
}
To setup, add the following to your gradle file
dependencies {
// Required -- JUnit 4 framework
testCompile 'junit:junit:4.12'
// Optional -- Mockito framework
testCompile 'org.mockito:mockito-core:1.10.19'
}
and then create a test file in src/test/your/package
package com.moldedbits.android.login;
// Imports...
@RunWith(JUnit4.class)
public class LoginPresenterUnitTests {
private LoginPresenter mPresenter;
@Before
public void setup() {
mPresenter = new LoginPresenter(null, null);
}
@Test
public void validationTest() {
assertThat(mPresenter.isValidUsername(""), is(false));
assertThat(mPresenter.isValidUsername("abc"), is(false));
assertThat(mPresenter.isValidUsername("a@b.com"), is(true));
}
}
To run an individual test, simply right click on the test and select run validationTest()
, or you can run all the tests in the class in a similar way.
That was simple, but what if we wanted to test how our business logic interacts with the UI. This is where the MVP pattern really shines. Since the only Android dependencies are in View, we can mock it out and unit test the Presenter.
Here’s how it looks
package com.moldedbits.android.login;
// Imports...
@RunWith(MockitoJUnitRunner.class)
public class LoginPresenterTest {
@Mock
LoginView mLoginView;
LoginPresenter mPresenter;
@Before
public void setup() {
mPresenter = new LoginPresenter(mLoginView, null);
}
@Test
public void loginInvalidUsername() {
mPresenter.login("abc", "abc");
verify(mLoginView, times(1)).setUsernameError("Invalid input");
}
@Test
public void loginEmptyUsername() {
mPresenter.login("", "abc");
verify(mLoginView, times(1)).setUsernameError("Required");
}
@Test
public void loginEmptyPassword() {
mPresenter.login("a@b.c", "");
verify(mLoginView, times(1)).setPasswordError("Required");
}
}
JUnit4 and MockitoJUnitRunner run the tests on your local JVM, and are very fast. Hence we prefer them wherever possible.
We can even take this further and mock retrofit
package com.moldedbits.android.login;
// Imports
@RunWith(MockitoJUnitRunner.class)
public class LoginPresenterMockApiTest {
@Mock
LoginView mLoginView;
@Mock
APIService mAPIService;
@Mock
Call<LoginResponse> mCall;
@Captor
ArgumentCaptor<Callback<LoginResponse>> mCallback;
private LoginPresenter mPresenter;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mPresenter = new LoginPresenter(mLoginView, mAPIService);
when(mAPIService.login(any(LoginRequest.class)))
.thenReturn(mCall);
}
@Test
public void loginSuccess() {
mPresenter.login("a@b.c", "abc");
verify(mCall).enqueue(mCallback.capture());
mCallback.getValue().onResponse(mCall, getMockSuccessResponse());
verify(mLoginView, times(1)).onLoginSuccess(any(User.class));
}
@Test
public void loginFailure() {
mPresenter.login("a@b.c", "abc");
verify(mCall).enqueue(mCallback.capture());
mCallback.getValue().onResponse(mCall, getMockFailureResponse());
verify(mLoginView, times(1)).onLoginFailure("Invalid credentials");
}
private Response<LoginResponse> getMockSuccessResponse() {
LoginResponse successResponse = new LoginResponse();
successResponse.setUser(getMockUser());
return Response.success(successResponse);
}
private Response<LoginResponse> getMockFailureResponse() {
String errorMessage = "{" +
" \"status\": \"failure\"," +
" \"error\": \"Invalid credentials\"" +
"}";
ResponseBody responseBody = ResponseBody.create(MediaType.parse("string/json"),
errorMessage.getBytes());
return Response.error(401, responseBody);
}
private User getMockUser() {
return new User(1, "Mock User");
}
}
Important thing to note here is the use of ArgumentCaptor. You can capture the arguments passed and call any method, pretty cool stuff!
Instrumented Unit tests
There are, however, times when you need the Android jars in your tests. Fortunately, it is pretty darn simple as well. All you need to do is update your test runner, and place your test in the folder src/androidTest/your/package
...
@RunWith(AndroidJUnit4.class)
public class LoginIntegrationTest {
...
}
You will also need to change the following in your build.gradle file
defaultConfig {
...
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
}
dependencies {
...
androidTestCompile 'com.android.support:support-annotations:25.0.0'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5'
// Optional -- UI testing with Espresso
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
}
Integration tests
Espresso makes it very easy to create an run integration tests for your code. Its just like writing a test script, you identify the view, perform an action, and optionally verify the result. We can create complex interactions, across multiple activities and fragments. Here is a simple example,
package com.moldedbits.android;
// Imports...
@RunWith(AndroidJUnit4.class)
@SmallTest
public class LoginIntegrationTest {
private String mUsername, mPassword;
@Rule
public ActivityTestRule<LoginActivity> mActivityRule = new ActivityTestRule<>(LoginActivity.class);
@Test
public void loginSuccess() {
initValidCredentials();
onView(withId(R.id.input_username))
.perform(typeText(mUsername), closeSoftKeyboard());
onView(withId(R.id.input_password))
.perform(typeText(mPassword), closeSoftKeyboard());
onView(withId(R.id.button_submit))
.perform(click());
IdlingResource resource = OkHttp3IdlingResource.create("OkHttp",
APIProvider.getClient());
Espresso.registerIdlingResources(resource);
onView(allOf(withId(android.support.design.R.id.snackbar_text),
withText("Logged in as test@moldedbits.com")))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));
Espresso.unregisterIdlingResources(resource);
}
@Test
public void loginFailure() {
initInvalidCredentials();
onView(withId(R.id.input_username))
.perform(typeText(mUsername), closeSoftKeyboard());
onView(withId(R.id.input_password))
.perform(typeText(mPassword), closeSoftKeyboard());
onView(withId(R.id.button_submit))
.perform(click());
IdlingResource resource = OkHttp3IdlingResource.create("OkHttp", APIProvider.getClient());
Espresso.registerIdlingResources(resource);
onView(allOf(withId(android.support.design.R.id.snackbar_text),
withText("Invalid credentials")))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));
Espresso.unregisterIdlingResources(resource);
}
private void initValidCredentials() {
mUsername = "test@moldedbits.com";
mPassword = "foobarfoo";
}
private void initInvalidCredentials() {
mUsername = "test@moldedbits.com";
mPassword = "wrong";
}
}
You will need to add the following dependency for the above example,
dependencies {
...
androidTestCompile 'com.jakewharton.espresso:okhttp3-idling-resource:1.0.0'
}
Conclusion
With JUnit4 and Espresso, writing and running automated tests for Android is now simple and efficient. In a future post, we will demonstrate how we integrate testing in our continuous integration system.
For this post, you can find the sample code on GitHub.
Happy coding!
The moldedbits Team