Mejorar la calidad de nuestras aplicaciones Android aplicando Testing

Como ya habréis podido ir viendo en los diferentes artículos que vamos publicando, mejorar la calidad de nuestras aplicaciones es una prioridad para nosotros. En este caso nos vamos a centrar en la calidad de las aplicaciones Android.

Nos parece importante dedicarle tiempo y aprender cómo mejorar estos procesos que llevamos a cabo. Por ello, como comentó mi compañero Gonzalo en un reciente artículo sobre la calidad de las aplicaciones dessarrolladas para iOS, guiados por ese objetivo, este verano realizamos un curso de testing para móviles con Karumi.

Aprender y mejorar

En ese articulo, Gonzalo explicaba en líneas generales los tipos de test en los que nos centramos, enfocandose luego en la manera de desarrollarlo para iOS.

En este artículo vamos a ver como podemos realizar los distintos tipos de test, pero para una aplicación Android.

Test unitarios

Como ya hemos visto en anteriores ocasiones, los test unitarios deben de probar pequeñas porciones de código y ser bastante rápidos.

Para ser más exactos, los test unitatios deben:

  • ser rápidos.
  • ser aislados: No tienen que depender del orden de ejecución de los mismo.
  • ser repetibles: siempre deben funcionar igual: o funcionan siempre o fallan siempre.
  • verificarse a si mismos.
  • ser fáciles de entender y de escribir.

Como ya hemos comentado en el artículo anterior, para evitar dependencias y probar exactamente lo que queremos probar, se utilizan los Test Doubles.

En Android, para realizar este tipo de tests se usa la libreria jUnit.

import org.junit.Test;
import static org.junit.Assert.*;

public class ApiClientTest {
   @Test
   public void testShouldReturnTrueIfEmailAndPasswordIsOK() throws Exception {
      ApiClient apiClient = new ApiClient(new MockClock());
      boolean res= apiClient.login("correctName@dominio.com","123456");
      assertEquals(true, res);
   }

   @Test
   public void testShouldReturnFalseIfEmailAndPasswordIsNoOK() throws Exception {
      ApiClient apiClient = new ApiClient(new MockClock());
      boolean res= apiClient.login("incorrectName@dominio.com","123456");
      assertEquals(false, res);
   }
}

En este test podemos ver que estamos probando la funcionalidad de logarnos.

En este caso hemos mockeado la clase Clock, que tiene dependencia con la parte del código que vamos a probar. De este modo, podemos tener control sobre lo que devuelve determinados métodos cuando se los llame, y probar la funcionalidad de login sin que factores externos afecten.

En el primer test, introducimos un valor que sabemos que es correcto, y por eso comprobamos que el resultado es true. Sabemos que al llamar al método login, nuestra aplicación no se conecta a ningún servidor para comprobar que el usuario y la contraseña son correctos, sino que es una comprobación que está dentro del código. En una aplicación normal, esta llamada se haría llamando a un servidor y para probarlo deberíamos mockear esas llamadas. En ese caso, estaríamos realizando un test de integración como veremos más adelante.

 

En el test anterior hemos comprobado la funcionalidad, pero las Activities de Android no se pueden probar con test unitarios. Aunque podemos probar expectativas sobre la UI, aplicando patrones como el ModelViewPresenter. Por ejemplo, comprobar que se ha llamado a un método para mostrar un mensaje de error en la pantalla:

public class LogInPresenterTest {
   private MockApiClient apiClient = new MockApiClient(new Clock());
   private MockLoginView mockView = new MockLoginView();
   @Test
   public void shouldShowErrorIfThereIsSomethingWrongWhileLogIn() {
       apiClient.setFailOnLogIn(true);
       LogInPresenter presenter = new LogInPresenter(apiClient, mockView);
       presenter.login("incorrectName@dominio.com","123456");
       assertTrue(mockView.getSomeoneInvokeShowErrorMessage());
    }
}

Para hacer este test mockeamos la vista de LoginView y comprobamos si se ha llamado al método de mostrar error.

