Simula la red, la persistencia, los relojes — y el estado de permisos
Simulas la red y la base de datos en tus tests. Pero el diálogo de permisos es la rama sin probar que llega rota a producción. Las llamadas de red, las escrituras a disco y el reloj del sistema se reemplazan con dobles de prueba porque son obviamente difíciles de controlar. El estado de permisos — cámara, micrófono, ubicación, notificaciones — es igual de externo, y sin embargo la mayoría de los códigos lo leen directo del sistema operativo. El resultado: la ruta de "concedido" se ejercita todos los días en tu propio dispositivo, y la ruta de "denegado" corre por primera vez en el teléfono de un usuario.
Cuatro sistemas externos, no tres
La lista clásica de cosas que simular en tests tiene tres entradas. Debería tener cuatro:
| Dependencia | Por qué rompe los tests | Qué devuelve el fake |
|---|---|---|
| Red | Lenta, inestable, necesita un servidor | Respuestas predefinidas, fallos inyectados |
| Persistencia | Filtra estado entre tests | Almacén en memoria, se reinicia por test |
| Reloj | El tiempo avanza; los tests se vuelven inestables | Un instante fijo o avanzado manualmente |
| Estado de permisos | Lo fija el SO, un diálogo por instalación | granted, denied o notDetermined — tú eliges |
Los tres primeros son práctica estándar — Martin Fowler catalogó el vocabulario (dummy, fake, stub, spy, mock) hace dos décadas, y java.time.Clock incluso trae fixed() específicamente para pruebas, para que los tests no dependan del reloj actual. El estado de permisos merece el mismo trato, porque tiene la misma forma: global, mutable, controlado por algo fuera de tu proceso.
El mecanismo: pon un protocolo delante del diálogo
Nunca llames a la API de permisos del SO directamente desde el código de la funcionalidad. Define una interfaz pequeña e inyéctala:
protocol CameraPermission {
var status: AVAuthorizationStatus { get } // .authorized, .denied,
// .restricted, .notDetermined
func request() async -> Bool
}
struct LivePermission: CameraPermission { /* wraps AVCaptureDevice */ }
struct FakePermission: CameraPermission { // test double
var status: AVAuthorizationStatus
var grantOnRequest = false
func request() async -> Bool { grantOnRequest }
}
En plataformas de Apple, los estados reales vienen de AVCaptureDevice.authorizationStatus(for:), que devuelve exactamente cuatro valores: .notDetermined, .restricted, .denied, .authorized. En Android, la costura equivalente envuelve ContextCompat.checkSelfPermission() y el launcher de solicitud. En ambos casos, el fake convierte cada estado en un simple argumento de constructor — sin ajustes del simulador, sin "resetear privacidad", sin diálogo que tocar.
Por qué importa más con tests generados por IA
Un agente de código solo puede generar tests para ramas que puede alcanzar. Si el estado de permisos viene de una llamada estática al SO, la rama de denegado es imposible de probar y el agente la salta en silencio. Pon un protocolo delante y el caso denegado se vuelve una línea de setup — FakePermission(status: .denied) — que un agente enumerará con gusto junto a concedido y aún-no-pedido. La rama que antes llegaba rota a producción por fin tiene cobertura, y es determinista en CI.
Movidas de usuario avanzado
- Prueba todos los estados, no solo dos. Apple tiene cuatro (
restrictedes el que todos olvidan — controles parentales, MDM). Android añade permisos de una sola vez que se revocan solos. Enumera el enum en un test parametrizado. - Prueba la transición, no solo el estado.
notDetermined → request → deniedes una ruta de código distinta a arrancar ya denegado. Elrequest()de tu fake debe dejarte guionar la respuesta. - Roba el patrón completo. swift-dependencies de Point-Free trae relojes, fechas y UUID controlables de serie y te deja registrar tus propias dependencias — los clientes de permisos encajan en el mismo lugar.
- Diseña la UX de denegado a propósito. La guía de Android es explícita: no bloquees la interfaz, no insistas, mantén la app usable con funcionalidad reducida. Solo puedes verificar ese comportamiento si el estado denegado está a una línea de distancia en un test.
Recursos
- Test Double — bliki de Martin Fowler
AVCaptureDevice.authorizationStatus(for:)— Apple Developer Documentation- Request runtime permissions — Android Developers
java.time.Clock— el patrón de reloj inyectable, Java SE 21- swift-dependencies — librería de inyección de dependencias de Point-Free
¿Construyendo una funcionalidad con IA? Yeda AI diseña, audita y entrega sistemas LLM de producción.