Prueba unitaria e inyección de dependencia en Swift

Encora | 17 de julio, 2017

ThumbnailTemplate_SJ17

 

La prueba unitaria es un concepto del que la mayoría de los desarrolladores han oído hablar, pero no todos lo implementan en sus proyectos. En este artículo, veremos cómo hacer pruebas unitarias básicas en Swift usando patios de recreo y cómo aplicar el concepto de inyección de dependencia para mejorar la calidad del código y, lo que es más importante, hacerlo comprobable.

Screen Shot 2017-07-13 at 9.25.40 AM.png

Running Test Cases on XCode

Cuando creamos una prueba unitaria necesitamos entender que lo que vamos a hacer es tomar una pequeña parte del código y verificar que hace lo que se supone que debe hacer. Suena simple, ¿verdad? Eso es porque lo es.

Veamos un ejemplo. Supongamos que tiene la siguiente función:

func sum(x: Int, y: Int) -> Int {
return x + y
}

Esta función es simple, toma dos valores y devuelve la suma de ambos valores como un número entero. Para comprobar que funciona podemos crear una prueba sencilla:

func sum(x: Int, y: Int) -> Int {
return x + y
}

Fácil, ¿verdad? Ahora veámoslo en acción. Abra un patio de recreo y escriba el siguiente código:

import UIKit
import XCTest

func sum(x: Int, y: Int) -> Int {
return x + y
}
class MyTestCase: XCTestCase {

func testSumThreePlusFiveIsEight(){
let result = sum(x: 3, y: 5)
XCTAssertEqual(result, 8, "Result should be 8")
}

}

XCTest es un marco de pruebas unitarias de iOS, XCTestCase es la clase de prueba predeterminada. Es muy sencillo de usar. Todo lo que tiene que hacer es crear una subclase y agregar las funciones de prueba. Todas las funciones que comienzan con la palabra prueba se tratarán como funciones de prueba.

Para ejecutar nuestra prueba, todo lo que tenemos que hacer es llamar a la función run () agregando la siguiente línea:

MyTestCase.defaultTestSuite().run()

Cuando lo ejecutamos, deberíamos ver un resultado exitoso en la consola:


Test Case '-[__lldb_expr_175.MyTestCase testSumThreePlusFiveIsEight]' passed (0.002 seconds).

Ahora bien, si vamos y modificamos la función de suma y el resultado no es correcto, la prueba fallará. Esto nos dice que rompimos algo que debe arreglarse antes de enviar nuestro código al control de versiones.

 

Inyección de dependencia en acción

Entonces, ¿por qué las pruebas unitarias son tan difíciles de implementar? Bueno, a veces nuestro código simplemente no se puede probar. Una de las razones por las que el código es difícil de probar es porque tiene muchas malas prácticas, como dependencias. Podemos usar la inyección de dependencia para resolver esto.

La inyección de dependencia es, en pocas palabras, cuando en un objeto, en lugar de crear una dependencia o pedirle a otro objeto (generalmente una fábrica) que cree la dependencia, mueves la dependencia necesaria al objeto externamente, lo que hace que el proceso de creación de la dependencia sea alguien responsabilidad de los demás.

Veamos un ejemplo de un ejemplo altamente dependiente (acoplado). Supongamos que tenemos las siguientes clases:

class Phone {
func call(number: String){
print ("Real phone calling to \(number)")
}
}
class PersonalAssitant {

let phone = Phone()
let bossNumber = "12345678"

func callBoss(){
phone.call(number: bossNumber)
}
}

Tenemos nuestra clase PersonalAssistant que posee un teléfono, sabe cuál es el número de teléfono de nuestro jefe y tiene una función para usar el teléfono y llamar a nuestro jefe. Luego tenemos la clase Phone que hace la llamada real al número dado.

Ahora intentemos agregar una prueba unitaria para asegurarnos de que nuestro asistente personal está llamando a nuestro jefe.

