Pruebas de interfaz en iOS
Al terminar: Crear el target de pruebas en Xcode y automatizar un flujo con XCUITest.
Los tests de Android corren en un Android simulado. Estos no: arrancan tu app de verdad en un simulador y la manejan desde fuera, como lo haría una persona. Son más lentos y más fieles, y cubren justo lo que los otros no pueden: que las pestañas cambien de verdad, que el sistema pinte el Form, que un ajuste sobreviva a cerrar la app.
Añadirlos tiene un trabajo previo que en Android no existe: hay que crear en Xcode un target nuevo, un producto más del proyecto, el que contiene los tests. Al terminar tendrás cuatro tests de interfaz que se ejecutan con Cmd + U y desde la Terminal.
Paso 1 · Crea el target de pruebas
Los nombres. En esta lección <tu app> y <tu esquema> son el nombre de tu app y de su esquema (los de Ejecutar en iOS). Si seguiste el curso, los dos se llaman Pokedex. El proyecto de Xcode se llama iosApp, y el target de pruebas que vas a crear, PokedexUITests.
Qué es y por qué ahora. Un target es una receta del proyecto de Xcode que produce algo: tu app es uno (<tu app>); los tests son otro, un paquete de pruebas de interfaz que Xcode instala en el simulador junto a la app y que la lanza. Hasta ahora el proyecto tenía un solo target.
- Con el proyecto abierto en Xcode, menú File → New → Target….
- En la ventana, arriba elige la pestaña iOS, baja hasta la sección de pruebas y elige UI Testing Bundle. Pulsa Next.
- Product Name:
PokedexUITests. En Project dejaiosAppy en Target to be Tested elige el target de tu app. (Equipo y organización pueden quedar como están: en el simulador no hace falta firma.) Pulsa Finish. - En el Project navigator aparece una carpeta
PokedexUITestscon dos ficheros de ejemplo. BorraPokedexUITestsLaunchTests.swift(clic derecho → Delete → Move to Trash): mide el tiempo de arranque y no lo necesitas. El otro,PokedexUITests.swift, lo sustituirás en el paso 3.
Dos comprobaciones antes de seguir:
- La versión mínima. Xcode suele dar al target nuevo el iOS más reciente. Selecciona el proyecto
iosApp(la raíz del navegador) → targetPokedexUITests→ pestaña General: en Minimum Deployments debe poner lo mismo que tu app (míralo en el target de tu app). Si es mayor, el simulador con un iOS más antiguo no podría instalarlo. - Que el esquema lo incluye. Un esquema (scheme) dice qué se construye y qué se ejecuta; es el menú desplegable junto al botón Run, como viste en Ejecutar en iOS. Menú Product → Scheme → Edit Scheme… (
Cmd + <) → Test en la barra izquierda: debe salirPokedexUITestscon la casilla marcada. Si la lista está vacía, pulsa + y añádelo.
Un último ajuste en el mismo sitio: Product → Scheme → Manage Schemes… y marca Shared en la fila de tu esquema. Un esquema compartido se guarda en el propio proyecto (en xcshareddata) y no en tu configuración personal: así lo ven por igual Xcode y xcodebuild, la herramienta de la Terminal con la que lanzarás los tests en el paso 7.
Comprueba que el proyecto ve el target. En la Terminal, desde la carpeta de tu proyecto:
xcodebuild -list -project iosApp/iosApp.xcodeprojBajo Targets: deben salir dos: el de tu app y PokedexUITests.
Paso 2 · Una app que arranca siempre igual
Antes de escribir un test hay que decidir qué app se prueba. La tuya abre de una manera u otra según tus claves de Supabase: con ellas, pide entrar; sin ellas, abre directamente las tres pestañas (haySupabase, de Configuración y secretos por entorno). Un test que depende de eso pasa en una máquina y falla en la de al lado. Y probar la app sin cuentas sería probar una configuración que ningún usuario va a tener.
La solución: que la prueba mande. Al lanzar la app, el test le pasa un argumento de lanzamiento, -uiTests, y la app, solo en ese caso, arranca con una configuración de mentira y una sesión ya dentro, y con Favoritos vacío y sus propios ajustes. Así no toca Supabase, y los tests empiezan siempre igual aunque tú hayas marcado favoritos o cambiado el tema. (La lista de la Pokédex sigue pidiendo Pokémon a PokeAPI, pero ningún test la mira.) A esas piezas de mentira, que ocupan el sitio de las de verdad solo durante la prueba, se les llama dobles de prueba (o simplemente «dobles»).
Antes del fichero real, un ejemplo ajeno a la Pokédex con las dos ideas nuevas: #if DEBUG y leer la lista de argumentos. Cada línea está comentada.
import Foundation
// `#if DEBUG` … `#endif`: lo de dentro solo se compila si existe la marca
// DEBUG. Xcode la pone en las compilaciones de depuración (Run y Test) y
// la quita en la de publicar (Release).
#if DEBUG
enum Pruebas {
// `arguments` es la lista de palabras con las que se lanzó el programa.
// `contains` dice si una de ellas está en esa lista.
static var activas: Bool {
ProcessInfo.processInfo.arguments.contains("-modoPrueba")
}
}
let precio = Pruebas.activas ? 0.0 : 2.5 // en pruebas, el café es gratis
#else
let precio = 2.5 // en la publicada no existe el modo
#endif
print("Café: \(precio) €")Salida real, lanzado de tres maneras:
depuración, sin argumento: Café: 2.5 €
depuración, con -modoPrueba: Café: 0.0 €
publicada, con -modoPrueba: Café: 2.5 €La tercera línea es la razón del #if: en la versión publicada el interruptor ni existe, así que nadie puede encenderlo desde fuera.
Ahora el fichero de la Pokédex, que es nuevo, en iosApp → iosApp → Core (junto a IosAppConfig.swift): clic derecho sobre Core → New File from Template… → Swift File → Next, nombre DoblesParaUITests y Create (el método de La lista en iOS; plan B: touch iosApp/iosApp/Core/DoblesParaUITests.swift). Escríbelo por partes. Primero, el interruptor, que es el de arriba con otro nombre:
import Foundation
import Shared
// `#if DEBUG` es una compilación condicional: lo de dentro solo existe en las
// compilaciones de depuración (las de Run y Test). En la que subes a la App
// Store (Release) este fichero entero desaparece, así que una doble no puede
// acabar publicada.
#if DEBUG
/// ¿Nos ha lanzado una prueba? Lo dice el argumento `-uiTests`, que el test
/// pone al arrancar la app.
enum Pruebas {
static var activas: Bool {
ProcessInfo.processInfo.arguments.contains("-uiTests")
}
}Después, la configuración de mentira. Hereda de BaseAppConfig (de Configuración y secretos por entorno), así que solo declara lo que le interesa:
/// Claves de mentira. Solo importa que no estén vacías: eso es lo que
/// enciende la parte de cuentas.
final class ConfigDeUITests: BaseAppConfig {
override var supabaseUrl: String { "https://pruebas.supabase.co" }
override var supabaseKey: String { "sb_publishable_de_pruebas" }
}Y el almacén seguro (el que guarda la sesión, de Guardar los tokens), en memoria y con una sesión ya dentro. Los nombres del JSON (acceso, refresco, caducaEn…) son los de SessionGuardada, la clase de TokenStore.kt que escribiste en esa lección:
/// Un almacén en memoria con una sesión ya guardada: la app arranca «con la
/// sesión iniciada» sin pasar por el Llavero ni por la red.
final class AlmacenDeUITests: SecureStorage {
private var valores: [String: String]
init() {
// `Date().timeIntervalSince1970` son los segundos que han pasado desde
// el 1 de enero de 1970, la forma habitual de guardar «un momento» como
// número. Caduca dentro de un año: una sesión vencida haría que la app
// saliera a la red a refrescarla, y estas pruebas no tocan la red.
let caduca = Int(Date().timeIntervalSince1970) + 60 * 60 * 24 * 365
// Este JSON lo lee el módulo compartido: los nombres de los campos
// tienen que ser exactamente estos. Es una cadena de varias líneas
// (`"""`); la `\` al final de cada línea significa «no cuentes este
// salto de línea», así el JSON queda en una sola línea.
valores = [
"sesion": """
{"acceso":"acceso-de-pruebas",\
"refresco":"refresco-de-pruebas",\
"caducaEn":\(caduca),\
"usuario":"u-pruebas",\
"email":"entrenador@pueblo-paleta.test"}
""",
]
}
// Las tres funciones de la interfaz `SecureStorage`, sobre un diccionario.
func leer(clave: String) -> String? { valores[clave] }
func guardar(clave: String, valor: String) { valores[clave] = valor }
func borrar(clave: String) { valores.removeValue(forKey: clave) }
}
#endifFalta usarlas. Abre iosApp → iosApp → iOSApp.swift y en el init, sustituye la llamada a startKoinIos(config:secureStorage:) por esta (el resto del init se queda):
#if DEBUG
// En una prueba: configuración y almacén de mentira. Si no, los de siempre.
KoinIosKt.startKoinIos(
config: Pruebas.activas ? ConfigDeUITests() : IosAppConfig(),
secureStorage: Pruebas.activas ? AlmacenDeUITests() : AlmacenDelLlavero()
)
#else
KoinIosKt.startKoinIos(config: IosAppConfig(), secureStorage: AlmacenDelLlavero())
#endifFalta una pieza, esta vez en Kotlin. Con -uiTests, la app de Swift ya lleva una sesión de mentira, pero Favoritos y el tema se guardan en ficheros del simulador (la base de Room y el fichero de ajustes de La base en cada plataforma y Ajustes en el núcleo). Si hubieras marcado un favorito a mano, el test de Favoritos vacíos fallaría; y el último test del paso 5 cambia el tema. La salida: durante un test de interfaz, la base vive en memoria (empieza vacía cada vez) y los ajustes van a otro fichero. Tus favoritos y tu tema, los de verdad, no se tocan.
Abre shared/src/iosMain/kotlin/com/sebastiannarvaez/pokedex/data/local/DatabaseBuilder.ios.kt (vista Project) y haz tres cambios. Primero, el import nuevo, junto a los de platform.Foundation:
// antes
import platform.Foundation.NSFileManager
import platform.Foundation.NSURL
// después
import platform.Foundation.NSFileManager
import platform.Foundation.NSProcessInfo
import platform.Foundation.NSURLSegundo, una propiedad nueva, encima de la nota que ya hay sobre getDatabaseBuilder(). Lee los argumentos con que se lanzó la app, igual que Pruebas.activas en Swift:
/**
* ¿Nos ha lanzado un test de interfaz? Lo dice el argumento `-uiTests`, el
* mismo que mira `Pruebas.activas` en Swift. Nadie puede lanzar con argumentos
* la app que se instala desde la App Store, así que fuera de los tests esto es
* siempre `false`.
*/
private val enUnTestDeInterfaz: Boolean
get() = NSProcessInfo.processInfo.arguments.contains("-uiTests")(No va dentro de un #if DEBUG porque Kotlin no tiene esa marca; no importa, por lo que dice el comentario.) Tercero, dos retoques en funciones que ya existen. En la primera línea de getDatabaseBuilder():
// antes
fun getDatabaseBuilder(): RoomDatabase.Builder<PokedexDatabase> {
val documentos: NSURL = NSFileManager.defaultManager.URLForDirectory(
// después
fun getDatabaseBuilder(): RoomDatabase.Builder<PokedexDatabase> {
// En un test de interfaz, la base vive en memoria: empieza vacía cada vez
// que se abre la app, y tus favoritos (los del fichero) no se tocan.
if (enUnTestDeInterfaz) return Room.inMemoryDatabaseBuilder<PokedexDatabase>()
val documentos: NSURL = NSFileManager.defaultManager.URLForDirectory(Y en la última línea de settingsPath():
// antes
return requireNotNull(documentos.path) + "/" + SETTINGS_FILE_NAME
// después
// En un test de interfaz, los ajustes van a otro fichero: el test puede
// poner el tema «Oscuro» sin cambiar el tuyo.
val nombre = if (enUnTestDeInterfaz) "pruebas-$SETTINGS_FILE_NAME" else SETTINGS_FILE_NAME
return requireNotNull(documentos.path) + "/" + nombreNo hace falta ningún import más: Room ya estaba. Pulsa Cmd + B: debe compilar (Xcode recompila también el framework de Kotlin). Esa app sigue siendo la de antes cuando la lanzas tú: sin -uiTests, nada de esto cambia.
Paso 3 · Los cuatro tests
Qué se prueba aquí es lo que Robolectric no puede: la app entera, en un simulador. Y ninguno depende de PokeAPI: un test que necesita red falla el día que la red va lenta, y entonces deja de creerse.
Abre PokedexUITests.swift (el del target nuevo), selecciona todo su contenido (Cmd + A) y escribe esto. Primero, la clase y su setUp:
import XCTest
// Las pruebas de interfaz arrancan la app de verdad en el simulador y la
// manejan desde fuera, como una persona. `XCTestCase` es la clase base de XCTest.
final class PokedexUITests: XCTestCase {
// La app que vamos a manejar. El `!` del tipo promete a Swift que, cuando
// se use, ya tendrá valor: se lo damos en `setUp`, antes de cada test.
private var app: XCUIApplication!
// Se ejecuta antes de CADA test: app nueva, recién lanzada. Los tests son
// los métodos que empiezan por `test`: así los reconoce Xcode.
override func setUp() {
// Si una comprobación falla, para ese test en vez de seguir a ciegas.
continueAfterFailure = false
app = XCUIApplication()
// El interruptor del paso 2: la app arranca con la sesión de mentira.
app.launchArguments = ["-uiTests"]
app.launch()
}Los dos primeros tests, que solo miran:
func testLaAppAbreConLasTresPestanas() {
// `waitForExistence` espera hasta 20 segundos a que aparezca: la app
// tarda en arrancar y un `exists` inmediato fallaría.
let pestanas = app.tabBars.firstMatch
XCTAssertTrue(pestanas.waitForExistence(timeout: 20))
// Cada pestaña es un botón de la barra, con el texto de su `Label`.
XCTAssertTrue(pestanas.buttons["Pokédex"].exists)
XCTAssertTrue(pestanas.buttons["Favoritos"].exists)
XCTAssertTrue(pestanas.buttons["Ajustes"].exists)
}
func testFavoritosVacioExplicaQueHacer() {
app.tabBars.buttons["Favoritos"].tap()
// Sin favoritos, la pantalla explica qué hacer. Es un texto estático.
let explicacion = app.staticTexts["Toca el corazón de cualquier Pokémon para guardarlo aquí."]
XCTAssertTrue(explicacion.waitForExistence(timeout: 10))
}Y el tercero, el del selector de tema. Escríbelo tal cual, con un fallo que verás en el paso 4: busca el selector por su nombre, «Tema».
func testAjustesMuestraElTemaYLoCambia() {
app.tabBars.buttons["Ajustes"].tap()
let tema = app.buttons["Tema"]
XCTAssertTrue(tema.waitForExistence(timeout: 20))
tema.tap()
// El `Picker` de un `Form` se abre como lista de opciones.
let oscuro = app.buttons["Oscuro"]
XCTAssertTrue(oscuro.waitForExistence(timeout: 5))
oscuro.tap()
}
}Provócalo. Con el esquema de tu app y un simulador elegidos arriba, pulsa Cmd + U (Product → Test). Arranca el simulador y la app se maneja sola. Los dos primeros pasan y el tercero falla, con un mensaje que no dice nada:
PokedexUITests.swift:NN: error: XCTAssertTrue failedUn experimento opcional, solo si tienes tus claves de Supabase puestas en Config.xcconfig: quita la línea app.launchArguments = ["-uiTests"] y vuelve a ejecutar con Cmd + U. Fallan los tres. El de las pestañas, con ese mismo XCTAssertTrue failed (espera la barra y nunca llega); los otros dos, con un mensaje como «Failed to tap "Favoritos" Button: No matches found for Descendants matching type TabBar» (en el del tema dirá "Ajustes" en vez de "Favoritos"). La razón es la misma: la app abre pidiendo entrar y no tiene pestañas. Vuelve a poner la línea. Si no has puesto claves no lo verás: la app ya abría directa.
Paso 4 · Mira lo que ve el test
En vez de adivinar, pídele al test que imprima el árbol de accesibilidad, la lista de todo lo que un lector de pantalla (o un test) puede ver. Añade esta línea justo antes del XCTAssertTrue(tema.waitForExistence…) y vuelve a ejecutar:
print(app.debugDescription)Esa salida se lee en Xcode: abre el Report navigator (Cmd + 9, el último icono de la barra izquierda), pulsa la primera fila de la lista (la ejecución más reciente) y, en el registro que se abre, busca «Tema» con Cmd + F. (También se puede sacar desde la Terminal, pero esa orden pide el identificador del simulador, que aprenderás a conseguir en el paso 7. Por ahora, Xcode basta.) Verás algo así:
StaticText, 0x103d13d40, {{32.0, 224.2}, {41.3, 20.3}}, label: 'Tema'
Button, 0x103d2b970, {{32.0, 217.3}, {338.0, 34.3}}, label: 'Tema, Lo que diga el sistema'El Picker de un Form no se llama «Tema». SwiftUI compone su etiqueta con el valor actual («Tema, Lo que diga el sistema»), y buscar «Tema» como botón no encuentra nada. Borra la sonda.
La solución es un identificador de accesibilidad: un nombre interno que solo ven las herramientas, no el usuario. Es el equivalente del testTag de Compose que usaste en Probar la pantalla de entrada en Android.
(El test no ve píxeles: ve ese árbol, el mismo que usa VoiceOver. Por eso lo que ayuda a una persona con lector de pantalla ayuda también al test.) Abre SettingsView.swift (iosApp → iosApp → Settings) y, justo después de la llave que cierra el Picker (la línea anterior a } header: {), añade el modificador:
}
// Un identificador, no la etiqueta. La etiqueta del Picker
// lleva el valor actual y además cambia al traducir la app;
// el identificador no cambia nunca.
.accessibilityIdentifier("ajustes.tema")
} header: {Hay una segunda ventaja: más adelante traducirás la app, y entonces el botón «Ajustes» pasará a llamarse «Settings»; un test que lo busque por su texto dejará de encontrarlo, y el identificador seguirá igual. Y en el test, cambia app.buttons["Tema"] por app.buttons["ajustes.tema"], y añade al final del método la comprobación de que el cambio se aplicó:
let tema = app.buttons["ajustes.tema"]
// … igual hasta oscuro.tap() …
// La etiqueta del selector lleva el valor dentro: «Tema, Oscuro».
XCTAssertTrue(app.buttons["ajustes.tema"].label.contains("Oscuro"))Ejecuta de nuevo (Cmd + U): pasan los tres. Antes de seguir, borra la sonda (print(app.debugDescription)) si todavía está.
Paso 5 · El test que solo se puede hacer aquí
Añádelo dentro de la clase, antes de su última }. Mata la app y vuelve a abrirla: algo que ni Robolectric ni un test de Compose pueden hacer, y la mejor prueba de que el ajuste se guarda de verdad.
// El ajuste tiene que sobrevivir a cerrar la app: si no, DataStore no está
// guardando y nadie se entera hasta que un usuario lo cuenta.
func testElTemaSobreviveAReabrirLaApp() {
app.tabBars.buttons["Ajustes"].tap()
let tema = app.buttons["ajustes.tema"]
XCTAssertTrue(tema.waitForExistence(timeout: 20))
tema.tap()
// Espera a que la lista de opciones aparezca antes de tocar «Oscuro».
let oscuro = app.buttons["Oscuro"]
XCTAssertTrue(oscuro.waitForExistence(timeout: 5))
oscuro.tap()
// Mata el proceso y vuelve a arrancarlo. Los argumentos de lanzamiento
// se conservan, así que la app arranca con las mismas dobles.
app.terminate()
app.launch()
app.tabBars.buttons["Ajustes"].tap()
let vuelta = app.buttons["ajustes.tema"]
XCTAssertTrue(vuelta.waitForExistence(timeout: 20))
XCTAssertTrue(vuelta.label.contains("Oscuro"))
}Paso 6 · Todo junto, desde Xcode
Cmd + U ejecuta los cuatro. Mira el Test navigator (Cmd + 6, el icono del rombo): cuatro tests con su marca verde. Mientras corren, el simulador muestra la app moviéndose sola.
Provócalo tú. Se ve en dos minutos. Antes de empezar, haz una copia de seguridad: en el Project navigator, clic derecho sobre PokedexUITests.swift → Show in Finder, y en el Finder copia el fichero al Escritorio (arrástralo con la tecla Option pulsada, o Cmd + C y Cmd + V). Añade dentro de la clase un quinto test que compruebe que el tema empieza en «Lo que diga el sistema»:
func testTemaInicialEsElDelSistema() {
app.tabBars.buttons["Ajustes"].tap()
let tema = app.buttons["ajustes.tema"]
XCTAssertTrue(tema.waitForExistence(timeout: 20))
XCTAssertTrue(tema.label.contains("Lo que diga el sistema"))
}Ejecuta los cinco con Cmd + U: el nuevo falla. El orden en que corren no es algo con lo que debas contar, pero aquí suele tocarle antes a testElTemaSobreviveAReabrirLaApp, que deja el tema en «Oscuro» en el fichero de ajustes de las pruebas, y ese fichero lo recuerda. Un test que depende de lo que corrió antes es frágil: pasa o falla según el orden. La solución sería que cada test que cambie el tema lo deje como estaba al terminar (tocar el selector y elegir «Lo que diga el sistema»).
La diferencia con Android: allí la regla de Compose monta la pantalla desde cero en cada test (createComposeRule). En iOS el test maneja una app real, con su propio proceso y su propio almacenamiento, y su estado persiste mientras no lo borres. Cuando acabes, borra el quinto test: pega encima la copia del Escritorio (o simplemente elimina el método a mano). Los cuatro tests de la lección no lo necesitan. Después, para limpiar el «Oscuro» que recuerda el fichero de pruebas, borra la app del simulador (Remove App).
Paso 7 · Y desde la Terminal
Es la misma ejecución sin abrir Xcode. Primero el identificador del simulador (el <UDID>, un código largo con guiones que ya usaste en Ejecutar en iOS). Esta orden lista los simuladores instalados y su identificador:
xcrun simctl list devices availableY la ejecución, con <UDID> sustituido por el del simulador que quieras. -destination dice en qué simulador correr, y -derivedDataPath build/dd pide que lo que se compile se guarde en la carpeta build/dd de tu proyecto, en vez de en la carpeta oculta de Xcode:
xcodebuild test -project iosApp/iosApp.xcodeproj -scheme <tu esquema> \
-sdk iphonesimulator -destination 'id=<UDID>' -derivedDataPath build/ddSi quieres la sonda del paso 4 también desde aquí, añade al final de esta orden -only-testing:PokedexUITests/PokedexUITests/testAjustesMuestraElTemaYLoCambia | grep "label: 'Tema": el primer trozo ejecuta solo ese test, y la barra | pasa toda la salida a grep, que deja únicamente las líneas que contienen label: 'Tema.
Al final debe salir Executed 4 tests, with 0 failures y ** TEST SUCCEEDED **. Tarda de cuarenta segundos a un minuto con el simulador ya arrancado.
Punto de control
xcodebuild -list -project iosApp/iosApp.xcodeprojenseña dos targets: el de tu app yPokedexUITests.Cmd + Udeja en el Test navigator (Cmd + 6) cuatro marcas verdes; yxcodebuild test, en la Terminal, termina conExecuted 4 tests, with 0 failuresy** TEST SUCCEEDED **. Cuatro, no cero.- Tu esquema aparece con Shared marcado en Manage Schemes y con
PokedexUITestsen su pestaña Test. - El selector de tema tiene
accessibilityIdentifier("ajustes.tema")y el test lo busca por ahí. - La app arranca con sesión sin importar si tus claves de Supabase están puestas:
-uiTestsactiva las dobles. Y con ese argumento, Favoritos empieza vacío y el tema es el de partida, aunque tú hayas marcado favoritos o cambiado el tema. - La app que lanzas tú desde Run sigue pidiendo entrar (si tienes claves) o abriendo directo (si no).
- Lo demás no cambia: las tres tareas del núcleo (
:shared:jvmTest :shared:testAndroidHostTest :shared:iosSimulatorArm64Test, sin filtro) siguen en 127, 89 y 92 yandroidAppen 13 (androidApp/build/reports/tests/testDebugUnitTest/index.html). Los cuatro de iOS son aparte: los cuenta Xcode.
Comprueba lo que entendiste
7 preguntas · necesitas 5 para completar la lección
1.Un target de pruebas creado pero no incluido en el esquema, ¿qué hace
xcodebuild test?2.¿Por qué el
Pickerde unFormno se encuentra buscando «Tema»?3.Además de resolver ese problema, ¿qué otra ventaja tiene usar identificadores de accesibilidad?
4.¿Por qué ninguno de estos tests depende de PokeAPI?
5.¿Qué comprobación solo se puede hacer en este lado y no con Robolectric?
6.¿Para qué sirve el argumento de lanzamiento
-uiTests?7.¿Qué pasa con
DoblesParaUITests.swiften la versión que subes a la App Store?