Mejora la calidad de tu Apps aplicando Testing – iOS

Siempre nos hemos preocupado por la calidad del Software que desarrollamos. De hecho es una de nuestras máximas. Para ello tenemos un equipo de QA que diseña los Test y valida cada uno de los desarrollos. Nada pasa a producción si no tenemos el OK de QA.

A lo largo del tiempo nos hemos dado cuenta de que en productos de largo recorrido hay multitud de pruebas que se realizan una y otra vez en los Test de QA. Y cada una de estas pruebas se realiza por cada versión de software que se publica. Esto se hace para comprobar que la integración de nuevas características en el Software no estropea nada del funcionamiento que ya existe. El Software llega a producción sin fallos pero los tiempos de QA se van aumentando por cada versión que se publica.

Una solución que adoptamos hace tiempo era añadir Tests automáticos de diferentes tipos. Test unitarios a nivel de clases y métodos, Test de integración con base de datos y servidores y Test de interfaz. Aplicando estos Test nos hemos dado cuenta de que este es el camino y es la manera de aplicar calidad desde la fase de desarrollo, reduciendo así los tiempos de cada publicación.

En el camino de aprender a programar haciendo test se nos cruzó el curso de Testing de Karumi. De hecho fue @david_a_velasco el que se percató de que podría ser interesante para Solid Gear y para cada uno de nosotros. Así que lo contratamos y durante 3 días en intensivo con @pedro_g_s y @flipper83 no hicimos otra cosa que Tests.

En este artículo voy a relatar que aprendimos poniendo el foco en iOS. En un próximo artículo mi compañera @PuripuriGRome hará lo propio con Android.

 

Curso de Testing de Karumi - iOS

 

Hablemos de testing.

Es evidente que aplicar testing añade tiempo de desarrollo. Y no es solo meter Test por meter. Sino varias cosas más:

  • Aplicar una cierta arquitectura al sistema para que sea susceptible de ser testable.
  • Decidir qué tipos de Test son interesantes para testar la característica que se está desarrollando..
  • Definir qué pruebas vamos a realizar.
  • Mantener nuestro sistema de integración continua con los nuevos Test.
  • Y por supuesto desarrollar y lanzar los Tests..

Esto nos va a llevar bastante más tiempo que desarrollar la característica que toque y ya. A cambio vamos a tener un Software de mayor calidad, más libre de bugs y crashes. No hay nada peor que descargar una App de la Store y que se cierre inesperadamente a la primera. Un alto porcentaje de usuarios la borrará y se descargara otra.

Pero tampoco nos volvamos locos.  Aplicar testing es una labor de aprendizaje y se adquiere soltura con el tiempo. Con lo cual a medida que vayamos aplicando Tests a nuestros desarrollos iremos siendo más rápidos.

Al final es mejorar como desarrollador y adquirir la práctica de añadir Tests a tus desarrollos al margen de si está especificado en el proyecto o no lo está.

Arquitectura

En el punto anterior indicaba el tema de utilizar una arquitectura que sea susceptible de ser testable. Y es que es importante programar según unos patrones de diseño de software que sigan los principios S.O.L.I.D.

Curso de Testing de Karumi - iOS

Hay mucho escrito sobre los principios S.O.L.I.D, os dejo un par de links por si no lo conocéis.

https://scotch.io/bar-talk/s-o-l-i-d-the-first-five-principles-of-object-oriented-design

https://en.wikipedia.org/wiki/Solid

En resumen S.O.L.I.D es un acrónimo que engloba cinco principios básicos de la programación orientada a objetos y diseño. Su objetivo es seguir un buen diseño de programación de tal manera que tanto para añadir nuevas funcionalidades como mantener las ya existentes debe hacerse de manera sencilla sin tener que modificar en gran medida el código antiguo.

Las claves son tener clases desacopladas pero con una buena cohesión. Hoy no me voy a centrar en estos temas pero si desarrollamos utilizando los patrones MVP o MVVM será más fácil añadir los Tests.

Qué y cómo aplicamos Testing

Curso de Testing de Karumi - iOS

Primero tenemos que saber qué queremos testar. Veamos estos ejemplos:

  • Quiero testar el carrito de compra de productos dentro de mi app para ver si funciona como se espera.
  • Quiero testar mi capa de red para ver si es consistente según las respuestas del servidor.
  • Quiero testar que los mensajes de interfaz que recibe el usuario son los esperados.