Este tipo de test unitarios se podrían realizar aparte de que hemos visto anteriormente, ya que comprueban de manera unitaria cosas distintas, para mejorar la calidad de las aplicaciones Android.

 

Test de integración

integration test

Como ya hemos visto en otras ocasiones, estos test sirven para comprobar como se relacionan los distintos componentes de nuestro software.

En este caso, que nos hemos centrado en una aplicación Android, lo que vamos a probar es que nuestra aplicación funcione correctamente ante distintas respuesta de backend. Por ejemplo: que cuando la respuesta es la que esperamos, la app funcione correctamente pero que si por ejemplo la respuesta es un 500, nuestra app no se rompa. Pero tenemos que tener claro que no queremos probar el backend desde nuestro cliente. El backend debe tener sus propios test unitarios.

Para probar que nuestra app funciona bien con las diferentes respuestas del servidor, vamos a usar la técnica del stubbing http, es decir, vamos a mockear las respuestas del servidor. Para realizar estos test en Android podemos hacer uso de diferentes herramientas como por ejemplo MockWebServer o Mockito.

 

En el ejemplo que vamos a hacer a continuación, vamos a usar MockWebServer. Cuando llamamos al método de login se hace una llamada al servidor. Estas peticiones son las que vamos a mockear porque que la respuesta del servidor sea correcta no es responsabilidad de este test, si no como se comporta nuestra app cuando lo hace.

Con enqueueMockResponse indicamos el status_code y la respuesta de la próxima petición que vamos a realizar. La respuesta viene dada en el fichero getLoginResponse.json, en el que se indica que el token es “aaaa”:

@Test
public void testShouldReturnOkWhenLoginDataIsOk () throws Exception {
  enqueueMockResponse(200, "getLoginResponse.json");
  String token = apiClient.login("correctName@dominio.com","123456");

  assertEquals(token, "aaaa");
}

 

Del mismo modo, llamando a enqueueMockResponse con otros valores, podremos comprobar que nuestra app hace lo que esperamos cuando recibe ese error, por ejemplo lanzando una excepción.


@Test (expected = UnknownErrorException.class)
public void shouldLaunchException () throws Exception {
  enqueueMockResponse(418);
  apiClient.login("incorrectName@dominio.com","123456");
}

 

Test de UI o de interfaz

Estos test comprueban la parte visible de la aplicación, que lo que el usuario ve es lo correcto. Además comprueban de la misma manera que el usuario va a realizar sus acciones.

Para realizar estos test hay diferentes librerías, como por ejemplo Espresso y Robotium.

Para el ejemplo que vamos a ver a continuación, vamos a usar Espresso. También vamos a usar Mockito para mockear las llamadas a la red, como ya hicimos antes con MockWebServer. Este test comprueba, que si no hay ningún super heroe en la lista, el mensaje que se muestra en pantalla es «No super heroes».


@Rule public IntentsTestRule activityRule =
new IntentsTestRule<>(MainActivity.class, true, false);

@Mock SuperHeroesRepository repository;

@Test
public void showsEmptyCaseIfThereAreNoSuperHeroes() {
   givenThereAreNoSuperHeroes();
   startActivity();
   onView(withText("No super heroes")).check(matches(isDisplayed()));
}

private void givenThereAreNoSuperHeroes() {
   List emptyList = new ArrayList<>();
   when(repository.getAll()).thenReturn(emptyList);
}

private MainActivity startActivity() {
  return activityRule.launchActivity(null);
}

Esta es la sintaxis de Mockito para indicar la respuesta que debe devolver cuando se llame al método getAll de la clase repository:  when(repository.getAll()).thenReturn(emptyList);

Espresso nos ofrece una manera de poder lanzar la actividad, cosa que no podemos hacer simplemente instanciandola como otras clases, porque los Fragments y las Activities las crea el sistema. Se hace a través de la regla @Rule