class PersonalAssistantTestClass: XCTestCase {

func testCallingBoss(){
let assistant = PersonalAssistant()
assistant.callBoss()
}
}

Y ejecútalo:


PersonalAssistantTestClass.defaultTestSuite().run()

Ahora, hay un problema con esta prueba. El código que estamos probando en realidad es llamar al jefe, lo que no hacemos. Queremos saber si el Asistente personal está haciendo la llamada, pero no queremos realmente hacer la llamada. Con nuestra implementación actual no podemos hacer eso, y ahí es donde Dependency Injection viene al rescate.

Primero, necesitaremos refactorizar nuestro código. Comencemos por crear un PhoneProtocol.

protocol PhoneProtocol {
func call(number: String)
}

Este protocolo nos dice que el objeto que implementa tiene una llamada a función que toma una cadena.

Ahora, modifiquemos nuestra clase de teléfono para que se ajuste al protocolo. Debe tener un aspecto como este:

class Phone: PhoneProtocol {
func call(number: String){
print ("Real phone calling to \(number)")
}
}

Ahora, la clase PersonalAssistant también debe refactorizarse.

class PersonalAssistant {

let phone: PhoneProtocol
let bossNumber: String

init(aPhone: PhoneProtocol, myBossNumber: String) {
phone = aPhone
bossNumber = myBossNumber
}

func callBoss(){
phone.call(number: bossNumber)
}
}

Revisemos los cambios: ahora, en lugar de tener su propio teléfono, la clase tendrá un teléfono que cumpla con el Protocolo de teléfono. Esto nos permite darle cualquier teléfono que queramos. Imagínese que la clase solía tener un teléfono con cable y ahora podemos darle un teléfono con cable o un teléfono celular. Esa es la inyección de dependencia, estamos inyectando la dependencia a través de su inicializador.

Ahora, para probarlo, necesitamos tener nuestro propio TestPhone. Para realizar la tarea podemos crear una clase como la siguiente:

class TestPhone: PhoneProtocol {

func call(number: String) {
print ("Test phone calling to \(number)")
}
}

Luego modificamos la prueba para crear nuestro teléfono de prueba y se lo damos a nuestro asistente:

    func testCallingBoss(){
let testPhone = TestPhone()
let assistant = PersonalAssistant(aPhone: testPhone, myBossNumber: "12345678") assistant.callBoss()
}

Si echas un vistazo a los registros, verás cómo la clase TestPhone está llamando al jefe.

Durante las pruebas, necesitamos verificar los resultados, lo que se puede hacer modificando nuestro TestPhone y agregando una propiedad "wasCalled" a la clase y actualizándola cuando se invoca la llamada a la función.

class TestPhone: PhoneProtocol {
var wasCalled = false

func call(number: String) {
print ("Fake phone calling to \(number)")
wasCalled = true
}
}

Actualicemos nuestra prueba para verificar la propiedad al final de la prueba:

func testCallingBoss(){
let testPhone = TestPhone()
let assistant = PersonalAssistant(aPhone: testPhone, myBossNumber: "12345678")
assistant.callBoss()
XCTAssertTrue(testPhone.wasCalled, "Assistant should have called the boss")
}

Ahora, nuestra prueba está completa. Al aplicar Dependency Injection, hemos mejorado la legibilidad, hemos hecho que nuestro código sea comprobable y las dependencias desacopladas, lo cual es una buena práctica.

La implementación de pruebas unitarias en nuestros proyectos proporciona muchos beneficios. Ayudará a encontrar errores en una etapa anterior al reducir la cantidad de errores que normalmente se encontrarían en producción. Esto ayuda a reducir los costos, ya que encontrar un error en las primeras etapas de desarrollo tendrá un impacto menor que encontrarlo en una versión publicada.

Screen Shot 2017-07-13 at 9.15.49 AM.png

Test results on Xcode
 
 
 
  

Contenido

Categorías

Compartir Artículo

Artículos Destacados