How to Do TDD in Android — Part 3: Mocking & Integration Tests
Continuing the TDD in Android series. In this installment we cover integration tests and how to test interactions between the business layer and the UI.
In Part 2 we implemented unit tests covering all the business logic. Now we’ll verify that the application works correctly when those layers interact.
Building the Login UI
First, create a basic login screen with a username field, password field, and login button. Create LoginActivity with this layout:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_login"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/dimen_40dp"
android:paddingRight="@dimen/dimen_40dp">
<EditText
android:id="@+id/txt_user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:hint="@string/user_name"
android:inputType="text" />
<EditText
android:id="@+id/txt_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_user_name"
android:layout_marginTop="@dimen/dimen_10dp"
android:hint="@string/password"
android:inputType="textPassword" />
<Button
android:id="@+id/btn_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_password"
android:layout_centerHorizontal="true"
android:text="@string/login" />
</RelativeLayout>
Following MVP, create a LoginView interface that connects the Activity to the Presenter:
public interface LoginView {
void showErrorMessageForUserPassword();
void showErrorMessageForMaxLoginAttempt();
void showLoginSuccessMessage();
}
LoginActivity implements LoginView:
public class LoginActivity extends AppCompatActivity implements LoginView {
private LoginPresenter loginPresenter;
private EditText txtUserName;
private EditText txtPassword;
private Button btnLogin;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
initializePresenter();
initializeViews();
}
private void initializePresenter() {
loginPresenter = new LoginPresenter(this);
}
private void initializeViews() {
txtUserName = (EditText) findViewById(R.id.txt_user_name);
txtPassword = (EditText) findViewById(R.id.txt_password);
btnLogin = (Button) findViewById(R.id.btn_login);
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
loginPresenter.checkUserPassword(
txtUserName.getText().toString().trim(),
txtPassword.getText().toString().trim()
);
}
});
}
@Override
public void showErrorMessageForUserPassword() {
Snackbar.make(txtPassword, R.string.error_user_password, Snackbar.LENGTH_LONG).show();
}
@Override
public void showErrorMessageForMaxLoginAttempt() {
Snackbar.make(btnLogin, R.string.error_max_login_attempt, Snackbar.LENGTH_LONG).show();
}
@Override
public void showLoginSuccessMessage() {
Snackbar.make(btnLogin, R.string.login_ok, Snackbar.LENGTH_LONG).show();
}
}
Update LoginPresenter to accept and use the view:
public class LoginPresenter {
private final LoginView loginView;
private int currentLoginAttempt = 0;
private static final int MAX_LOGIN_ATTEMPT = 3;
private static final String USER = "user";
private static final String PASSWORD = "password";
public LoginPresenter(LoginView loginView) {
this.loginView = loginView;
}
public int newLoginAttempt() {
return ++currentLoginAttempt;
}
public boolean isLoginAttemptExceeded() {
return currentLoginAttempt >= MAX_LOGIN_ATTEMPT;
}
public boolean checkUserPassword(String user, String password) {
boolean ret = true;
if (isLoginAttemptExceeded()) {
loginView.showErrorMessageForMaxLoginAttempt();
ret = false;
} else if (user.equals(USER) && password.equals(PASSWORD)) {
loginView.showLoginSuccessMessage();
} else {
loginView.showErrorMessageForUserPassword();
ret = false;
newLoginAttempt();
}
return ret;
}
}
After this change, the existing tests no longer compile — LoginPresenter now requires a LoginView object. How do we provide one in a test? That’s where mocking comes in.
What is mocking?
When testing an object whose methods depend on another object, you simulate that dependency instead of creating a real instance. This simulation is called mocking, and it lets you write true unit tests.
We’ll use Mockito. Add it to build.gradle:
testCompile "org.mockito:mockito-core:2.+"
androidTestCompile "org.mockito:mockito-android:2.+"
Now mock the LoginView:
loginView = mock(LoginView.class);
Pass it to LoginPresenter:
@Test
public void checkIfLoginAttemptIsExceeded() {
LoginPresenter loginPresenter = new LoginPresenter(loginView);
Assert.assertEquals(1, loginPresenter.newLoginAttempt());
Assert.assertEquals(2, loginPresenter.newLoginAttempt());
Assert.assertEquals(3, loginPresenter.newLoginAttempt());
Assert.assertTrue(loginPresenter.isLoginAttemptExceeded());
}
Run it — it’s green again.
Integration tests (View ↔ Presenter)
To verify that the view calls the correct method after a presenter interaction, use Mockito’s verify():
@Test
public void checkUserAndPasswordIsCorrect() {
LoginPresenter loginPresenter = new LoginPresenter(loginView);
loginPresenter.checkUserPassword("user", "password");
verify(loginView).showLoginSuccessMessage();
}
This verifies that showLoginSuccessMessage() is called — which means the presenter and view are correctly integrated. The rest of the integration tests:
@Test
public void checkUserAndPasswordIsNotCorrect() {
LoginPresenter loginPresenter = new LoginPresenter(loginView);
loginPresenter.checkUserPassword("user1", "password1");
verify(loginView).showErrorMessageForUserPassword();
}
@Test
public void checkIfLoginAttemptIsExceededWithMessage() {
LoginPresenter loginPresenter = new LoginPresenter(loginView);
loginPresenter.checkUserPassword("user1", "password1");
loginPresenter.checkUserPassword("user1", "password1");
loginPresenter.checkUserPassword("user1", "password1");
loginPresenter.checkUserPassword("user1", "password1");
verify(loginView).showErrorMessageForMaxLoginAttempt();
}
Download the current project state: https://github.com/jamontes79/TDD_Ejemplo/tree/518f5e7e79cdb741f7470360b1b7433eb1810e59
What do you think of Mockito for Android? Do you use it regularly in your projects?