← Back to blog

Pruebas en Apps iOS — Tests de Integración Directa

Una guía práctica sobre cómo implementar tests de integración en aplicaciones iOS para verificar que múltiples componentes funcionan correctamente en conjunto.

Los tests de integración ocupan la capa intermedia de la pirámide de pruebas. Permiten verificar que diferentes partes de un proyecto se integren correctamente, ya sean componentes propios o código externo.

Analogía del Automóvil

Para entender mejor este concepto, considere un automóvil: cuando conectamos el volante al sistema de dirección, verificamos que girar el volante mueva las ruedas en la dirección deseada. Del mismo modo, al conectar los pedales al motor, esperamos que presionar el acelerador proporcione potencia y que soltarlo la detenga.

En desarrollo de software, los tests de integración toman múltiples piezas de código que deben trabajar conjuntamente y garantizan que el resultado sea el esperado.

Ejemplo Práctico

Supongamos una aplicación de gestión de tareas donde verificaremos que la clase TaskManager y la clase Database funcionen correctamente juntas:

class TaskManager {
    private let database: Database

    init(database: Database) {
        self.database = database
    }

    func addTask(_ task: Task) {
        database.saveTask(task)
    }

    func retrieveTasks() -> [Task] {
        return database.getAllTasks()
    }

    func deleteTasks() {
        database.deleteAllTasks()
    }
}

class Database {
    internal var tasks: [Task] = []

    func saveTask(_ task: Task) {
        tasks.append(task)
    }

    func getAllTasks() -> [Task] {
        return tasks
    }

    func deleteAllTasks() {
        tasks.removeAll()
    }
}

Objetivos del Test

  • Cuando se invoca addTask, la tarea debe añadirse a la base de datos
  • Al llamar retrieveTasks, debe retornar la tarea creada
  • Al ejecutar deleteTasks, todas las tareas deben eliminarse

Implementación Inicial

func testAddAndRetrieveAndRemoveTasks() {
    var database = Database()
    var taskManager = TaskManager(database: database)

    let task1 = Task(id: 1, title: "Buy milk", completed: false)
    let task2 = Task(id: 2, title: "Walk the dog", completed: false)

    taskManager.addTask(task1)
    taskManager.addTask(task2)
    var retrievedTasks = taskManager.retrieveTasks()

    XCTAssertEqual(retrievedTasks.count, 2)
    XCTAssertEqual(retrievedTasks[0].title, "Buy milk")
    XCTAssertEqual(retrievedTasks[1].title, "Walk the dog")

    taskManager.deleteTasks()
    retrievedTasks = taskManager.retrieveTasks()
    XCTAssertEqual(retrievedTasks.count, 0)
}

Este enfoque es desorganizado. Aplicar buenas prácticas en tests, como el principio de responsabilidad única, mejora tanto la calidad de los tests como la del código de producción.

Refactorización

Usar el Modificador Internal

El modificador internal permite acceder a propiedades desde cualquier archivo dentro del mismo módulo, pero no desde fuera:

internal var tasks: [Task] = []

Separar por Responsabilidades

func testAddTasks() {
    var database = Database()
    var taskManager = TaskManager(database: database)

    let task1 = Task(id: 1, title: "Buy milk", completed: false)
    let task2 = Task(id: 2, title: "Walk the dog", completed: false)

    taskManager.addTask(task1)
    taskManager.addTask(task2)

    XCTAssertEqual(database.tasks[0].title, "Buy milk")
    XCTAssertEqual(database.tasks[1].title, "Walk the dog")
}

func testRetrieveTasks() {
    var database = Database()
    var taskManager = TaskManager(database: database)
    database.tasks = [
      Task(id: 1, title: "Buy milk", completed: false),
      Task(id: 2, title: "Walk the dog", completed: false)
    ]
    var retrievedTasks = taskManager.retrieveTasks()
    XCTAssertEqual(retrievedTasks.count, 2)
}

func testRemoveTasks() {
    var database = Database()
    var taskManager = TaskManager(database: database)
    database.tasks = [
      Task(id: 1, title: "Buy milk", completed: false),
      Task(id: 2, title: "Walk the dog", completed: false)
    ]
    taskManager.deleteTasks()
    XCTAssertEqual(database.tasks.count, 0)
}

Eliminar Duplicación con setUp y tearDown

var database: Database!
var taskManager: TaskManager!
let task1 = Task(id: 1, title: "Buy milk", completed: false)
let task2 = Task(id: 2, title: "Walk the dog", completed: false)

override func setUp() {
    super.setUp()
    database = Database()
    taskManager = TaskManager(database: database)
}

override func tearDown() {
    database = nil
    taskManager = nil
    super.tearDown()
}

func setTasksToDatabase() {
    database.tasks = [task1, task2]
}

func testAddTasks() {
    taskManager.addTask(task1)
    taskManager.addTask(task2)

    XCTAssertEqual(database.tasks[0].title, "Buy milk")
    XCTAssertEqual(database.tasks[1].title, "Walk the dog")
}

func testRetrieveTasks() {
    setTasksToDatabase()
    var retrievedTasks = taskManager.retrieveTasks()
    XCTAssertEqual(retrievedTasks.count, 2)
}

func testRemoveTasks() {
    setTasksToDatabase()
    taskManager.deleteTasks()
    XCTAssertEqual(database.tasks.count, 0)
}

Los métodos setUp() y tearDown() se ejecutan antes y después de cada test respectivamente. Para que Xcode detecte una función como test, debe tener el prefijo test.

Aunque el número de líneas no se reduce significativamente, el código resultante es más legible y reutilizable dentro del archivo de tests.