Después con Espresso, comprobamos que lo que aparece en la pantalla es lo que esperamos ver: onView(withText(«No super heroes»)).check(matches(isDisplayed()));

 

Un detalle muy interesante de Espresso, es la posibilidad de poder lanzar cualquier actividad que necesites.

Vamos a poner un ejemplo para entenderlo mejor, queremos comprobar que en nuestra app se puede crear una carpeta. Para ello, un usuario abriría la app, introduciría su usuario y contraseña y crearía una carpeta. Si todos estos pasos los pusiéramos en un test, este podría fallar por muchísimas razones, y puede que ninguna tuviese que ver con crear carpeta. Si en cambio, mandamos los datos a la app para que el usuario esté logado y lanzamos la actividad de crear carpeta y la creamos, estaríamos solo comprobando esto:


private CreateFolderActivity startActivity() {
    Intent intent = new Intent();
    return activityRule.launchActivity(intent);
}

 

Tests de captura de pantalla

Captura de pantalla

Existen otras maneras de realizar tests de UI, una de ellas es a través de capturas de pantallas. Para realizar estos test se usa la librería screenshots.

El procedimiento para realizar este tipo de test es el siguiente: realizas un test, le ejecutas y se generan una imagen con el resultado de la pantalla al terminar el test. Se debe verificar a ojo que la imagen resultante es la correcta, es decir que este tipo de test no se verifican por sí solos. Una vez que se considera que la captura de pantalla es correcta, la siguiente vez que se lancen los test se comparará la imagen resultante con la primera generada, y si coinciden el test es correcto.

@Test
public void showsJustOnlyOneSuperHero() {
  givenThereAreSomeSuperHeroes(1, ANY_AVENGERS);

  Activity activity = startActivity();

  compareScreenshot(activity);
}

 

Testing exploratorio o testing basado en propiedades

Está claro que a lo largo de los test que se realizan como los anteriormente descritos, no se prueban todos los casos posibles. Por ejemplo, si probamos un formulario, no probamos todos los posibles casos de entrada de un campo, si no que indicamos que si la entrada es A el resultado es B. Y eso es lo que comprobamos en nuestro test. Pero dejamos un montón de casos de prueba sin probar.

Con las propiedades de un objeto y usando la Librería: Junit-quickCheck, generamos un número grande de entradas y además aleatorias.

Por ejemplo, sabemos que un formulario es válido si se cumple la propiedad de que el password tenga una longitud menor que 6:


import com.pholser.junit.quickcheck.Property;
import com.pholser.junit.quickcheck.runner.JUnitQuickcheck;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertTrue;

@RunWith(JUnitQuickcheck.class) public class LoginFormProperties {
    @Property public void testShoulBePassOkIfAllTheLoginValidAreWithOrMoreThanSixCharactersPassword (String password){
        //System.out.println("------------------->" + password);
        LoginForm loginForm = new LoginForm();
        assertTrue(loginForm.isLoginValid() == password.length >= 6);
    }
}

De este modo, el test es ejecutado el número configurado de veces, por defecto es 100, pasándole en este caso cadenas aleatorias para la contraseña, que es lo que hemos indicado en la función.

 

Conclusión

Realizar tests lleva su tiempo, por lo que, obviamente, hay que empezar por desarrollar los test que mayor valor tienen para nuestra aplicación. Y sobretodo, no obsesionarse con el número de tests, es mejor tener pocos test buenos (que sean consistentes y que no fallen aleatoriamente) que muchos malos.

 

¿Qué tipos de test realizas tú para mejorar la calidad de tus aplicaciones Android?

 

Artículos relacionados

Mejora la calidad de tu Apps aplicando Testing – iOS

Test automáticos en Android con Appium

Test Automáticos para iOS con Appium: Appium UI

Test automatizados en iOS

Tests unitarios en JavaScript. Sinon.

Lo mejor y peor de Mocha y de los unit tests en JavaScript

Unit tests y Mock en Python: Introducción

Deja un comentario

¿Necesitas una estimación?

Calcula ahora