Continuamos con la serie de tutoriales para implementar TDD en Android. Aquí veremos los tests de integración y cómo trabajar sobre la capa UI.
Recordemos que en el anterior artículo “¿Cómo hacer TDD en Android? Parte 2 – Tests Unitarios” vimos como implementar los tests unitarios que cubrían todas las pruebas de la capa de negocio.
A continuación entraremos a ver como probar el correcto funcionamiento de nuestra aplicación y su integración con la capa de UI.
Lo primero que haremos será crear una pantalla básica de Login, con su campo de usuario, contraseña y su botón para acceder.
Para ello crearemos nuestra Activity llamada LoginActivity, cuyo xml asociado será el siguiente:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" 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" tools:context="com.jamontes79.tdd_ejemplo.login.LoginActivity"> <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:visibility="visible" 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_centerVertical="true" 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:layout_marginTop="@dimen/dimen_10dp" android:paddingLeft="@dimen/dimen_10dp" android:paddingRight="@dimen/dimen_10dp" android:text="@string/login" /> </RelativeLayout>
Siguiendo el modelo MVP tendremos que crear un interfaz que conecte nuestra Activity con nuestro Presenter, lo llamaremos LoginView cuyo código será el siguiente:
public interface LoginView { void showErrorMessageForUserPassword(); void showErrorMessageForMaxLoginAttempt(); void showLoginSuccessMessage(); }
Nuestra Activity deberá implementar el interfaz LoginView, quedando el código resultante como se muestra a continuación:
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(); } }
El siguiente pasó sera adaptar el presentador para que se comunique con la Activity, para ello lo modificaremos de la siguiente forma:
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; } }
Después de estos cambios nuestros tests ya no compilan, ya que al Presenter debemos pasarle un objeto LoginView como parámetro, pero… ¿Cómo hacemos esto? Aquí es donde entra en juego el Mocking.
¿Qué es el Mocking?
Cuando tenemos un objeto del que queremos probar sus métodos y este depende de otro, debemos simular este objeto en lugar de crear una instancia real del mismo, este proceso de simulación se llama “moking” y lo usamos para realizar pruebas unitarias.
Para realizar estas simulaciones o moking vamos a usar Mockito.
Para usar Mockito en nuestro proyecto debemos añadir la siguiente referencia en el fichero build.gradle.
testCompile "org.mockito:mockito-core:2.+" androidTestCompile "org.mockito:mockito-android:2.+"
Ahora podemos mockear nuestro LoginView de la siguiente manera:
loginView= mock(LoginView.class);
Ahora ya podemos pasar este objeto a nuestro LoginPresenter, quedando el test de la siguiente manera:
@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()); }
Con esto ya podemos ejecutar el test y vemos como se marca en verde.
Ahora procederemos a realizar los test de integración.
Tests de Integración (Relación entre Vista y Presentador)
Para comprobar que nuestra vista muestra un resultado concreto tras llamar al presentador usaremos el método verify().
@Test public void checkUserAndPasswordIsCorrect(){ LoginPresenter loginPresenter = new LoginPresenter(loginView); loginPresenter.checkUserPassword("user","password"); verify(loginView).showLoginSuccessMessage(); }
En el código anterior verificamos que nuestra vista llama al método showLoginSuccessMessage(), por lo que podemos asegurar que nuestro presentador está perfectamente integrado con la vista.
El resto de tests quedan de la siguiente manera:
@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(); }
Puedes descargar el estado actual del proyecto desde el siguiente enlace de github: https://github.com/jamontes79/TDD_Ejemplo/tree/518f5e7e79cdb741f7470360b1b7433eb1810e59.
¿Que te parece el uso de Mockito para Android? ¿Lo implementas habitualmente en tus proyectos?