Cómo hacer pruebas unitarias de User Defaults

User Defaults nos proveen un mecanismo para persistir datos en nuestras apps. Son muy útiles cuando necesitamos guardar información que debe estar disponible incluso después del usuario haber cerrado la aplicación.

Sin embargo, pueden generar cierta confusión al momento de realizar pruebas unitarias a las mismas. A continuación veremos dos opciones:

  • Opción 1: Creando pruebas unitarias usando un Mock.
  • Opción 2: Creando una instancia para las pruebas.

Opción 1: Creando pruebas unitarias usando un Mock

Una primera opción podría ser hacer un mock y así poder inyectar un objeto a nuestros objetos que se comporte como User Defaults en producción, pero que pueda ser sustituido por otra implementación a la hora de realizar pruebas.

Para crear User Defaults usamos el método:

UserDefaults.standard.set(10, forKey: "com.app.counter")

Para leer podemos usar métodos definidos dependiendo del tipo de datos que deseamos obtener, en este caso usaremos integer:

UserDefaults.standard.integer(forKey: "com.app.counter")

Deseamos poder realizar pruebas sin tener que grabar en disco la información, por lo que crearemos un protocolo para inyectar un objeto que pueda proveer estas funcionalidades:

protocol UserDefaultsProtocol {
func set(_ value: Int, forKey: String)
func integer(forKey: String) -> Int
}

Para conformar a este protocolo necesitaremos ofrecer métodos para guardar (func set(Int, forKey: String)) y leer datos (func integer(forKey: String) -> Int).

Ahora veamos cómo podríamos usar esto en nuestro código:

struct TodoListService {
init(defaults: UserDefaultsProtocol) {
...
}
}

Aquí vemos el TodoListService, el cual espera como parámetro defaults que debe conformar a UserDefaultsProtocol. Esto nos permitiría probar nuestro TodoListService y poder verificar que las llamadas a defaults son realizadas cuando corresponde.

Esta forma de trabajar es muy útil para la mayoría de los casos. Inyectamos una dependencia y damos seguimiento al comportamiento de la misma utilizando alguna técnica para pruebas unitarias, como por ejemplo, el uso de un Spy, o pasando un closure en el constructor el cual se ejecutará cuando se realice una acción determinada. Veamos este segundo caso:

struct UserDefaultsMock: UserDefaultsProtocol {
private let completion: () -> Void
init(completion: @escaping () -> Void) {
self.completion = completion
}
func set(_ value: Int, forKey: String) {
completion()
}
func integer(forKey: String) -> Int {
...
}
}

Ahora veamos cómo usaríamos esto en una prueba unitaria:

func testUserDefaultIsSet() throws {
let expectation = expectation(description: #function)
let mock = UserDefaultsMock {
expectation.fulfill()
}
let service = TodoListService(defaults: mock)
service.doSomething()
wait(for: [expectation])
}

Esta forma de realizar la inyección y las pruebas es útil, pero no es la única, y dependiendo del caso tampoco la recomendada.

Opción 2: Creando una instancia para las pruebas

Otra opción es creando una instancia de UserDefaults que podamos configurar como necesitemos. Usando el siguiente código en el método setUp() del XCTestCase, podemos usar UserDefaults sin persistir en disco, sino en memoria:

// 1
private var userDefaults: UserDefaults!
override func setUp() {
super.setup()
// 2
userDefaults = UserDefaults(suiteName: #file)
// 3
userDefaults.removePersistentDomain(forName: #file)
}

Con este código:

  1. Declaramos una variable de tipo UserDefaults que usaremos para leer y escribir datos en memoria.
  2. Creamos un objeto de UserDefaults inicializado con los valores predeterminados, usando como nombre el contenido de #file.
  3. Borramos el contenido existente; esto es útil porque así tenemos una instancia vacía para cada prueba unitaria.

En muchos casos, esto puede ser suficiente. Para realizar la prueba unitaria que vimos anteriormente, ya no tendríamos que crear un mock, sino pasar directamente la instancia de userDefaults que declaramos.

func testUserDefaultIsSet() throws {
let service = TodoListService(defaults: userDefaults)
service.doSomething()
XCTAssertEqual(1, userDefaults.integer("com.app.counter"))
}

Ahora, con la prueba unitaria, podemos probar que el entero tiene el valor que requerimos, el cual se modifica cuando ejecutamos doSomething(). Sin necesidad de usar un mock, esta técnica puede ser más fiable en la mayoría de los casos, aunque para algunos casos de uso se necesiten usar mocks.

Comparte este artículo

Subscríbete a nuestro Newsletter

Mantente al día en el mundo de las aplicaciones móviles con nuestro blog especializado.

Artículos semanales

Todas las semanas artículos nuevos sobre el mundo de las aplicaciones móviles.

No spam

No te enviaremos spam, solo contenido de calidad. Puedes darte de baja cuando quieras.

Contenido de calidad

Nada de contenido generado de manera automática usando ChatGPT.

Recomendaciones

Tips indispensables sobre mejores prácticas y metodologías.

© 2024 AsyncLearn