Un toque de EarlGrey: prueba de interfaz de usuario de la aplicación Toptal Talent

Publicado: 2022-03-11

Una de las cosas más importantes que puede hacer como probador para que su trabajo sea más eficiente y rápido es automatizar la aplicación que está probando. Confiar únicamente en las pruebas manuales no es factible, ya que necesitaría ejecutar el conjunto completo de pruebas todos los días, a veces varias veces al día, probando todos los cambios introducidos en el código de la aplicación.

Este artículo describirá el viaje de nuestro equipo para identificar EarlGrey 1.0 de Google como la herramienta que funcionó mejor para nosotros en el contexto de la automatización de la aplicación iOS Toptal Talent. El hecho de que lo estemos usando no significa que EarlGrey sea la mejor herramienta de prueba para todos, simplemente resulta que es la que se adapta a nuestras necesidades.

Por qué hicimos la transición a EarlGrey

A lo largo de los años, nuestro equipo ha creado diferentes aplicaciones móviles tanto en iOS como en Android. Al principio, consideramos usar una herramienta de prueba de interfaz de usuario multiplataforma que nos permitiera escribir un solo conjunto de pruebas y ejecutarlas en diferentes sistemas operativos móviles. Primero, optamos por Appium, la opción de código abierto más popular disponible.

Pero con el paso del tiempo, las limitaciones de Appium se hicieron cada vez más obvias. En nuestro caso, los dos principales inconvenientes de Appium eran:

  • La cuestionable estabilidad del marco provocó muchas fallas en las pruebas.
  • El proceso de actualización comparativamente lento obstaculizó nuestro trabajo.

Para mitigar la primera deficiencia de Appium, escribimos todo tipo de ajustes de código y trucos para hacer que las pruebas sean más estables. Sin embargo, no había nada que pudiéramos hacer para abordar el segundo. Cada vez que se lanzaba una nueva versión de iOS o Android, Appium tardaba mucho en ponerse al día. Y muy a menudo, por tener muchos errores, la actualización inicial no se podía utilizar. Como resultado, a menudo nos vimos obligados a seguir ejecutando nuestras pruebas en una versión anterior de la plataforma o apagarlas por completo hasta que estuviera disponible una actualización funcional de Appium.

Este enfoque estaba lejos de ser ideal, y debido a estos problemas, junto con otros que no cubriremos en detalle, decidimos buscar alternativas. Los principales criterios para una nueva herramienta de prueba fueron mayor estabilidad y actualizaciones más rápidas . Después de investigar un poco, decidimos utilizar herramientas de prueba nativas para cada plataforma.

Entonces, hicimos la transición a Espresso para el proyecto de Android y a EarlGrey 1.0 para el desarrollo de iOS. En retrospectiva, ahora podemos decir que fue una buena decisión. El tiempo "perdido" debido a la necesidad de escribir y mantener dos conjuntos diferentes de pruebas, uno para cada plataforma, se compensó con creces al no tener que investigar tantas pruebas incompletas y al no tener ningún tiempo de inactividad en las actualizaciones de versiones.

Estructura del proyecto local

Deberá incluir el marco en el mismo proyecto de Xcode que la aplicación que está desarrollando. Así que creamos una carpeta en el directorio raíz para alojar las pruebas de IU. La creación del archivo EarlGrey.swift es obligatoria al instalar el marco de prueba y su contenido está predefinido.

Aplicación Toptal Talent: estructura del proyecto local

EarlGreyBase es la clase principal para todas las clases de prueba. Contiene los métodos generales de instalación y tearDown , XCTestCase setUp En setUp , cargamos los stubs que generalmente se utilizarán en la mayoría de las pruebas (más información sobre stubs más adelante) y también establecemos algunos indicadores de configuración que hemos notado que aumentan la estabilidad de las pruebas:

 // Turn off EarlGrey's network requests tracking since we don't use it and it can block tests execution GREYConfiguration.sharedInstance().setValue([".*"], forConfigKey: kGREYConfigKeyURLBlacklistRegex) GREYConfiguration.sharedInstance().setValue(false, forConfigKey: kGREYConfigKeyAnalyticsEnabled)

Usamos el patrón de diseño de objeto de página: cada pantalla de la aplicación tiene una clase correspondiente donde se definen todos los elementos de la interfaz de usuario y sus posibles interacciones. Esta clase se llama una "página". Los métodos de prueba están agrupados por características que residen en archivos y clases separados de las páginas.

Para darle una mejor idea de cómo se muestra todo, así es como se ven las pantallas de Inicio de sesión y Contraseña olvidada en nuestra aplicación y cómo se representan mediante objetos de página.

Esta es la apariencia de las pantallas de Inicio de sesión y Contraseña olvidada en nuestra aplicación.

Más adelante en el artículo, presentaremos el contenido del código del objeto de la página de inicio de sesión.

