Skip to main content
← Blog
Android

How to Do TDD in Android — Part 3: Mocking & Integration Tests

3 min read
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?


More in Android