Se diferencian tres tipos distintos de Test:  

  • Los que aplican a los requerimientos de negocio.
  • Los referidos a la integración con el API
  • Los que son de interfaz.

Ahora tenemos que ver cómo lo vamos a testar:

1 ) En el caso de requerimientos de negocio como el carrito de la compra de productos de nuestra app debemos aislar la prueba a solo esta funcionalidad.

Para ello esta parte de nuestro software debe seguir el Principio de Inversión de Dependencia que forma parte de S.O.L.I.D. Esto quiere decir que este código que es el núcleo de nuestra aplicación no dependa de los detalles de implementación, como pueden ser el framework que utilizamos de base de datos o de conexión al servidor.

Si esto no es así es muy complicado hacer tests ya que existirán dependencias con otras clases y no podremos testar de manera aislada.

Para simular estas dependencias necesitamos Test Doubles, que son objetos que simulan diferentes tipos de dependencias. Estos son más comúnmente conocidos como Mocks, aunque realmente un Mock es un tipo de Test Double y hay otros tipos dependiendo de las dependencia que queramos simular.

2) En el caso de que queramos testar la integración de una API deberemos de simular el comportamiento del servidor y realizar diferentes Tests sobre escenarios pre-configurados.

Para simular las respuestas del servidor usando nuestro código deberemos utilizar un framework que intercepta las llamadas y contesta como si fuera el servidor con las respuestas que hemos pre-configurado.

3) En el caso de que queramos testear la interfaz deberemos reemplazar algunas dependencias utilizando Test Doubles para simular los diferentes escenarios y chequear que la información que se presenta en la interfaz es correcta.

Deberemos de simular la fuente de datos para no tener que depender de un servidor y deberemos de ser capaces de cargar solo las vistas que son objetos del Test.

A través de un framework de automatización de UI podremos darle órdenes a la interfaz como si fuéramos el usuario y comprobar los elementos de pantalla para saber si son los esperados.

 

Tipos de Test

Voy a ir explicando uno a uno cada tipo de test que aprendimos en el curso de Karumi. En algunos casos mostraré código de ejemplo en Swift y los frameworks y librerías que son necesarias para poder realizarlos.

Curso de Testing de Karumi - iOS

 

Test Unitario

Un Test Unitario permite verificar de forma individual una funcionalidad de nuestra App, representada normalmente por un método de código. Esto sirve para asegurar que cada funcionalidad funciona correctamente y eficientemente por separado.

El uso más habitual es lanzar los Test unitarios para verificar pruebas de regresión y así comprobar que no se ha estropeado nada que antes funcionaba.

Antes de nada saber que para trabajar con testing en Xcode debemos de tener en nuestro proyecto el target de Tests y crear ahí las clases de testing. Esas clases deberán extender de “XCTestCase” y tener un “setUp” y un “tearDown” como aparece en la imagen. Estos métodos se lanzan antes y después de ejecutar cada Test y sirven para configurar el entorno y después dejarlo como estaba.

Este enlace es interesante para preparar Xcode.

Ahora vamos a ver un ejemplo de Test Unitario.

func testShouldReturnTrueIfEmailAndPasswordIsOK () {
let apiClient = ApliClient(clock: Clock())

let result = apiClient.login(email: "name@domain.com", password: "123456")

XCTAssertEqual(true, result)
}

func testShouldReturnFalseIfEmailWrongAndPasswordIsOK () {
let apiClient = ApliClient(clock: Clock())

let result = apiClient.login(email: "otherName@domain.com", password: "123456")

XCTAssertEqual(false, result)
}

En la imagen se pueden observar dos Test Unitarios que están comprobando el método de login de nuestra API. Los dos Test pasarán correctamente.

En el primero con XCTAssertEqual estamos comprobando que la variable “result” sea “true” porque esperamos que el resultado del login esté bien. Y en el segundo comprobamos lo contrario.

Esto es una prueba muy simple y la clase “ApiClient” no se está conectando con ningún servidor ni repositorio. Pero como hemos comentado antes en la realidad si que estaría conectado así que para simular esas conexiones deberíamos “mockearla”.

Para hacer esto deberemos de crear una clase tipo “MockApiClient” que extienda de “ApiClient” y que no se conecte con la red o repositorio de datos sino que simule esas respuestas.  

 

Test de Integración

Curso de Testing de Karumi - iOS

Estos Test permiten verificar la integración entre diferentes elementos de software. Un ejemplo es para verificar la integración de nuestra API de Red.

