UI Testing con EarlGrey 2: Parte 3

Bastián Véliz
Concrete Latinoamérica
10 min readJul 8, 2020

¡Hola Hola! Si es que llegaste hasta acá en búsqueda de lo que ofrecí en la primera entrega de esta serie de artículos, significa que ya has leído las otras 2 partes. Si aún no lo has hecho, puedes leerlas acá:

En el capítulo anterior de “UI Testing con EarlGrey2”

Resumidamente, en la segunda parte:

  • Se preparó la aplicación de prueba agregando identificadores de accesibilidad.
  • Se preparó el target de pruebas de UI para suplir algunas fallas de EarlGrey.
  • Se escribió una prueba usando EarlGrey 2 y se explicó la misma en detalle.

En esta tercera parte finalmente habilitaré el soporte de pruebas de caja blanca y escribiré una prueba para ejemplificarlo.

Recuerda que todo lo que se avanzó en la segunda parte se encuentra en la rama black-box-tests del repositorio que contiene el proyecto de ejemplo.

Pruebas de “caja ¿qué?”

A lo largo de esta serie de artículos he mencionado los conceptos de pruebas de caja negra y caja blanca ¿Pero cómo los aplicamos en el proyecto de ejemplo?

En la parte 2 implementé pruebas en donde el foco principal se centró en verificar que la interacción de un usuario funcione como se espera, dado cierto comportamiento.

¿Cómo la aplicación realiza los llamados a los servicios?

¿De qué forma son construidas las vistas y en qué momento se inyectan las dependencias?

¿Cuáles son las clases y estructuras de datos involucradas?

Todas estas preguntas no pueden ser respondidas, al menos en ese contexto. ¿Por qué? Esto se debe principalmente que las pruebas escritas usando EarlGrey 2 utilizan XCUITests como base y se estructuran sobre en el concepto de caja negra, lo que implica en que no es posible acceder a la estructura interna del elemento a probar. En este caso, no es posible acceder al código fuente de la aplicación ni realizar modificaciones al mismo.

En este artículo, en cambio, se implementarán pruebas de caja blanca, las cuales sí toman en cuenta y utilizan la estructura interna del elemento a probar. Esto implica que las pruebas deberían poder acceder, por ejemplo, al código fuente de la aplicación y modificar su comportamiento.

¿Pero no que EarlGrey 2 se basa en XCUITests?

¿Cómo puedes acceder a los elementos internos de la aplicación si tus pruebas utilizan una herramienta que no lo permite?

Es aquí donde la integración entre EarlGrey 2 y eDistantObject entra en escena.

Funcionamiento interno de EarlGrey 2 — Google

En resumidas cuentas:

  • Cada interacción realizada desde una prueba de EarlGrey es empaquetada en una invocación.
  • La invocación es enviada desde la prueba hacia la aplicación, todo esto usando eDistantObject .
  • La aplicación procesa la invocación, realiza la acción que corresponda y devuelve la respuesta hacia la prueba.

Esto es utilizado por EarlGrey para poder realizar las interacciones y simular gestos de usuario sobre la aplicación, lo que hasta acá proporciona soporte completo para pruebas de caja negra. Sin embargo, también es posible realizar interacciones que involucren modificar elementos internos de la aplicación en tiempo de ejecución, es decir, lo que se espera de una prueba de caja blanca. Esto se verá en detalle, a continuación.

¡Comencemos!

Agregando soporte para pruebas de caja blanca

Para habilitar las pruebas de caja blanca es necesario crear un bundle, el cual será inyectado en el binario de la aplicación al momento de compilar el target de pruebas de UI. Esto se debe a que dentro de este bundle irá todo el código que permitirá comunicarnos con la aplicación desde la prueba que se ejecuta en ItunesSimpleSearchUITests.

Además, es necesario aplicar algunas configuraciones a ItunesSimpleSearchUITests para que tome el bundle que se creará e inyectarlo en la aplicación.

Configurando bundle

En primer lugar, es necesario crear un nuevo target de tipo bundle. Para ello:

  • En Xcode: File -> New -> Target
  • Selecciona macOS -> Bundle
  • El nombre del bundle es HelperBundle
Creando el bundle llamado HelperBundle

Con el bundle creado, debemos hacer los siguientes cambios en la sección Build Settings :

  • En la sección Enable Bitcode selecciona No.
  • Base SDK debe quedar configurado como iOS
HelperBundle — Configuración de Base SDK
  • Agregar -ObjC en Other Linker Flags