Métodos de utilidad personalizados

La forma en que EarlGrey sincroniza las acciones de prueba con la aplicación no siempre es perfecta. Por ejemplo, podría intentar hacer clic en un botón que aún no está cargado en la jerarquía de la interfaz de usuario, lo que provocaría un error en la prueba. Para evitar este problema, creamos métodos personalizados para esperar hasta que los elementos aparezcan en el estado deseado antes de interactuar con ellos.

Aquí están algunos ejemplos:

 static func asyncWaitForVisibility(on element: GREYInteraction) { // By default, EarlGrey blocks test execution while // the app is animating or doing anything in the background. //https://github.com/google/EarlGrey/blob/master/docs/api.md#synchronization GREYConfiguration.sharedInstance().setValue(false, forConfigKey: kGREYConfigKeySynchronizationEnabled) element.assert(grey_sufficientlyVisible()) GREYConfiguration.sharedInstance().setValue(true, forConfigKey: kGREYConfigKeySynchronizationEnabled) } static func waitElementVisibility(for element: GREYInteraction, timeout: Double = 15.0) -> Bool { GREYCondition(name: "Wait for element to appear", block: { var error: NSError? element.assert(grey_notNil(), error: &error) return error == nil }).wait(withTimeout: timeout, pollInterval: 0.5) if !elementVisible(element) { XCTFail("Element didn't appear") } return true }

Otra cosa que EarlGrey no hace por sí solo es desplazar la pantalla hasta que el elemento deseado se vuelve visible. Así es como podemos hacer eso:

 static func elementVisible(_ element: GREYInteraction) -> Bool { var error: NSError? element.assert(grey_notVisible(), error: &error) if error != nil { return true } else { return false } } static func scrollUntilElementVisible(_ scrollDirection: GREYDirection, _ speed: String, _ searchedElement: GREYInteraction, _ actionElement: GREYInteraction) -> Bool { var swipes = 0 while !elementVisible(searchedElement) && swipes < 10 { if speed == "slow" { actionElement.perform(grey_swipeSlowInDirection(scrollDirection)) } else { actionElement.perform(grey_swipeFastInDirection(scrollDirection)) } swipes += 1 } if swipes >= 10 { return false } else { return true } }

Otros métodos de utilidad que faltan en la API de EarlGrey que identificamos son contar elementos y leer valores de texto. El código de estas utilidades está disponible en GitHub: aquí y aquí.

Creación de apéndices de llamadas a la API

Para asegurarnos de evitar resultados de prueba falsos causados ​​por problemas del servidor back-end, usamos la biblioteca OHHTTPStubs para simular llamadas al servidor. La documentación en su página de inicio es bastante sencilla, pero presentaremos cómo agregamos respuestas en nuestra aplicación, que usa la API de GraphQL.

 class StubsHelper { static let testURL = URL(string: "https://[our backend server]")! static func setupOHTTPStub(for request: StubbedRequest, delayed: Bool = false) { stub(condition: isHost(testURL.host!) && hasJsonBody(request.bodyDict())) { _ in let fix = appFixture(forRequest: request) if delayed { return fix.requestTime(0.1, responseTime: 7.0) } else { return fix } } } static let stubbedEmail = "[email protected]" static let stubbedPassword = "password" enum StubbedRequest { case login func bodyDict() -> [String: Any] { switch self { case .login: return EmailPasswordSignInMutation( email: stubbedTalentLogin, password: stubbedTalentPassword ).makeBodyIdentifier() } } func statusCode() -> Int32 { return 200 } func jsonFileName() -> String { let fileName: String switch self { case .login: fileName = "login" } return "\(fileName).json" } } private extension GraphQLOperation { func makeBodyIdentifier() -> [String: Any] { let body: GraphQLMap = [ "query": queryDocument, "variables": variables, "operationName": operationName ] // Normalize values like enums here, otherwise body comparison will fail guard let normalizedBody = body.jsonValue as? [String: Any] else { fatalError() } return normalizedBody } }

La carga del stub se realiza llamando al método setupOHTTPStub :

 StubsHelper.setupOHTTPStub(for: .login)

Poniendo todo junto

Esta sección demostrará cómo usamos todos los principios descritos anteriormente para escribir una prueba de inicio de sesión real de extremo a extremo.

 import EarlGrey final class LoginPage { func login() -> HomePage { fillLoginForm() loginButton().perform(grey_tap()) return HomePage() } func fillLoginForm() { ElementsHelper.waitElementVisibility(emailField()) emailField().perform(grey_replaceText(StubsHelper.stubbedTalentLogin)) passwordField().perform(grey_tap()) passwordField().perform(grey_replaceText(StubsHelper.stubbedTalentPassword)) } func clearAllInputs() { if ElementsHelper.elementVisible(passwordField()) { passwordField().perform(grey_tap()) passwordField().perform(grey_replaceText("")) } emailField().perform(grey_tap()) emailField().perform(grey_replaceText("")) } } private extension LoginPage { func emailField(file: StaticString = #file, line: UInt = #line) -> GREYInteraction { return EarlGrey.selectElement(with: grey_accessibilityLabel("Email"), file: file, line: line) } func passwordField(file: StaticString = #file, line: UInt = #line) -> GREYInteraction { return EarlGrey.selectElement( with: grey_allOf([ grey_accessibilityLabel("Password"), grey_sufficientlyVisible(), grey_userInteractionEnabled() ]), file: file, line: line ) } func loginButton(file: StaticString = #file, line: UInt = #line) -> GREYInteraction { return EarlGrey.selectElement(with: grey_accessibilityID("login_button"), file: file, line: line) } } class BBucketTests: EarlGreyBase { func testLogin() { StubsHelper.setupOHTTPStub(for: .login) LoginPage().clearAllInputs() let homePage = LoginPage().login() GREYAssertTrue( homePage.assertVisible(), reason: "Home screen not displayed after successful login" ) } }

Ejecución de pruebas en CI

Usamos Jenkins como nuestro sistema de integración continua y ejecutamos las pruebas de IU para cada confirmación en cada solicitud de incorporación de cambios.

Usamos fastlane scan para ejecutar las pruebas en CI y generar informes. Es útil tener capturas de pantalla adjuntas a estos informes para las pruebas fallidas. Desafortunadamente, el scan no brinda esta funcionalidad, por lo que tuvimos que personalizarlo.

En la función tearDown() , detectamos si la prueba falló y guardamos una captura de pantalla del simulador de iOS si lo hizo.

 import EarlGrey import XCTest import UIScreenCapture override func tearDown() { if testRun!.failureCount > 0 { // name is a property of the XCTest instance // https://developer.apple.com/documentation/xctest/xctest/1500990-name takeScreenshotAndSave(as: name) } super.tearDown() } func takeScreenshotAndSave(as testCaseName: String) { let imageData = UIScreenCapture.takeSnapshotGetJPEG() let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) let filePath = "\(paths[0])/\(testCaseName).jpg" do { try imageData?.write(to: URL.init(fileURLWithPath: filePath)) } catch { XCTFail("Screenshot not written.") } }

Las capturas de pantalla se guardan en la carpeta del Simulador y deberá buscarlas desde allí para adjuntarlas como artefactos de construcción. Usamos Rake para administrar nuestros scripts de CI. Así es como reunimos los artefactos de prueba:

 def gather_test_artifacts(booted_sim_id, destination_folder) app_container_on_sim = `xcrun simctl get_app_container #{booted_sim_id} [your bundle id] data`.strip FileUtils.cp_r "#{app_container_on_sim}/Documents", destination_folder end

Conclusiones clave

Si está buscando una forma rápida y confiable de automatizar sus pruebas de iOS, no busque más allá de EarlGrey. Es desarrollado y mantenido por Google (¿necesito decir más?) y, en muchos aspectos, es superior a otras herramientas disponibles en la actualidad.

Deberá jugar un poco con el marco para preparar métodos de utilidad para promover la estabilidad de la prueba. Para hacer esto, puede comenzar con nuestros ejemplos de métodos de utilidad personalizados.

Recomendamos realizar pruebas en datos extraídos para asegurarse de que sus pruebas no fallarán porque el servidor back-end no tiene todos los datos de prueba que esperaría que tuviera. Utilice OHHTTPStubs o un servidor web local similar para realizar el trabajo.

Cuando ejecute sus pruebas en CI, asegúrese de proporcionar capturas de pantalla de los casos fallidos para facilitar la depuración.

Quizás se pregunte por qué no migramos a EarlGrey 2.0 todavía, y aquí hay una explicación rápida. La nueva versión se lanzó el año pasado y promete algunas mejoras con respecto a la v1.0. Desafortunadamente, cuando adoptamos EarlGrey, v2.0 no era particularmente estable. Por lo tanto, todavía no hicimos la transición a v2.0. Sin embargo, nuestro equipo está esperando ansiosamente una corrección de errores para la nueva versión para que podamos migrar nuestra infraestructura en el futuro.

Recursos en línea

La guía de inicio de EarlGrey en la página de inicio de GitHub es el lugar desde el que desea comenzar si está considerando el marco de prueba para su proyecto. Allí encontrará una guía de instalación fácil de usar, la documentación de la API de la herramienta y una práctica hoja de trucos que enumera todos los métodos del marco de una manera fácil de usar mientras escribe sus pruebas.

Para obtener información adicional sobre cómo escribir pruebas automatizadas para iOS, también puede consultar una de nuestras publicaciones de blog anteriores.