Como dijimos la pruebas deben de ser aisladas y no tendremos que utilizar la conexión de internet y un servidor para replicar las respuestas. Para ello utilizaremos una librería de testing llamada Nocilla que intercepta esas llamadas y responde como esté identificado en el Test.

A parte de Nocilla también utilizaremos la librería Nimble. Esta librería comprueba los valores de los Test como lo hace las clases de XCTest pero de manera más simple y con soporte a Test que contengan llamadas asíncronas.

Veamos un par de ejemplos:

Test 1

func testShouldReturnOkWhenLoginDataIsOk () {
        
        let url = Constants.Server.urlAuthServer + Constants.Server.PostTo.auth + Constants.Server.PostTo.basicAuth 
        let expectedReturn = 200
        let userName = "pperez@gmail.com"
        let password = "Password1"
        let expectedToken = "JWT_this_is_the_token"
        let expectedRefreshToken = "This_is_the_refresh_token"
        var newToken = ""
        
        stubRequest("POST", url)
            .withBody(fromJsonFile("loginParameters"))?
            .andReturn(expectedReturn)?
            .withBody(fromJsonFile("loginWithSuccess"))
        
        networkManager.authUser(userName: userName, password: password, onSuccess: { (token) in
            newToken = token
        }) { (error) in
            newToken = ""
        }

        expect(newToken).toEventually(equal(expectedToken), timeout: timeout)
    }

Aquí tenemos un Test que comprueba que se debe de obtener el token apropiado cuando los datos del login son correctos.

Vemos que dividimos las estructuras de los Test en tres partes:  

A) En la primera parte asignamos todas las variables que necesitamos y usando la librería de Nocilla  a través del método “stubRequest”. Indicamos que tendrá que interceptar una llamada tipo “POST” a una url que hemos especificado con uno datos de login que van en el fichero “loginParameters.json”. Según lo que hemos configurado el Servidor simulado tendrá que responder con un código HTTP “200” y con un JSON que se llama “loginWithSuccess.json” que es lo que nos respondería el Servidor real si todo va bien.

B) En la segunda parte lanzamos el método “authUser” de nuestra API de red llamada “NetworkManager” con los parámetros de login correctos. Esta llamada recibe el token correcto.

C) En la tercera parte utilizamos el método “expect” de la librería Nimble con el parámetro “toEventually” para llamadas asíncronas que compara el token esperado con el obtenido.

Test 2

func testShouldReturnFailWhenTheLoginDataIsWrong () {
        let url = Constants.Server.urlAuthServer + Constants.Server.PostTo.auth + Constants.Server.PostTo.basicAuth
        let expectedReturn: Int = 401 //Needs autenthication
        let userName = "pperez@gmail.com"
        let password = "Password1"
        var newToken = ""
        var sgError: SGError!
        
        stubRequest("POST", url)
            .withBody(fromJsonFile("loginParameters"))?
            .andReturn(expectedReturn)
        
        networkManager.authUser(userName: userName, password: password, onSuccess: { (token) in
            newToken = token
        }) { (error) in
            sgError = error
        }
        
        expect(sgError).toEventually(equal(SGError.invalidCredentials), timeout: timeout)
    }

 Este segundo test comprobamos la respuesta de nuestra API de red simulando una respuesta HTTP 401 del servidor.

En la llamada “authUser” del “networkManager” los que nos interesa capturar es el “SGError” (una extensión de Error) que luego comprobaremos en el método expect si es el correcto con el tipo de error “SGError.invalidCredentials” correspondiente con el código HTTP 401.

Como veis de esta forma podemos aplicar Tests a todos nuestros métodos de nuestra API de red, así detectaremos sus vulnerabilidades y las podremos resolver para que nunca lleguen a producción.

 

Test de Interfaz de Usuario

Estos Tests se utilizan para comprobar que lo que aparece en pantalla es lo correcto que tiene que visualizar el usuario.

Como dijimos anteriormente es muy importante tener muy presente el principio de Inversión de Dependencias para desacoplar las interfaces del origen de los datos que muestran.

Además como tratamos con interfaces es importante que podamos cargar las vistas sin que estas tengan que estar relacionadas con un punto de anclaje común para ser utilizadas.

Y por último debemos usar un Framework de automatización para poder manejar la interfaz desde el código. Nosotros vamos a utilizar KIF que permite estas acciones y la identificación de los elementos en pantalla a través de etiquetas de accesibilidad.

