Pruebas unitarias asíncronas con Swift Testing
Libranner Santos
11 septiembre, 20245min de lectura
Es necesario utilizar Xcode 16 Beta o superior para trabajar con Swift Testing.
En el artículo anterior de esta serie vimos cómo evaluar pruebas usando la macro #expect
. Sin embargo, esta macro tiene una limitante y es que no nos facilita crear pruebas para código que utiliza completion blocks. Para realizar este tipo de pruebas utilizamos confirmation(_:expectedCount:sourceLocation:_:)
.
Para validar que un evento ocurre cero o más veces en XCTest
, podíamos lograr esto usando XCTestExpectation
. En este artículo veremos cómo podemos realizar esto usando confirmation
.
Para esto necesitaremos utilizar un código de base al cual iremos agregando pruebas unitarias:
protocol Provider {func getNotes() async -> [String]func getNotes(completion: ([String]) -> Void)}
Este protocolo define los requerimientos de nuestros proveedores de información. Tenemos dos métodos que implementar, ambos llamados getNotes
, pero uno es async
y el otro recibe un completion
. Ambos métodos retornan un arreglo de String
.
Ahora las implementaciones:
struct LiveProvider: Provider {func getNotes(completion: ([String]) -> Void) {}func getNotes() async -> [String] {[]}}struct MockProvider: Provider {func getNotes(completion: ([String]) -> Void) {completion(["Note 1", "Note 2"])}func getNotes() async -> [String] {["Note 1", "Note 2"]}}
Ahora tenemos dos struct
que conforman a Provider
. La primera, LiveProvider
, es solo una implementación vacía y sería el proveedor de información que usaríamos en producción. Mientras que MockProvider
es un mock que simula las respuestas, ideal para pruebas, ya que no requiere llamadas a un servidor.
Por último, necesitaremos una modificación a NotesManager
, una clase que ya utilizamos en un artículo anterior, Evaluando pruebas con Swift Testing.
final class NotesManager {private let provider: Providerprivate(set) var notes = [String]()// 1init(provider: Provider = LiveProvider()) {self.provider = provider}// 2func getRemoteNotes() async -> [String] {return await provider.getNotes()}// 3func getRemoteNotes(completion: @escaping ([String]) -> Void) {provider.getNotes(completion: completion)}}
¿Qué hace este código?
- En el constructor esperamos un
Provider
, y por defecto pasamos elLiveProvider
. Esto nos permitirá hacer inyección de dependencias y así poder utilizar diferentes objetos dependiendo del caso. - La primera implementación de
getRemoteNotes
esasync
y llama directamente aprovider.getNotes()
, retornando el resultado. - La segunda implementación de
getRemoteNotes
espera un completion y ejecutaprovider.getNotes(completion: completion)
.
Este es el código para el cual escribiremos las pruebas unitarias.
Escribiendo pruebas unitarias asíncronas
Podemos crear un nuevo archivo File > New > File from Template, seleccionamos Swift Testing Unit Test e indicamos el nombre NoteManagerTests.swift.
Borramos todo el contenido del archivo y agregamos lo siguiente:
// 1import Testing@testable import MyApp@Suitestruct NotesManagerTests {// 2private let manager = NotesManager(provider: MockProvider())// 4@Test func getRemoteNotes() async {let notes = await manager.getRemoteNotes()#expect(["Note 1", "Note 2"] == notes)}}
Este código:
- Importa
Testing
para poder utilizar la librería. - Instanciamos un
NotesManager
pasando en provider el mockMockProvider()
. - Creamos la prueba unitaria
getRemoteNotes()
, la cual esasync
y verificamos quegetRemoteNotes()
retorna los datos esperados["Note 1", "Note 2"]
.
Evaluando pruebas unitarias usando confirmation
Hasta ahora, esta prueba es bastante sencilla y no se diferencia de lo que hemos visto en artículos anteriores. Sin embargo, ¿qué pasa si necesitamos probar la implementación que recibe un completion?
Justo debajo de getRemoteNotes()
, agregamos la siguiente prueba unitaria:
// 1@Test func getRemoteNotesWithCompletion() async {// 2await confirmation("Service called one time") { operation inmanager.getRemoteNotes { _ inoperation()}}}
En este código:
- Creamos otra prueba llamada
getRemoteNotesWithCompletion
, que también esasync
, porqueconfirmation(_:expectedCount:sourceLocation:_:)
es una operación asíncrona. - Usando
confirmation
, pasamos un comentario como primer parámetro, el cual se mostrará en los resultados de las pruebas. Luego pasamos un closure, el cual nos provee una variable que hemos ejecutadooperation
. Hacemos la llamada agetRemoteNotes(completion:)
y dentro del closure ejecutamosoperation()
. Esto incrementará el conteo de llamadas esperadas porconfirmation
.
Por defecto, el número de llamadas esperadas es uno, es decir, que esta prueba espera que operation()
se ejecute una sola vez. Podemos cambiar el número de veces esperadas utilizando el parámetro expectedCount
. Por ejemplo, si quisiéramos que fueran dos en total:
await confirmation("Service called one time", expectedCount: 2) {...}
Ejecuta todas las pruebas usando Command + U, para verificar que todas se ejecutan exitosamente. ¡Buen trabajo!
Conclusión
confirmation
es una de las piezas que hace Swift Testing aún más completo. Nos provee una manera fácil de lidiar con pruebas que requieren ejecutar completion blocks. Este tipo de código es muy común, especialmente en código legado, en aplicaciones donde async/await no se utiliza o en casos donde no hayamos completado la migración a concurrencia moderna.
Ahora contamos con otra herramienta gracias a confirmation
. Todavía nos quedan cosas que explorar sobre Swift Testing, ¡hasta la próxima!