Evaluando pruebas con Swift Testing
Libranner Santos
21 agosto, 20247min de lectura
Es necesario utilizar Xcode 16 Beta o superior para trabajar con Swift Testing.
Cuando escribimos pruebas, siempre debemos evaluar algún tipo de resultado, ya sea que se arroje un error o que una variable contenga un valor específico. Para realizar esto, Swift Testing provee varias herramientas, dos de estas son:
#require
para evaluar requerimientos antes de realizar operaciones en las pruebas.#expect
para indicar cuál es el resultado esperado dentro de la prueba.
Si no tienes experiencia con Swift Testing, te recomendamos leer los primeros artículos de esta serie:
A continuación, veremos ejemplos de cada una de estas herramientas. Si deseas seguir el tutorial e ir practicando, debes crear un nuevo proyecto con Xcode 16 y escribir el siguiente código en un nuevo archivo:
// 1final class NotesManager {private(set) var notes = [String]()func add(_ note: String) {notes.append(note)}// 2func removeLast() throws {guard !notes.isEmpty else {throw NotesError.noNotesToRemove}notes.removeLast()}// 3func getLastNote() -> String? {return notes.last}}// 4enum NotesError: Error {case noNotesToRemove}
En este código:
- Definimos
NotesManager
, el cual es una clase simple que nos permite agregar notas usandoadd(:_)
y remover la última nota agregada al arreglo usandoremoveLast()
. - Nota que
removeLast()
puede arrojar un error si ejecutamos este método y no hay ninguna nota agregada. getLastNote()
retorna la última nota agregada, en realidad el último String agregado anotes
.- Definimos
NotesError
que contienenoNotesToRemove
, que es el error que arrojamos enremoveLast()
.
Evaluar resultados usando #expect
Las expectations permiten verificar los valores o respuestas deseadas en las pruebas unitarias. Swift Testing cambia la forma en que estamos acostumbrados a usar esta herramienta. Para evaluar resultados, usamos la macro #expect(throws:_:sourceLocation:performing:)
.
Por ejemplo, si quieres escribir una prueba que verifique que un valor es mayor de 100, debes escribir el siguiente código:
@Test func valueShouldBeHigherThan100() {...#expect(value > 100, "Custom Comment")}
La macro #expect
accepta un comentario, el cual en caso de fallar la prueba será mostrado en los reportes.
Agreguemos unas cuantas pruebas al código que vimos en un principio. Si creaste un proyecto e indicaste que usarías Swift Testing para las pruebas, debes tener un directorio cuyo nombre termina "Tests". En nuestro caso, el proyecto se llama MyApp, así que dentro del directorio MyAppTests, abriremos MyAppTests.swift, y sustiuiremos todo el código existente con este:
// 1import Testing@testable import MyApp// 2@Suitestruct NotesManagerTests {// 3private let manager = NotesManager()// 4@Test func addNote() {manager.add("New Note")manager.add("New Note 2")#expect(manager.notes.count == 2)}// 5@Test func removeNote() throws {manager.add("New Note")try manager.removeLast()#expect(manager.notes.isEmpty)}}
Veamos que hace este código:
- Importamos
Testing
para poder hacer uso de Swift Testing y luego importamosMyApp
usando@testable
para tener acceso al código con nivelinternal
de acceso. - Creamos una Suite de pruebas llamada
NotesManagerTests
. Si quieres saber más de Suite y cómo organizar pruebas, puedes leer este artículo Organiza Pruebas con Swift Testing usando Suites y Tags. - Usamos una referencia a
NotesManager
para todas las pruebas, así que la declaramos de manera global para la Suite. Es importante tener en cuenta que será inicializada nuevamente para cada prueba unitaria. - Escribimos la prueba para verificar que cuando agregamos una nota se refleja en el conteo. Para esto, usamos la macro
#expect
, luego de agregar dos notas. - Creamos una segunda prueba para verificar que las notas se eliminan correctamente. Como
removeLast()
puede arrojar un error, necesitamos usatry
y marcar la prueba comothrows
. Gracias a esto último, si ocurre un error, el resultado mostrará la prueba como fallida.
Con esto hemos visto cómo podemos evaluar diferentes condiciones de manera simple y robusta gracias a la macro #expect
. Sin embargo, en la segunda prueba removeNote()
, podemos hacer algo aún mejor.
Evaluando errores
Usando #expect(throws:)
podemos escribir pruebas unitarias que verifican que un error es arrojado. Por ejemplo:
@Test func removeItemError() throws {let model = Model()#expect(throws: ItemError.modelEmpty) {try model.remove(at: 2)}}
Con este código estamos validando que al ejecutar model.remove(at: 2)
, se arroja el error ItemError.modelEmpty
. Podemos validar el tipo de error exacto, o usar por ejemplo ItemError.self
o (any Error).self
si queremos que la verificación sea más genérica.
Si deseáramos hacer verificaciones más complejas cuando estamos haciendo pruebas de que se arroja un error, podemos utilizar otra versión de expect
:
@Test func removeItemError() throws {let model = Model()#expect {try model.remove(at: 2)} throws: { error in// Validaciones avanzadas}}
Usando la macro expect
de esta forma, pasamos dos closures: uno con la operación a ejecutar y otro donde recibimos el error arrojado y retornamos true
o false
según corresponda.
Como ya sabemos cómo crear pruebas unitarias de errores más robustas, creemos una prueba para nuestra Suite. Debajo de removeNote()
, agrega el siguiente código:
@Test func removeLastThrowsError() throws {#expect(throws: Error.self, "No notes to remove") {try manager.removeLast()}}
Con este código estamos evaluando que se arroja un error si ejecutamos removeLast()
cuando no hay notas agregadas. Esto puede ser suficiente en muchos casos, pero gracias a Swift 6.0 y Typed Throws, podemos hacer algo mejor. Reemplaza el código anterior con este:
@Test func removeLastThrowsError() throws {#expect(throws: NotesError.self, "No notes to remove") {try manager.removeLast()}}
Este código es muy parecido al anterior, pero en vez de evaluar Error.self
usamos un tipo de error específico NotesError.self
, lo cual hace la prueba más específica y expresiva.
Pero espera, aún hay más. Podemos indicar exactamente qué error esperamos. Puedes reemplazar NotesError.self
con NotesError.noNotesToRemove
en el código anterior. Así expresamos de manera más clara la intención de la prueba.
Evaluar requerimientos con #require
Cuando escribimos pruebas, puede ser muy útil evaluar que el estado de una variable u objeto esté en un estado específico antes de continuar. Para estos casos, podemos usar la macro #require
. Veamos un ejemplo. Debajo de la prueba removeLastThrowsError()
, agrega el siguiente código:
@Test func getLastNote() throws {let newNote = "New Note"manager.add(newNote)let lastNote = try #require(manager.getLastNote(), "Last note cannot be nil")#expect(lastNote == newNote)}
Con esta prueba queremos validar que getLastNote()
retorna la última nota agregada. Este método puede retornar nil
, así que hacemos uso de #require
para validar que debe retornar un valor diferente a nil
. Debemos usar try
, ya que si no se cumple la condición, se arrojará un error y la prueba se mostrará como fallida. Al igual que #expect
podemos pasar un comentario como parámetro.
Si el requerimiento se cumple, usamos #expect
para evaluar que el valor retornado es el adecuado.
Si tienes experiencia con XCTest
, probablemente has usado XCTUnwrap
para realizar estas evaluaciones de requerimientos.
Conclusión
Evaluar resultados y requerimientos es muy sencillo gracias a Swift Testing y las macros #expect
y #require
. Evaluar los errores que son retornados es muy fácil gracias a la capacidad de #expect
de evaluar errores arrojados por el código.
Hasta ahora hemos visto algunas de las funcionalidades y beneficios de Swift Testing. Sin embargo, todavía falta bastante que explorar. ¡Hasta una nueva entrega!