Vamos a verlo en acción. Este Test comprueba que el nombre que aparece cuando se selecciona un elemento de la lista es el correcto.

func testShowTheNameOfSuperHeroTappedInTheList () {
let superHeros = givenThereAreSomeSuperHeroes()

openSuperHeroesViewController()

let tableView = tester().waitForView(withAccessibilityLabel:"SuperHeroesTableView") as! UITableView

let indexPath = IndexPath(item:0, section:0)

tester().tapRow(at:indexPath, in:tableView)

tester().waitForView(withAccessibilityLabel: superHeros[0].name)

}

Vemos que primero obtenemos una lista de objetos de un método que simula un repositorio de datos igual que si los proporcionara una base de datos o un servidor.

Después tenemos el método “openSuperHeroesViewController” que lanza esta pantalla en el simulador. Después os enseño el método para que veáis como carga solo la vista de la interfaz que le interesa.

Usando la etiqueta de accesibilidad “SuperHeroesTableView” obtenemos el objeto UITableView. Lo necesitamos para poder interactuar con el.

Con el método “tester” de KIF seleccionamos la primera fila de la tabla y de nuevo con el método “tester” esperamos a que aparezca la label correcta que se llamará igual que el nombre de el primer objeto.

Aquí tenéis el método “openSuperHeroesViewController

fileprivate func openSuperHeroesViewController() {
let superHeroesViewController = ServiceLocator()
.provideSuperHeroesViewController() as! SuperHeroesViewController
superHeroesViewController.presenter = SuperHeroesPresenter(ui: superHeroesViewController,
getSuperHeroes: GetSuperHeroes(repository: repository))
let rootViewController = UINavigationController()
rootViewController.viewControllers = [superHeroesViewController]
present(viewController: rootViewController)
tester().waitForAnimationsToFinish()
}

Primero obtiene el UIViewController que necesita. Se ha delegado la creación de las vistas a la clase «ServiceLocator». Se asigna un «Presenter» para controlar el UIViewController, esto sigue la arquitectura Model View Presenter. Como punto de anclaje usa un UINavigationController y asigna nuestro UIViewController como root. La penultima linea hace la presentación en pantalla y la última utiliza el método tester de KIF para esperar a que la interfaz este presentada.

Todo desacoplado y fácil de conectar, muy valido para desarrollar la aplicación y por supuesto indispensable para aplicar Tests de UI.

Como hemos visto este es otro de los tipos de Test muy útil para comprobar los estados y valores que deben de tener los elementos de nuestra interfaz cuando la mostramos y aplicamos en ella diferentes acciones.

 

Tests de captura de pantalla

Estos Tests son utilizados para que el resultado esperado de la interfaz de la aplicación sea legítimo con el diseño acordado. También se utilizan en las pruebas de regresión para comprobar que las vistas mantengan el estado de interfaz correcto después de haber estado añadiendo características.

En definitiva con estos Test se genera una interfaz y se toma una captura de pantalla que luego se compara con la imagen del resultado esperado. 

Para  generar la interfaz exactamente con la misma información que está en la imagen necesitamos inyectar esa información desde el entorno de testing como ya venimos haciendo anteriormente. Ya sabéis seguir el principio de Inversión de Dependencias.

Para realizar estos Tests utilizamos el framework de Facebook “FBSnapshotTestCase”.  

Veamos un Test de este tipo.  Este test carga una interfaz que es una lista con diez items y lo verifica que el aspecto sea el esperado.

func testShowTenSuperHeroesAreShownProperly () {
_ = givenThereAreSomeSuperHeroes(10)

let viewController = getSuperHeroDetailViewController()

verify(viewController: viewController)
}

Como veis en la imagen estos Test son muy sencillos, claro que lo complicado var por debajo. 

En la primera línea a través de un repositorio mockeado estamos generando una colección de diez objetos. Qué son los que vamos a mostrar en un listado en pantalla.

En la segunda línea vamos a cargar la interfaz, que es la lista con los objetos que hemos mockeado anteriormente.

En la tercera línea se verifica que la interfaz de ese view controller sea exacta a la de la imagen que le corresponde.

Si el Test pasa correctamente todo perfecto sino es que hay algo que ha modificado la interfaz dando un resultado no esperado.

 

Testing exploratorio

