Tutorial de pruebas de Android: Pruebas unitarias como un verdadero droide verde

Publicado: 2022-03-11

Como desarrolladores de aplicaciones experimentados, a medida que maduran las aplicaciones que desarrollamos, tenemos la corazonada de que es el momento de comenzar a probar. Las reglas comerciales a menudo implican que el sistema debe proporcionar estabilidad a lo largo de diferentes versiones. Idealmente, también queremos automatizar el proceso de compilación y publicar la aplicación automáticamente. Para esto, necesitamos herramientas de prueba de Adnroid para garantizar que la compilación funcione como se esperaba.

Las pruebas pueden proporcionar un nivel adicional de confianza sobre las cosas que construimos. Es difícil (si no imposible) construir un producto perfecto y libre de errores. Por lo tanto, nuestro objetivo será mejorar nuestras probabilidades de éxito en el mercado configurando un conjunto de pruebas que detectará rápidamente los errores recién introducidos en nuestra aplicación.

Tutorial de prueba de Android

Cuando se trata de Android y las diversas plataformas móviles en general, la prueba de aplicaciones puede ser un desafío. Implementar pruebas unitarias y seguir los principios del desarrollo basado en pruebas o similar a menudo puede parecer poco intuitivo, como mínimo. No obstante, las pruebas son importantes y no deben darse por sentadas ni ignorarse. David, Kent y Martin han discutido los beneficios y las desventajas de las pruebas en una serie de conversaciones entre ellos compiladas en un artículo titulado "¿TDD está muerto?". También puede encontrar las conversaciones de video reales allí y obtener más información si las pruebas se ajustan a su proceso de desarrollo y en qué medida podría incorporarlas, a partir de ahora.

En este tutorial de pruebas de Android, lo guiaré a través de las pruebas unitarias y de aceptación, regresión en Android. Nos centraremos en la abstracción de la unidad de pruebas en Android, seguida de ejemplos de pruebas de aceptación, con el objetivo de hacer que el proceso sea lo más rápido y simple posible para acortar los ciclos de retroalimentación del desarrollador y el control de calidad.

¿Debería leerlo?

Este tutorial explorará las diferentes posibilidades cuando se trata de probar aplicaciones de Android. Los desarrolladores o administradores de proyectos que deseen comprender mejor las posibilidades de prueba actuales de la plataforma Android pueden decidir utilizar este tutorial si desean adoptar alguno de los enfoques mencionados en este artículo. Sin embargo, esto no es una bala de plata, ya que la discusión involucrada en un tema de este tipo varía inherentemente de un producto a otro junto con los plazos, la calidad de la base de código del código, el nivel de acoplamiento del sistema, la preferencia del desarrollador en el diseño de la arquitectura, la vida útil proyectada de la función para prueba, etc

Pensando en Unidades: Pruebas de Android

Idealmente, queremos probar una unidad/componente lógico de una arquitectura de forma independiente. De esta manera podemos garantizar que nuestro componente funcione correctamente para el conjunto de entradas que esperamos. Las dependencias se pueden simular, lo que nos permitirá escribir pruebas que se ejecuten rápidamente. Además, podremos simular diferentes estados del sistema en función de la entrada suministrada a la prueba, cubriendo casos exóticos en el proceso.

El objetivo de las pruebas unitarias de Android es aislar cada parte del programa y mostrar que las partes individuales son correctas. Una prueba unitaria proporciona un contrato escrito estricto que debe cumplir la pieza de código. Como resultado, ofrece varios beneficios. —Wikipedia

roboelectrico

Robolectric es un marco de pruebas unitarias de Android que le permite ejecutar pruebas dentro de la JVM en su estación de trabajo de desarrollo. Robolectric reescribe las clases SDK de Android a medida que se cargan y hace posible que se ejecuten en una JVM normal, lo que resulta en tiempos de prueba más rápidos. Además, maneja la inflación de vistas, la carga de recursos y más cosas que se implementan en código C nativo en dispositivos Android, lo que hace que la necesidad de emuladores y dispositivos físicos para ejecutar pruebas automatizadas sea obsoleta.

