Caso práctico: CI/CD y pruebas unitarias para tres apps bancarias de marca blanca en diez semanas
Cómo Mobian Studio llevó un SDK fintech y tres apps bancarias de marca blanca de builds manuales a un solo CI/CD y casi triplicó las pruebas unitarias del SDK.
La mayoría de los artículos sobre CI/CD describen cómo debería ser un pipeline. Este describe un pipeline que construimos de verdad: para una plataforma fintech cuyo SDK de Android da vida a tres apps bancarias de marca blanca.
Cuando empezamos, ninguno de los seis repositorios tenía un CI que funcionara. Las builds se hacían a mano, los números de versión se incrementaban a mano en commits y una biblioteca solo existía en el equipo de un desarrollador. Diez semanas después, el SDK y tres apps cliente compartían un único sistema de CI/CD, cada build para testers podía reproducirse a partir de una etiqueta de Git y el conjunto de pruebas unitarias del SDK casi se había triplicado.
En este artículo repasaremos el punto de partida, el diseño del pipeline, la estrategia de pruebas, los resultados con fechas y el trabajo que sigue pendiente.
El punto de partida
El cliente es una plataforma fintech que está detrás de varias apps bancarias y de pagos. La familia de productos incluye:
-
Un SDK de Android de unos 29 módulos de Gradle que convierte el JSON del backend en pantallas nativas y que también gestiona pagos, biometría y escaneo de tarjetas.
-
Tres apps bancarias de marca blanca construidas sobre ese SDK, cada una con su propio modelo de ramas y de publicación.
-
Una biblioteca de red y un conjunto de pruebas end-to-end a cargo de QA.
En julio y agosto de 2026, la situación era esta:
-
En el repositorio del SDK había plantillas de CI antiguas, pero no se ejecutaba nada.
-
Los version codes se incrementaban a mano: unos 95 commits de este tipo en una app desde 2025, y 28 en otra.
-
No se podía reproducir una build a partir de un checkout limpio, porque una biblioteca solo existía en la caché local de Maven de un desarrollador.
-
Algunos módulos de pruebas del SDK ni siquiera compilaban: la primera ejecución del CI lo puso de manifiesto.
-
El SDK tenía 1122 pruebas ejecutadas y un 8,1 % de cobertura total; la app principal tenía 56 pruebas.
📌 Idea clave:
Nadie del equipo estaba haciendo nada mal. Sin un pipeline, el conocimiento sobre las publicaciones vive sin más en la cabeza y en los portátiles de las personas, y en un sector bancario regulado eso es un riesgo en sí mismo.
Las restricciones
El pipeline tenía que adaptarse a la infraestructura real del cliente, no a una ideal:
-
GitLab autoalojado, migrado desde gitlab.com en julio de 2026.
-
Un único runner de macOS compartido, sin Docker, que también usaban otros proyectos de la empresa y cuyo espacio de trabajo persiste entre jobs.
-
Builds que abarcan dos repositorios: cada build de una app combina una rama de la app con una rama del SDK que puede no estar fusionada todavía.
-
Un sector bancario regulado, que empuja hacia builds reproducibles, firma de publicación fuera de Git y análisis de dependencias.
Lo que construimos: un pipeline para tres apps
1. Un formulario web hasta los testers
Una build para testers se inicia desde un único formulario de GitLab: se elige la rama de la app, la rama del SDK, el tipo de build y los grupos de testers. Después, el pipeline ejecuta seis etapas:
-
Preparar: resuelve la rama del SDK a un único commit, para que todos los jobs posteriores usen exactamente el mismo código del SDK.
-
Probar: pruebas unitarias de la app y del SDK, cobertura y un análisis de dependencias.
-
Compilar: un APK firmado (más el AAB para Play Store en las publicaciones), con las credenciales de firma tomadas de variables de CI.
-
Distribuir: Firebase App Distribution a los grupos de testers seleccionados.
-
Notificar: un comentario en Jira en cada ticket incluido, con el enlace a la build y la cobertura, y el paso a “Ready for QA”.
-
Etiquetar: la misma etiqueta en el commit de la app y en el commit del SDK.
2. Cada build para testers es reproducible
Como la etiqueta se aplica en ambos repositorios después de la distribución, una etiqueta significa “esta build exacta llegó a los testers”. Hacer checkout de la etiqueta en ambos repositorios reproduce la build. Antes, eso no era posible en absoluto.
3. Una plantilla, adaptaciones mecánicas
Las tres apps ejecutan el mismo pipeline de seis etapas. Solo cambian unas pocas constantes (etiqueta de Jira, prefijo de las etiquetas de Git, grupos de testers), que viven en un único job de plantilla oculto. La segunda y la tercera app se adaptaron en días, a pesar de que una de ellas usa un modelo de ramas de publicación y tenía siete bloqueos distintos que resolver.
4. Controles de calidad donde la gente ya trabaja
-
Resultados de las pruebas en la pestaña Tests de la merge request.
-
Un badge de cobertura de la lógica en el widget de la merge request y marcas de cobertura en las líneas modificadas.
-
SonarQube en la rama principal.
-
OWASP Dependency-Check con un SBOM CycloneDX en la app principal.
5. Un runner compartido tratado como no fiable
Los secretos viven solo en variables de CI, no se guarda nada en el runner, cada job clona el SDK en su propio espacio de trabajo (después de que, en una ocasión, jobs paralelos borraran el checkout unos de otros) y las builds de publicación firmadas siempre evitan la caché de compilación.
Lo que construimos: una estrategia de pruebas que empieza por la arquitectura
La cobertura era la vara de medir, pero el verdadero trabajo consistió en hacer que el código fuera testeable. Cuatro decisiones dieron forma al resultado.
1. Medir el código que importa
Definimos un conjunto de lógica: dominio, datos, utilidades que no dependen de Android, los motores de peticiones y de eventos, y los ViewModels base. El código de UI, el cableado de DI y el código generado se siguen reportando, pero no tienen objetivo. Solo con esto, el punto de partida honesto pasó del 8 % (todo el código) a alrededor del 24 % (la lógica).
2. Arquitectura con efecto trinquete
Siete reglas de arquitectura automatizadas se ejecutan como pruebas unitarias: por ejemplo, nada de tipos de Android en el código de lógica y nada de inyección de dependencias global. Las infracciones actuales quedan congeladas en un archivo de referencia: una infracción nueva hace fallar la build, y también una corregida, hasta que se elimina del archivo de referencia. La deuda solo puede reducirse.
3. Primero las costuras, después las pruebas
Cada dependencia de Android o de terceros (analítica, permisos, biometría, caché de pantallas, firma de peticiones) recibió una pequeña interfaz en la capa de dominio y un adaptador fuera del control de cobertura. A partir de ahí, la lógica se ejecuta en la JVM pura en milisegundos y escribir pruebas pasa a ser barato.
4. Fijar el comportamiento, no corregirlo en silencio
Las pruebas de la capa de datos registran lo que el código hace hoy. Cada comportamiento dudoso se convierte en una pregunta para producto o backend en lugar de en un cambio silencioso: 76 preguntas abiertas hasta ahora. Cuando se toma una decisión, la prueba que fija ese comportamiento falla a propósito y se actualiza junto con la corrección.
Los resultados
Todas las cifras siguientes son a 5 de octubre de 2026, obtenidas de la API de pipelines de GitLab o contadas en los repositorios.
| Métrica | Antes | Después |
| Repositorios con un CI que funciona | 0 de 6 | 4 de 6 (SDK + 3 apps) |
| Del merge a los testers | Builds manuales, incrementos de versión manuales | Unos 6–8 minutos, totalmente automatizado |
| Builds para testers a través del pipeline | — | 101 desde el 8 de septiembre |
| Pipelines ejecutados | — | Unos 530 |
| Pruebas unitarias del SDK | 1141 (11 de septiembre) | 3246 (×2,8) |
| Cobertura de la lógica del SDK | Alrededor del 24 % | 70,9 % |
| Las pruebas fallidas bloquean los merges | No | Sí, en el SDK y en la app principal |
| Imports de Android en el código de lógica | 298 | 192 (−36 %) |
| Reproducir una build para testers | Imposible | Hacer checkout de una etiqueta en ambos repositorios |
El tramo más rápido: la cobertura de la lógica pasó del 30,7 % al 70,9 % en siete días (del 28 de septiembre al 5 de octubre), cuando se cubrió toda la capa de datos a lo largo de 13 tickets. Ese salto solo fue posible porque las costuras de la arquitectura ya estaban en su sitio.
Las pruebas también se amortizaron antes de alcanzar el objetivo de cobertura. Al escribir las pruebas de fijación salieron a la luz cierres inesperados ante datos mal formados del servidor, carencias de compatibilidad futura en versiones antiguas de la app y un bucle de reintentos de renovación del token demasiado agresivo; todo ello ya está registrado y priorizado.
Lo que sigue pendiente
Preferimos publicar las carencias junto con los resultados:
-
Todavía no hay un umbral mínimo de cobertura: los merges se bloquean cuando fallan las pruebas, no cuando baja la cobertura. Los hitos del 40 % y el 60 % ya se han superado; el del 80 % sigue por delante.
-
Dos apps siguen teniendo pruebas no bloqueantes y no han adoptado el kit de pruebas compartido.
-
Dos repositorios no tienen CI, y las pruebas end-to-end todavía se ejecutan en una máquina local.
-
Un único runner es un punto único de fallo: durante dos días, los jobs esperaron hasta agotar el tiempo de espera mientras no estaba disponible. Un segundo runner o un grupo con autoescalado es el siguiente paso.
El método que ahora aplicamos con otros clientes
-
Fijar las dependencias compartidas y etiquetar cada repositorio que interviene en una build, para poder reproducir cualquier build para testers.
-
Usar una plantilla de pipeline por familia de productos, con las diferencias reducidas a un puñado de constantes.
-
Cerrar el ciclo donde la gente ya trabaja: el enlace a la build y la cobertura van al ticket de Jira y a la merge request.
-
Redefinir la cobertura antes de perseguirla, para que la cifra sea honesta y el objetivo, alcanzable.
-
Avanzar con efecto trinquete, no de golpe: congelar las infracciones actuales y hacer fallar cualquier infracción nueva.
-
Tratar un runner compartido como no fiable: secretos en variables de CI y builds de publicación nunca en caché.
Conclusión
En diez semanas, una familia de productos fintech pasó de APKs compilados a mano a un único sistema de CI/CD: unos 530 pipelines, 101 builds automatizadas para testers, del merge a los testers en minutos y un conjunto de pruebas del SDK que creció de 1141 a 3246 pruebas, con una cobertura de la lógica del 70,9 %. Nada de esto requirió un nuevo proveedor de CI; solo un pipeline diseñado en torno a la infraestructura real del cliente y una arquitectura que permite hacer pruebas.
En Mobian, este es el tipo de trabajo que preferimos mostrar en lugar de describir. Si sus publicaciones todavía dependen del portátil de una sola persona, podemos empezar con una auditoría de su proceso actual de compilación y publicación y decirle con franqueza qué merece la pena automatizar primero.
Preguntas frecuentes
¿Cuánto tiempo llevó implantar el CI/CD?
Unas diez semanas, de finales de julio a principios de octubre de 2026. El primer pipeline del SDK en verde se ejecutó al cabo de aproximadamente un mes. Las builds para testers a través del pipeline empezaron a principios de septiembre. A partir de ahí, trasladar el mismo pipeline a las demás apps bancarias llevó días, no semanas.
¿Por qué medir la cobertura de la lógica en lugar de la cobertura total?
La cobertura total cuenta el código de UI, el cableado de inyección de dependencias y el código generado, donde las pruebas unitarias aportan poco valor. La cobertura de la lógica solo cuenta el código de dominio, de datos y de los motores, donde los errores cuestan dinero. Así la cifra es honesta y el objetivo, alcanzable. En este proyecto, el punto de partida era un 8,1 % de cobertura total, pero alrededor de un 24 % de cobertura de la lógica, antes de escribir una sola prueba nueva.
¿El pipeline se ejecutaba en un CI en la nube?
No. El cliente tiene un GitLab autoalojado con un único runner de macOS compartido, así que el pipeline se diseñó para ese entorno: cada job obtiene su propio checkout del SDK, los secretos viven solo en variables de CI y las builds de publicación firmadas nunca se toman de la caché de compilación compartida.
¿Está terminado el trabajo?
No del todo, y preferimos decirlo. El umbral mínimo de cobertura (bloquear un merge cuando la cobertura baja) aún no está implantado, las pruebas de dos de las apps todavía no son bloqueantes y el runner único sigue siendo un riesgo de capacidad y disponibilidad. El hito del 80 % de cobertura de la lógica todavía está por alcanzar.
¿Puede funcionar este enfoque en un producto iOS o multiplataforma?
Los principios son trasladables: fijar las dependencias compartidas, etiquetar cada build para testers, medir el código que importa y añadir pruebas cuando la arquitectura lo permita. Este proyecto en concreto era solo de Android, así que en iOS las herramientas cambian (Xcode, Swift Package Manager, XCTest), pero el método es el mismo.