Testing en iOS

Home > News  > Testing en iOS

¿Cómo usarlo?

Nuestro proyectos pueden variar en características y en necesidades, por eso es ideal tener en cuenta el abanico completo para testing en iOS.

Por ejemplo imaginemos estas 2 situaciones, nuestra app:

  • Tiene una interfaz compleja y necesitamos asegurarnos que sea usable desde el iPhone más pequeño hasta el iPad más grande.
  • Soporta viejas versiones de iOS, así que hay posibilidad de que ciertas APIs no funcionen como esperamos en cada versión. 

Necesitamos asegurarnos de que nuestro código funcione en todas, para eso tenemos a disposición distintas técnicas de testing.

 

Pero, ¿qué es testing?

Son métodos que corren nuestro código y se aseguran de que cumpla con las expectativas que le indicamos, son ideales para mantener el correcto funcionamiento de nuestra app a través del tiempo y evitan nuevos bugs o errores. 

 

 

Los principales tipos de test en iOS son: Tests unitarios y Tests de UI.

¿Cómo se escriben?

Tests unitarios

Estos métodos corren una pequeña parte de nuestro código para verificar que cierto input, o parámetro que le pasemos produzca el resultado que esperamos. Si el resultado no es correcto, nuestro código podría tener bugs.

Supongamos que queremos testear que nuestro método este devolviendo un String correctamente, podemos empezar a testear con el framework de test nativo en Swift, llamado XCTest:

    let valorATestear: String = “unValorRandom”
    XCTAssertEqual(valorATestear, "unValorRandom", "They are not equal")
    // El test va a pasar exitosamente

Frameworks de testing

Podemos simplificar la lectura de Test Unitarios usando Quick y Nimble. Incluilos en tu proyecto así:

// Agregalos como dependencias en un Podfile de Cocoapods
pod 'Quick'
pod 'Nimble'
// Luego importalos en la Unit Test Class con estas líneas
import Quick
import Nimble

 

Supongamos que tenemos que asegurarnos que nuestro método para fechas entregue strings sin valores vacíos (nil) y de la forma que esperamos:

describe("date utils") {
context(“cuando lo transformarmos con el método toString") {              
                string = date?.toString(format: .long)
                it(“deberia entregar un valor que no sea nil") {
                    expect(string).toNot(beNil())
                }          
                it(“debería mostrar correctamente el año“) {
                    expect(string).to(endWith("2037"))
                }
            }
        }

Cada vez que corramos este test debería pasar correctamente, así estaremos seguros que no introducimos un error en nuestro código. 

¿Cómo testear servicios?

Con test unitarios también podemos testear que nuestro servicio para comunicarnos con APIs externas no tenga errores. Para esto podemos simular, o mockear, una API creando un archivo .JSON, esto da la capacidad de controlar los datos que devuelve y evita esperar por conexiones lentas. La cargamos en el código de test de la siguiente forma:

func getEvents (success: @escaping (_ events: Array) -> ()) {
let makeUrl =  Bundle(for: type(of: self)).url(forResource: "Events", withExtension: "json")!
AF.request(makeUrl, method: .get).validate().responseDecodable (of: EventResponse.self) { response in …

 

Ahora ya puedo testear la función getEvents() mockeada escribiendo predicciones, por ejemplo “Espero que mi método me devuelva un Array de 20 eventos con X nombre..”

context(“cuando pido eventos al servicio") {             
   waitUntil { done in MockNetworkingProvider.shared.getEvents() { (result) in  events += result
                        done() }}                
it(“debe darme 20 eventos”) {
                    expect(events).to(haveCount(20))}               
      it(“con los nombres especificados”) {
           expect(events[0].title).to(contain("Avengers NOW!"))
           expect(events[1].title).to(contain("Secret Empire"))
expect(events[……

 

Testeando UI con Nimble-Snapshots

¿Qué pasa si nuestra app necesita más soporte en lo relacionado con la interfaz visual?

Nimble también tiene una librería para matchear que dos vistas sean iguales. Eso significa que podemos guardar una vista de referencia y, cada vez que corramos los tests, compararla con la vista a testear. 

Así nos vamos a asegurar que nuestra vista no ha sufrido cambios a través del tiempo.

// Agregalos como dependencias en un Podfile de Cocoapods
pod 'Nimble-Snapshots’
// Luego importalos en la Unit Test Class con el siguiente código
import Nimble_Snapshots
// Carga la vista y comprueba la fidelidad con  el método haveValidSnapshot()                                
     view = CharacterDetailViewController()
     view?.setHero(hero: result.first!)
     view?.loadViewIfNeeded()            
  it(“debería mostrar correctamente el view controller “) {
                expect(view).to(haveValidSnapshot())
     }

 

Tests de UI

¿Cómo podemos asegurarnos que los elementos visuales funcionen siempre?

Contrario a los Tests unitarios, en los Test de UI no se testea el resultado de un pedazo de código sino la interfaz visual que produce. Esta se prueba corriendo la app, luego se pueden simular acciones y gestos, y al final se comprueban las predicciones que hagamos.

Por ejemplo, podemos comprobar en la View Hierarchy que ciertas vistas con determinados tags existen, si estas condiciones se cumplen, el test es exitoso. 

// Podemos simular interacciones con Text Fields
app.textFields["Email"].tap()
     app.textFields["Email"].typeText(“ejemplo@mail.com")
     app.textFields["Password"]
     app.textFields["Email"].typeText(“pass1234”!)
     app.buttons["Login"].tap()
// luego chequeamos que en la View Hierarchy exista el elemento indicado
    XCTAssertTrue(app.tabBars["Tab Bar"].waitForExistence(timeout: 10))
    print(“El login fue exitoso!“)


¿Cómo le podemos sacar más provecho a esta herramienta?

Artifacts

Se les llama Artifacts a aquellos archivos que sean resultado de un flujo de trabajo como pueden ser los tests. 

 

Eso significa que podemos testear en distintos dispositivos y obtener screenshots automáticamente, para asegurarnos que nuestra UI se ve correctamente en cada tamaño. Para esto, podríamos escribir un test que navegue por distintas pantallas y nos de screenshots en iPhone 5, iPhone 13, iPad Mini y iPad Pro. 

Además podemos incluir herramientas para obtener reportes. Por ejemplo, Slather reporta cuanto código se está cubriendo con tests y exporta un reporte en HTML.

 

Integrar tests en un flujo de trabajo

 

Todo lo que vimos requiere que cada vez que terminamos de escribir código apretemos play en cada test que aparece en el Test Navigator de XCode, esto puede volverse difícil de incluir en la práctica. Para asegurarnos que siempre se corran tests, y ser notificados en tiempo real si algo falla, podemos incluir CI/CD en nuestro proyecto.

Básicamente, CI (Continuous Integration) es un flujo de trabajo que corre cuando introducimos un cambio el código (commit), y CD (Continuous Delivery) es un flujo de trabajo que corre cuando queremos dar release a una nueva versión de la app.

 

 

Las herramientas más conocidas de CI/CD son Bitrise, Jenkins, Travis, etc. Mi combinación preferida es Fastlane + Gitlab CI, después de un par de configuraciones podemos correr los tests en Gitlab cada vez que hagamos un commit en el repositorio.

 

 

Cada herramienta de repositorio tiene su forma de mostrar los reportes de los tests en cada commit que se hizo. También podemos ver en consola todos los logs para detectar si hubo un test fallido y cual fue. Y hasta podemos descargar los artifacts generados por los tests.

 

Comentarios finales

  • Cada proyecto es distinto, algunos incluyen más UI y otros usan más Networking. Lo ideal es elegir qué tipo de tests y herramientas nos van a servir más.
  • Los tests previenen futuros bugs, pero requieren mantenimiento.
  • Lo recomendado es que los tests cubran el 85% del proyecto.
  • Escribir buenos tests es una habilidad que se aprende con práctica.
  • Es buena idea estar al tanto del trabajo del QA Automation del equipo, si es que hay uno, para cubrir la totalidad del proyecto.

Como vemos, hay muchas herramientas que facilitan un proceso de testing continuo, ahora queda en nosotros decidir cuál nos va a servir más y que tipos de tests necesita nuestro proyecto.

Compartí en redes sociales estas buenas prácticas con tus amigos desarrolladores, para que al trabajar en equipo implementen un código más limpio y sólido.