Mockito

Mockito es un marco de simulación que nos permite escribir pruebas limpias en Java. Simplifica el proceso de creación de dobles de prueba (simulacros), que se utilizan para reemplazar las dependencias originales de un componente/módulo utilizado en producción. Una respuesta de StackOverflow analiza las diferencias entre simulacros y stubs en términos bastante simples que puede leer para obtener más información.

 // you can mock concrete classes, not only interfaces LinkedList mockedList = mock(LinkedList.class); // stubbing appears before the actual execution when(mockedList.get(0)).thenReturn("first"); // the following prints "first" System.out.println(mockedList.get(0)); // the following prints "null" because get(999) was not stubbed System.out.println(mockedList.get(999));

Adicionalmente, con Mockito podemos verificar si se ha llamado a un método:

 // mock creation List mockedList = mock(List.class); // using mock object - it does not throw any "unexpected interaction" exception mockedList.add("one"); mockedList.clear(); // selective, explicit, highly readable verification verify(mockedList).add("one"); verify(mockedList).clear(); 

androide de prueba

Ahora, sabemos que podemos especificar pares de acción-reacción que definen lo que sucede una vez que ejecutamos una acción específica en el objeto/componente simulado. Por lo tanto, podemos simular módulos completos de nuestra aplicación y, para cada caso de prueba, hacer que el módulo simulado reaccione de una manera diferente. Las diferentes formas reflejarán los posibles estados del componente probado y el par de componentes simulados.

Examen de la unidad

En esta sección, asumiremos la arquitectura MVP (Model View Presenter). Las actividades y los fragmentos son las vistas, siendo los modelos la capa de depósito para las llamadas a la base de datos o servicios remotos, y el presentador es el "cerebro" que une todo esto implementando una lógica específica para controlar las vistas, los modelos y el flujo de datos a través de la solicitud.

Componentes de abstracción

Simulacros de vistas y modelos