Y llegamos al último tipo de test que voy a explicar. Este tipo de test es un poco diferente a los anteriores. En los anteriores hacíamos pruebas controladas para saber que nuestro software funciona dentro de los requisitos del proyecto. Así cubrimos muchas de los posibles fallos que podría tener nuestro software, pero dejarlo a cero es una utopia.

Los test exploratorios sirven para poner a prueba las funcionalidades de tú app, conocerla mejor e investigar posibles fallos que no estemos teniendo en cuenta. Como todo esto es muy amplio se aconseja centrarse en una funcionalidad de la app y diseñar pruebas exploratorias para encontrar posibles fallos. 

Un ejemplo de su uso podría ser poner a prueba los campos de un formulario de entrada de datos para introducir cadenas de diferentes tamaños con diferentes caracteres de diferentes lenguajes. Puede que encontremos fallos con determinadas combinaciones.

Para poder realizar estos Tests en Xcode tendremos que usar la librería de terceros SwiftCheckLa sintaxis de esta librería es un tanto especial y le deberéis dedicar tiempo si queréis llegar a manejarla. Voy a enseñaros un caso práctico sencillo.

Tenemos un formulario de Login típico con dos campos: userName y password. Hemos creado una clase del tipo LoginForm que contiene esos campos. 

class LoginForm {
let charactersOfMinimunPasswordAccepted: Int = 6

public let userName: String
public let password: String

public var isLoginValid: Bool {
return self.password.length < charactersOfMinimunPasswordAccepted ? false : true
}

init(userName: String, password: String) {
self.userName = userName
self.password = password.length < charactersOfMinimunPasswordAccepted ? "" : password
}
}

Es una clase de lógica de un formulario de Login con los campos de userName y password. En la creación del objeto se asigna valor a la propiedad de password solo si el password tiene más de 6 caracteres sino queda vacío. También tenemos una propiedad dinámica que nos dirá si el objeto de login es valido o no. 

Vamos a realizar un Test exploratorio para analizar esta lógica y ver si es consistente probando con 100 cadenas aleatorias de passwords distintas. 

class LoginFormSpecs: XCTestCase {

fileprivate let anyUserName = "userName"
fileprivate let anyPassword = "thePassword"
fileprivate let charactersMinimunForPassword = 6

func testShoulBePassOkIfAllTheLoginValidAreWithOrMoreThanSixCharactersPassword () {

property("The password characters are allways more than six or zero")
<- forAll {(password: String) in
let loginForm = LoginForm(userName: self.anyUserName, password: password)
print("----> \(password)")
return (loginForm.isLoginValid) == (password.length >= self.charactersMinimunForPassword)
}
}

Vamos a utilizar la función «property» de la librería SwiftCheck para crear 100 objetos de Login Form con cadenas de password aleatorias de diferente tamaño y contenido. Según veis el único valor que se va a variar es el de “password” el de userName le dejamos fijo porque no nos interesa para este Test.

Podéis comprobar como al final del property comprobamos si es loginForm es válido con respecto a la lógica del tamaño del password.

Estos son el comienzo de los logs:

Curso de Testing de Karumi - iOS

 

Ahí podéis ver como cuando comienza la prueba empieza a crear objetos con esos valores de password.

Curso de Testing de Karumi - iOS

Aquí podéis como finaliza y como indica que ha pasado 100 tests satisfactoriamente.

La clave del test exploratorio es conocer mejor la aplicación. Esta prueba a funcionado correctamente pero se pueden seguir diseñando más Test exploratorios hasta encontrar alguno que falle y mejorar el código.

 

Conclusión

Espero que con esto tengáis una idea de la cantidad de tipos de Test que existen así como las herramientas que hay para ponerlos en práctica en iOS.  También espero realicéis un diseño de clases según los principios S.O.L.I.D si no lo estáis haciendo ya ;).

Como decía al principio aplicar testing es un esfuerzo que costará tiempo y disminuirá vuestra velocidad de desarrollo. Lo mejor sería tener una cobertura del 100% de Test en nuestro código pero si no disponemos de tanto tiempo podemos empezar con identificar que aporta valor de negocio a nuestro producto y comenzar por ahí a aplicar Tests.

A por ello!!!

Artículos relacionados

Usar Swift en clases Objetive – C

Llega iOS 11, analizamos lo más destacado de la WWDC 2017

Introducción a la programación orientada a protocolos con Swift

Los 8 crímenes más comunes contra la calidad del software

Test unitarios en JavaScript. Sinon

Deja un comentario

¿Necesitas una estimación?

Calcula ahora