HelperBundle — Configuración de Other Linker Flags
  • Añadir @loader_path/Frameworks en Runpath Search Paths
HelperBundle — Configuración de Runpath Search Paths
  • Agregar las siguientes variables en la sección User Header Search Paths seleccionando recursive:
$(SRCROOT)/EarlGrey2
$(SRCROOT)/EarlGrey2/Submodules/eDistantObject
HelperBundle — Configuración de User Header Search Paths
  • En la sección Bundle Loader agrega:
$(TARGET_BUILD_DIR)/ItunesSimpleSearch.app/ItunesSimpleSearch
HelperBundle — Configuración de Bundle Loader

En la sección Build Phases debemos asignar las siguientes configuraciones:

  • Agregar el target principal, es decir ItunesSimpleSearch en la sección Dependencies
HelperBundle — Configuración de Dependencies
  • Agregar AppFramework.framework en la sección Link Binary With Libraries y dejar el campo Status como Optional
HelperBundle — Configuración de Link Binary With Libraries

Configurando target de pruebas de UI

En nuestro target ItunesSimpleSearchUITests debemos integrar el bundle que se creó en el paso anterior, para ello debes hacer lo siguiente:

  • Haz clic en el botón (+) y selecciona New Copy Files Phase.
  • Selecciona Absolute Path para Destination.
  • En la sección Path agrega:
$(TARGET_BUILD_DIR)/../../ItunesSimpleSearch.app/EarlGreyHelperBundles

Si sigues todos estos pasos, debería verse así:

ItunesSimpleSearchUITests — Configuración de Copy Files

Además, debes agregar la biblioteca SDWebImageSwiftUI en la sección Link Binary With Libraries

ItunesSimpleSearchUITests — Configuración de Copy Files

En este punto ya tenemos las configuraciones necesarias para poder compilar la aplicación y las pruebas. Sin embargo al intentar correrlas te encontrarás con la siguiente pantalla:

Nuestras pruebas — Fallando otra vez

Esto se debe a que como el bundle creado no tiene archivos no hay nada que cargar de forma dinámica y a EarlGrey no le gustan estas cosas. Sin embargo, no todo esta perdido. De hecho, a partir de acá comienza la mejor parte de la historia.

Conectando la aplicación con las pruebas de UI

Al fin ha llegado el momento, la hora de la verdad… por lo que has estado esperando desde hace dos artículos atrás. Al fin podrás saber cómo consultar y modificar elementos de la aplicación en tiempo de ejecución mientras corres una prueba de UI.

Dejando el hype aparte (si es que existe) lo primero que se debe hacer es definir que elementos obtener o modificar de la aplicación. Para ello, lo que recomienda la documentación de EarlGrey para swift es utilizar un protocolo y definir allí los elementos a consultar e inyectar. En este caso, lo que haré será configurar una inyección de dependencias para simular diferentes respuestas que podría entregar la API de iTunes. Todo esto se hará como prometí, en tiempo de ejecución y desde el target de pruebas de UI.

Creando las bases de comunicación

En primer lugar debes crear un archivo llamado SwiftTestHost.swift en HelperBundle como se muestra a continuación:

Creando SwiftTestHost.swift

Al momento de crear este archivo ten en cuenta lo siguiente:

  • Selecciona tanto HelperBundle como ItunesSimpleSearchUITest en la sección Targets.
  • Si te Xcode te ofrece crear un archivo de Bridging Header, selecciona Create Bridging Header. Esto creará el archivo HelperBundle-Birgding-Header.h en el que debes agregar las siguientes cabeceras:
#import "AppFramework/Action/GREYAction.h"
#import "AppFramework/Action/GREYActionBlock.h"
#import "AppFramework/Action/GREYActions.h"
#import "CommonLib/DistantObject/GREYHostApplicationDistantObject.h"
#import "CommonLib/Matcher/GREYElementMatcherBlock.h"
#import "CommonLib/Matcher/GREYMatcher.h"

Luego, agrega lo siguiente a SwiftTestHost.swift :

Los métodos definidos en el protocolo ya dan una idea de la forma en la que se hará la inyección de dependencias. Veremos la implementación del mismo en detalle más adelante.

Agregando Mocks