En este ejemplo de prueba de Android, simularemos vistas, modelos y componentes del repositorio, y realizaremos pruebas unitarias del presentador. Esta es una de las pruebas más pequeñas, dirigida a un solo componente en la arquitectura. Además, utilizaremos métodos de creación de apéndices para establecer una cadena de reacciones adecuada y comprobable:

 @RunWith(RobolectricTestRunner.class) @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18) public class FitnessListPresenterTest { private Calendar cal = Calendar.getInstance(); @Mock private IFitnessListModel model; @Mock private IFitnessListView view; private IFitnessListPresenter presenter; @Before public void setup() { MockitoAnnotations.initMocks(this); final FitnessEntry entryMock = mock(FitnessEntry.class); presenter = new FitnessListPresenter(view, model); /* Define the desired behaviour. Queuing the action in "doAnswer" for "when" is executed. Clear and synchronous way of setting reactions for actions (stubbing). */ doAnswer((new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { ArrayList<FitnessEntry> items = new ArrayList<>(); items.add(entryMock); ((IFitnessListPresenterCallback) presenter).onFetchAllSuccess(items); return null; } })).when(model).fetchAllItems((IFitnessListPresenterCallback) presenter); } /** Verify if model.fetchItems was called once. Verify if view.onFetchSuccess is called once with the specified list of type FitnessEntry The concrete implementation of ((IFitnessListPresenterCallback) presenter).onFetchAllSuccess(items); calls the view.onFetchSuccess(...) method. This is why we verify that view.onFetchSuccess is called once. */ @Test public void testFetchAll() { presenter.fetchAllItems(false); // verify can be called only on mock objects verify(model, times(1)).fetchAllItems((IFitnessListPresenterCallback) presenter); verify(view, times(1)).onFetchSuccess(new ArrayList<>(anyListOf(FitnessEntry.class))); } }

Simulando la capa de red global con MockWebServer

A menudo es conveniente poder simular la capa de red global. MockWebServer nos permite poner en cola respuestas para solicitudes específicas que ejecutamos en nuestras pruebas. Esto nos da la oportunidad de simular respuestas oscuras que esperamos del servidor, pero que no son fáciles de reproducir. Nos permite asegurar una cobertura completa mientras escribimos poco código adicional.

El repositorio de código de MockWebServer proporciona un buen ejemplo al que puede consultar para comprender mejor esta biblioteca.

Dobles de prueba personalizados

Puede escribir su propio modelo o componente de repositorio e inyectarlo en la prueba proporcionando un módulo diferente al gráfico de objetos usando Dagger (http://square.github.io/dagger/). Tenemos la opción de verificar si el estado de la vista se actualizó correctamente en función de los datos proporcionados por el componente del modelo simulado:

 /** Custom mock model class */ public class FitnessListErrorTestModel extends FitnessListModel { // ... @Override public void fetchAllItems(IFitnessListPresenterCallback callback) { callback.onError(); } @Override public void fetchItemsInRange(final IFitnessListPresenterCallback callback, DateFilter filter) { callback.onError(); } }
 @RunWith(RobolectricTestRunner.class) @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18) public class FitnessListPresenterDaggerTest { private FitnessActivity activity; private FitnessListFragment fitnessListFragment; @Before public void setup() { /* setupActivity runs the Activity lifecycle methods on the specified class */ activity = Robolectric.setupActivity(FitnessActivity.class); fitnessListFragment = activity.getFitnessListFragment(); /* Create the objectGraph with the TestModule */ ObjectGraph localGraph = ObjectGraph.create(TestModule.newInstance(fitnessListFragment)); /* Injection */ localGraph.inject(fitnessListFragment); localGraph.inject(fitnessListFragment.getPresenter()); } @Test public void testInteractorError() { fitnessListFragment.getPresenter().fetchAllItems(false); /* suppose that our view shows a Toast message with the specified text below when an error is reported, so we check for it. */ assertEquals(ShadowToast.getTextOfLatestToast(), "Something went wrong!"); } @Module( injects = { FitnessListFragment.class, FitnessListPresenter.class }, overrides = true, library = true ) static class TestModule { private IFitnessListView view; private TestModule(IFitnessListView view){ this.view = view; } public static TestModule newInstance(IFitnessListView view){ return new TestModule(view); } @Provides public IFitnessListInteractor provideFitnessListInteractor(){ return new FitnessListErrorTestModel(); } @Provides public IFitnessListPresenter provideFitnessPresenter(){ return new FitnessListPresenter(view); } } }

Ejecución de pruebas

Estudio Android

Puede hacer clic derecho fácilmente en una clase de prueba, método o paquete de prueba completo y ejecutar las pruebas desde el cuadro de diálogo de opciones en el IDE.

Terminal

La ejecución de pruebas de aplicaciones de Android desde la terminal crea informes para las clases probadas en la carpeta "compilar" del módulo de destino. Aún más, si planea configurar un proceso de compilación automatizado, utilizará el enfoque de terminal. Con Gradle, puede ejecutar todas las pruebas con sabor a depuración ejecutando lo siguiente:

 gradle testDebug

Acceso a la "prueba" del conjunto de fuentes desde la versión de Android Studio

La versión 1.1 de Android Studio y el complemento Android Gradle brindan soporte para la prueba unitaria de su código. Puede obtener más información leyendo su excelente documentación al respecto. La característica es experimental, pero también es una gran inclusión, ya que ahora puede cambiar fácilmente entre sus pruebas unitarias y conjuntos de fuentes de pruebas de instrumentación desde el IDE. Se comporta de la misma manera que si cambiara de sabor en el IDE.

Pruebas de unidades de Android

Facilitar el proceso

Escribir pruebas de aplicaciones de Android puede no ser tan divertido como desarrollar la aplicación original. Por lo tanto, algunos consejos sobre cómo facilitar el proceso de escribir pruebas y evitar problemas comunes al configurar el proyecto serán de gran ayuda.

AfirmarJ Android

AssertJ Android, como habrás adivinado por el nombre, es un conjunto de funciones auxiliares que se construye pensando en Android. Es una extensión de la popular biblioteca AssertJ. La funcionalidad proporcionada por AssertJ Android va desde afirmaciones simples, como "assertThat(view).isGone()", hasta cosas tan complejas como:

 assertThat(layout).isVisible() .isVertical() .hasChildCount(4) .hasShowDividers(SHOW_DIVIDERS_MIDDLE)

Con AssertJ Android y su extensibilidad, tiene garantizado un buen punto de partida simple para escribir pruebas para aplicaciones de Android.

Camino Roboeléctrico y Manifiesto

Mientras usa Robolectric, puede notar que tiene que especificar la ubicación del manifiesto y que la versión del SDK está configurada en 18. Puede hacer esto al incluir una anotación de "Configuración".

 @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18)

Ejecutar pruebas que requieren Robolectric desde la terminal puede presentar nuevos desafíos. Por ejemplo, puede ver excepciones como "Tema no establecido". Si las pruebas se ejecutan correctamente desde el IDE, pero no desde la terminal, es posible que esté intentando ejecutarlas desde una ruta en la terminal donde la ruta de manifiesto especificada no se puede resolver. Es posible que el valor de configuración codificado para la ruta del manifiesto no apunte a la ubicación correcta desde el punto de ejecución del comando. Esto se puede resolver mediante el uso de corredores personalizados:

 public class RobolectricGradleTestRunner extends RobolectricTestRunner { public RobolectricGradleTestRunner(Class<?> testClass) throws InitializationError { super(testClass); } @Override protected AndroidManifest getAppManifest(Config config) { String appRoot = "../app/src/main/"; String manifestPath = appRoot + "AndroidManifest.xml"; String resDir = appRoot + "res"; String assetsDir = appRoot + "assets"; AndroidManifest manifest = createAppManifest(Fs.fileFromPath(manifestPath), Fs.fileFromPath(resDir), Fs.fileFromPath(assetsDir)); return manifest; } }

Configuración de Gradle

Puede usar lo siguiente para configurar Gradle para pruebas unitarias. Es posible que deba modificar los nombres de dependencia y las versiones requeridas según las necesidades de su proyecto.

 // Robolectric testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.9.5' testCompile 'com.squareup.dagger:dagger:1.2.2' testProvided 'com.squareup.dagger:dagger-compiler:1.2.2' testCompile 'com.android.support:support-v4:21.0.+' testCompile 'com.android.support:appcompat-v7:21.0.3' testCompile('org.robolectric:robolectric:2.4') { exclude module: 'classworlds' exclude module: 'commons-logging' exclude module: 'httpclient' exclude module: 'maven-artifact' exclude module: 'maven-artifact-manager' exclude module: 'maven-error-diagnostics' exclude module: 'maven-model' exclude module: 'maven-project' exclude module: 'maven-settings' exclude module: 'plexus-container-default' exclude module: 'plexus-interpolation' exclude module: 'plexus-utils' exclude module: 'wagon-file' exclude module: 'wagon-http-lightweight' exclude module: 'wagon-provider-api' }

Roboelectric y Servicios de Juego

Si está utilizando Google Play Services, deberá crear su propia constante de número entero para la versión de Play Services para que Robolectric funcione correctamente en esta configuración de aplicación.

 <meta-data android:name="com.google.android.gms.version" android:value="@integer/gms_version" tools:replace="android:value" />

Dependencias Robolectric para apoyar bibliotecas

Otro problema de prueba interesante es que Robolectric no puede hacer referencia a las bibliotecas de soporte correctamente. La solución es agregar un archivo “project.properties” al módulo donde están las pruebas. Por ejemplo, para las bibliotecas Support-v4 y AppCompat, el archivo debe contener:

 android.library.reference.1=../../build/intermediates/exploded-aar/com.android.support/support-v4/21.0.3 android.library.reference.2=../../build/intermediates/exploded-aar/com.android.support/appcompat-v7/21.0.3

Pruebas de aceptación/regresión

Las pruebas de aceptación/regresión automatizan parte del paso final de las pruebas en un entorno real, 100 % Android. No utilizamos clases de sistema operativo Android simuladas en este nivel: las pruebas se ejecutan en dispositivos y emuladores reales.

pruebas de aceptación y regresión de Android

Estas circunstancias hacen que el proceso sea mucho más inestable debido a la variedad de dispositivos físicos, configuraciones de emuladores, estados de dispositivos y conjuntos de funciones de cada dispositivo. Además, depende en gran medida de la versión del sistema operativo y del tamaño de la pantalla del teléfono para decidir cómo se mostrará el contenido.

Es un poco complejo crear la prueba correcta que pase en una amplia gama de dispositivos, pero como siempre, debe soñar en grande pero comenzar de a poco. La creación de pruebas con Robotium es un proceso iterativo. Con algunos trucos, se puede simplificar mucho.

robótico

Robotium es un marco de automatización de prueba de Android de código abierto que existe desde enero de 2010. Vale la pena mencionar que Robotium es una solución paga, pero viene con una prueba gratuita justa.

Para acelerar el proceso de escritura de pruebas de Robotium, pasaremos de la escritura manual de pruebas a la grabación de pruebas. La compensación es entre la calidad del código y la velocidad. Si está realizando grandes cambios en su interfaz de usuario, se beneficiará mucho del enfoque de grabación de pruebas y podrá grabar nuevas pruebas rápidamente.

Testdroid Recorder es una grabadora de prueba gratuita que crea pruebas de Robotium a medida que registra los clics que realiza en la interfaz de usuario. Instalar la herramienta es muy fácil, como se describe en sus documentaciones acompañadas de un video paso a paso.

Dado que Testdroid Recorder es un complemento de Eclipse y nos referimos a Android Studio a lo largo de este artículo, idealmente sería motivo de preocupación. Sin embargo, en este caso no es un problema, ya que puedes usar el complemento directamente con un APK y registrar las pruebas contra él.

Una vez que haya creado las pruebas, puede copiarlas y pegarlas en Android Studio, junto con cualquier dependencia que requiera la grabadora Testdroid, y estará listo para comenzar. La prueba grabada se parecería a la siguiente clase:

 public class LoginTest extends ActivityInstrumentationTestCase2<Activity> { private static final String LAUNCHER_ACTIVITY_CLASSNAME = "com.toptal.fitnesstracker.view.activity.SplashActivity"; private static Class<?> launchActivityClass; static { try { launchActivityClass = Class.forName(LAUNCHER_ACTIVITY_CLASSNAME); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } private ExtSolo solo; @SuppressWarnings("unchecked") public LoginTest() { super((Class<Activity>) launchActivityClass); } // executed before every test method @Override public void setUp() throws Exception { super.setUp(); solo = new ExtSolo(getInstrumentation(), getActivity(), this.getClass() .getCanonicalName(), getName()); } // executed after every test method @Override public void tearDown() throws Exception { solo.finishOpenedActivities(); solo.tearDown(); super.tearDown(); } public void testRecorded() throws Exception { try { assertTrue( "Wait for edit text (id: com.toptal.fitnesstracker.R.id.login_username_input) failed.", solo.waitForEditTextById( "com.toptal.fitnesstracker.R.id.login_username_input", 20000)); solo.enterText( (EditText) solo .findViewById("com.toptal.fitnesstracker.R.id.login_username_input"), "[email protected]"); solo.sendKey(ExtSolo.ENTER); solo.sleep(500); assertTrue( "Wait for edit text (id: com.toptal.fitnesstracker.R.id.login_password_input) failed.", solo.waitForEditTextById( "com.toptal.fitnesstracker.R.id.login_password_input", 20000)); solo.enterText( (EditText) solo .findViewById("com.toptal.fitnesstracker.R.id.login_password_input"), "123456"); solo.sendKey(ExtSolo.ENTER); solo.sleep(500); assertTrue( "Wait for button (id: com.toptal.fitnesstracker.R.id.parse_login_button) failed.", solo.waitForButtonById( "com.toptal.fitnesstracker.R.id.parse_login_button", 20000)); solo.clickOnButton((Button) solo .findViewById("com.toptal.fitnesstracker.R.id.parse_login_button")); assertTrue("Wait for text fitness list activity.", solo.waitForActivity(FitnessActivity.class)); assertTrue("Wait for text KM.", solo.waitForText("KM", 20000)); /* Custom class that enables proper clicking of ActionBar action items */ TestUtils.customClickOnView(solo, R.id.action_logout); solo.waitForDialogToOpen(); solo.waitForText("OK"); solo.clickOnText("OK"); assertTrue("waiting for ParseLoginActivity after logout", solo.waitForActivity(ParseLoginActivity.class)); assertTrue( "Wait for button (id: com.toptal.fitnesstracker.R.id.parse_login_button) failed.", solo.waitForButtonById( "com.toptal.fitnesstracker.R.id.parse_login_button", 20000)); } catch (AssertionFailedError e) { solo.fail( "com.example.android.apis.test.Test.testRecorded_scr_fail", e); throw e; } catch (Exception e) { solo.fail( "com.example.android.apis.test.Test.testRecorded_scr_fail", e); throw e; } } }

Si observa detenidamente, notará que gran parte del código es bastante sencillo.

Al grabar pruebas, no se limite a las declaraciones de "espera". Espere a que aparezcan los diálogos, las actividades, los textos. Esto garantizará que la actividad y la jerarquía de vistas estén listas para interactuar cuando realice la acción en la pantalla actual. Al mismo tiempo, tome capturas de pantalla. Las pruebas automatizadas generalmente están desatendidas, y las capturas de pantalla son una de las formas en que puede ver lo que realmente sucedió durante esas pruebas.

Ya sea que las pruebas pasen o fallen, los informes son su mejor amigo. Puede encontrarlos en el directorio de compilación "módulo/compilación/salidas/informes":

informe de prueba

En teoría, el equipo de control de calidad podría registrar las pruebas y optimizarlas. Al poner esfuerzo en un modelo estandarizado para optimizar los casos de prueba, podría lograrse. Cuando normalmente graba pruebas, siempre tiene que modificar un par de cosas para que funcione sin problemas.

Finalmente, para ejecutar estas pruebas desde Android Studio, puede seleccionarlas y ejecutarlas como lo haría con las pruebas unitarias. Desde la terminal, es un one-liner:

 gradle connectedAndroidTest

Rendimiento de las pruebas

La prueba de unidad de Android con Robolectric es extremadamente rápida, porque se ejecuta directamente dentro de la JVM en su máquina. En comparación con eso, las pruebas de aceptación en emuladores y dispositivos físicos son mucho más lentas. Según el tamaño de los flujos que esté probando, puede llevar desde unos segundos hasta unos minutos por caso de prueba. La fase de prueba de aceptación debe usarse como parte de un proceso de construcción automatizado en un servidor de integración continua.

La velocidad se puede mejorar mediante la paralelización en múltiples dispositivos. Consulte esta excelente herramienta de Jake Wharton y los muchachos de Square http://square.github.io/spoon/. También tiene buenos reportajes.

la comida para llevar

Hay una variedad de herramientas de prueba de Android disponibles y, a medida que el ecosistema madure, el proceso de configurar un entorno comprobable y escribir pruebas será más fácil. Todavía hay más desafíos que abordar, y con una amplia comunidad de desarrolladores trabajando en problemas diarios, hay mucho espacio para discusiones constructivas y comentarios rápidos.

Use los enfoques descritos en este tutorial de prueba de Android para guiarlo a la hora de enfrentar los desafíos que tiene por delante. Si tiene problemas, vuelva a consultar este artículo o las referencias vinculadas para encontrar soluciones a problemas conocidos.

En una publicación futura, analizaremos la paralelización, la automatización de la compilación, la integración continua, los ganchos de Github/BitBucket, el control de versiones de artefactos y las prácticas recomendadas para administrar proyectos masivos de aplicaciones móviles con mayor profundidad.