Crea un grupo llamado Mock en HelperBundle y agrega los siguientes archivos:

  • JSONHelper.swift con la lógica para cargar archivos JSON desde HelperBundle
  • MockLookupRepository.swift con la lógica de simular una respuesta/error de servicio:
  • lookup.json con el contenido de la respuesta de servicio:
{
"resultCount": 1,
"results": [
{
"wrapperType": "track",
"kind": "song",
"artistId": 434832774,
"collectionId": 804145376,
"trackId": 804145419,
"artistName": "Romeo Santos",
"collectionName": "Fórmula, Vol. 2 (Deluxe Edition)",
"trackName": "Eres Mía",
"collectionCensoredName": "Fórmula, Vol. 2 (Deluxe Edition)",
"trackCensoredName": "Eres Mía",
"artistViewUrl": "https://itunes.apple.com/us/artist/romeo-santos/434832774?uo=4",
"collectionViewUrl": "https://itunes.apple.com/us/album/eres-m%C3%ADa/804145376?i=804145419&uo=4",
"trackViewUrl": "https://itunes.apple.com/us/album/eres-m%C3%ADa/804145376?i=804145419&uo=4",
"previewUrl": "https://audio-ssl.itunes.apple.com/apple-assets-us-std-000001/Music/v4/27/34/e1/2734e191-351e-3476-07fb-2c1b9f1d2576/mzaf_6415530891726922440.plus.aac.p.m4a",
"artworkUrl30": "https://is1-ssl.mzstatic.com/image/thumb/Music4/v4/19/41/22/194122c7-fee7-f5ba-4fc2-8cf130611faf/source/30x30bb.jpg",
"artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Music4/v4/19/41/22/194122c7-fee7-f5ba-4fc2-8cf130611faf/source/60x60bb.jpg",
"artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Music4/v4/19/41/22/194122c7-fee7-f5ba-4fc2-8cf130611faf/source/100x100bb.jpg",
"collectionPrice": 13.99,
"trackPrice": 1.29,
"releaseDate": "2014-02-25T08:00:00Z",
"collectionExplicitness": "explicit",
"trackExplicitness": "notExplicit",
"discCount": 1,
"discNumber": 1,
"trackCount": 19,
"trackNumber": 6,
"trackTimeMillis": 250640,
"country": "USA",
"currency": "USD",
"primaryGenreName": "Música tropical",
"isStreamable": true
}
]
}
  • lookup-2.json con lo siguiente:
{
"resultCount": 1,
"results": [
{
"wrapperType": "track",
"kind": "song",
"artistId": 434832774,
"collectionId": 1258804404,
"trackId": 1258805162,
"artistName": "Romeo Santos, Daddy Yankee & Nicky Jam",
"collectionName": "Golden",
"trackName": "Bella y Sensual",
"collectionCensoredName": "Golden",
"trackCensoredName": "Bella y Sensual",
"collectionArtistName": "Romeo Santos",
"artistViewUrl": "https://itunes.apple.com/us/artist/romeo-santos/434832774?uo=4",
"collectionViewUrl": "https://itunes.apple.com/us/album/bella-y-sensual/1258804404?i=1258805162&uo=4",
"trackViewUrl": "https://itunes.apple.com/us/album/bella-y-sensual/1258804404?i=1258805162&uo=4",
"previewUrl": "https://audio-ssl.itunes.apple.com/apple-assets-us-std-000001/AudioPreview128/v4/74/c9/02/74c90240-46ce-4127-7347-15de726d377f/mzaf_4771328211351455789.plus.aac.p.m4a",
"artworkUrl30": "https://is4-ssl.mzstatic.com/image/thumb/Music118/v4/7e/fc/5b/7efc5b10-043b-c068-4c02-8a6dabba5cd4/source/30x30bb.jpg",
"artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Music118/v4/7e/fc/5b/7efc5b10-043b-c068-4c02-8a6dabba5cd4/source/60x60bb.jpg",
"artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Music118/v4/7e/fc/5b/7efc5b10-043b-c068-4c02-8a6dabba5cd4/source/100x100bb.jpg",
"collectionPrice": 12.99,
"trackPrice": 1.29,
"releaseDate": "2017-07-21T07:00:00Z",
"collectionExplicitness": "explicit",
"trackExplicitness": "notExplicit",
"discCount": 1,
"discNumber": 1,
"trackCount": 18,
"trackNumber": 10,
"trackTimeMillis": 204701,
"country": "USA",
"currency": "USD",
"primaryGenreName": "Música tropical",
"isStreamable": true
}
]
}

Creando la magia

Este punto es uno de los más importantes, debido a que se agregará la lógica que permite la inyección de dependencias.

En primer lugar, en HelperBundle crea un archivo llamado DistantObject+SwiftTestHost.swift con el siguiente contenido:

Al detenerse a observar este archivo, tenemos lo siguiente:

  • Se crea una extensión de la clase GreyHostApplicationDistantObject, uno de los dos “objetos distantes” que posee EalrGrey y que utilizan la biblioteca eDistantObject para comunicarse con la aplicación principal.
  • La extensión implementa el protocolo SwiftTestHost , el cual define los métodos con los parámetros necesarios para realizar la inyección.
  • Como este bundle tiene como dependencia la aplicación principal, se puede hacer uso de la sentencia @testable import ItunesSimpleSearch y, por ende, se exponen las clases del módulo como si se estuviese escribiendo una prueba. Esto permite poder tener acceso la clase Resolver , la cual posee todas las dependencias del proyecto. Usando Resolver.shared.add es posible reemplazar las dependencias de la aplicación.

Con todo esto listo, ya es posible crear una nueva prueba que realice la inyección.

La prueba final

Para demostrar que la inyección de dependencias es realizada correctamente, se creará la siguiente prueba:

  • Seleccionar la barra de búsqueda y buscar Metallica utilizando internet.
  • Seleccionar el primer elemento de la lista de resultados.
  • Verificar que los elementos presentados en la pantalla de detalle correspondan a una canción de Romeo Santos (sí, bastante diferente).

Para comenzar, crea un archivo de prueba de UI llamado IntegrationTests.swift y agrega el siguiente contenido:

A partir de esto, podemos destacar lo siguiente:

  • Se crea una extensión que contiene un objeto de tipo SwiftTestHost , el cual posee la referencia al “objeto distante” que está conectado con la aplicación principal.
  • En la prueba, se hace la inyección de dependencias llamando a:
self.host.injectLookupDependency(fromJSON: .lookup)
  • Esa llamada produce que se ejecute el código presente en el archivo DistantObject+SwiftTestHost.swift, lo que a su vez hace que se reemplace la dependencia en la aplicación principal.
  • Además, junto con la verificación de la propiedad accessibilityIdentifier también se verifica el valor acessibilityLabel para así estar seguros que el contenido mostrado corresponde al mock que fue inyectado.

Si ejecutamos la prueba, podemos ver el siguiente resultado:

Corriendo la prueba — La inyección de dependencias funcionó

A partir de acá, puedes crear otra prueba usando el contenido de lookup-2.json o crear una prueba que verifique que se muestre en pantalla el mensaje de error. Las posibilidades son infinitas.

Conclusiones

Después de un largo camino recorrido, de unas cuantas pruebas escritas y por sobre todo después de muchas configuraciones aplicadas, puedo concluir lo siguiente:

  • Integrar EarlGrey2 en un proyecto no es trivial. Si deseas crear pruebas de caja negra puedes agregar el pod y ya está. Sin embargo, para tener soporte de pruebas de caja blanca los pasos que hay que seguir son muchos y esto podría resultarle tedioso a más de alguno. Se requiere de mucha paciencia y dedicación (piensen que me tomó TRES artículos para explicar bien como integrarlo)
  • A pesar de lo anterior, si lo que deseas es tener una suite de pruebas de UI robusta y que pueda simular lo más posible la interacción del usuario y sin modificar la aplicación, todo ese esfuerzo lo vale. EarlGrey2 es una gran herramienta que lleva las pruebas de UI al siguiente nivel porque además de su gama de matcher que ya es buena, con su integración con eDistantObject agrega la posibilidad de modificar una aplicación ejecutándose y así poder probar distintas casuísticas. En el caso del proyecto de ejemplo, se produce un beneficio colateral que implica no necesitar crear servidores con mocks utilizando otras herramientas, ya que todo ese código está contenido en el bundle, haciendo toda esa lógica aún más mantenible y modificable.
  • Todo lo anterior solamente será posible si tu aplicación fue diseñada y construida utilizando patrones de diseño y que éstos estén bien aplicados. Es fundamental que al momento de construir tu aplicación lo hagas inmediatamente pensando en que debe ser capaz de ser probada, tanto a nivel unitario como a nivel funcional.

Hemos llegado al final de esta serie de artículos. Espero que te haya servido para conocer y querer utilizar esta gran herramienta. Si tienes alguna pregunta que deses hacer, no dudes en hacerla en la caja de comentarios.

El resultado final de toda esta integración, más algún contenido adicional se encuentran en la rama white-box-tests del repositorio con el proyecto de prueba.

Muchas gracias por leer.

--

--