Las pruebas unitarias son un método de verificación de software en el que se prueba la corrección de módulos individuales o funciones de código de forma aislada del resto del sistema. Según Martin Fowler, 2026, las pruebas unitarias son la base de CI/CD y la refactorización, proporcionando retroalimentación rápida sobre el funcionamiento del código. Las pruebas modulares ayudan a detectar errores en las primeras etapas del desarrollo, reduciendo el costo de corregirlos decenas de veces.
Puntos clave
Las pruebas unitarias son el proceso de verificar unidades individuales del código fuente (funciones, métodos, clases) de forma aislada del resto del programa. Cada prueba ejecuta un escenario de uso específico del módulo y comprueba que el resultado coincide con el esperado. Las pruebas unitarias se escriben en el mismo lenguaje de programación que el código principal y se ejecutan automáticamente en el entorno de desarrollo o en un pipeline de CI/CD. A diferencia de las pruebas de integración, las pruebas unitarias no interactúan con bases de datos reales, sistemas de archivos o servicios de red.
El objetivo principal es la retroalimentación rápida sobre la corrección del código después de los cambios. Si un desarrollador refactoriza un método, el conjunto de pruebas unitarias confirma que el comportamiento no se ha roto. Según Google Testing Blog (2025), los proyectos con cobertura de pruebas unitarias superior al 60% tienen 2.5 veces menos incidentes en producción. Beneficios adicionales: documentación del código (las pruebas muestran cómo usar la API), simplificación de la refactorización (se puede cambiar la implementación manteniendo el comportamiento) y diagnóstico rápido de regresiones.
No toda prueba automatizada es una prueba unitaria. Criterios: se prueba un solo módulo (clase o función), las dependencias externas se reemplazan con mocks o stubs, la prueba se ejecuta en milisegundos y no requiere iniciar un servidor o base de datos. Una prueba que accede a una base de datos real es una prueba de integración. Una prueba que abre un navegador es una prueba E2E. Comprender los límites entre los tipos de pruebas es importante para distribuir correctamente los esfuerzos en la pirámide de pruebas.
Las pruebas unitarias de calidad siguen los principios FIRST formulados por Robert C. Martin. Cada prueba debe ser Fast (rápida — milisegundos), Isolated (aislada — no depende de otras pruebas), Repeatable (reproducible — mismo resultado en cualquier máquina), Self-validating (autovalidable — resultado "passed" o "failed", sin verificación manual) y Timely (oportuna — escrita antes o simultáneamente con el código). Violar cualquier principio reduce el valor de la prueba.
Una plantilla estándar para escribir pruebas unitarias. Arrange — preparación de datos y dependencias: crear objetos, configurar mocks, establecer parámetros de entrada. Act — ejecución de la acción probada: llamar a un método o función. Assert — verificación del resultado: comparar el valor real con el esperado. La división en tres bloques hace que la prueba sea legible y comprensible. Si el bloque Assert requiere lógica compleja, es probable que la prueba esté verificando demasiado a la vez.
// Ejemplo de prueba unitaria con patrón AAA en Kotlin con JUnit 5
class CalculatorTest {
private lateinit var calculator: Calculator
@BeforeEach
fun setUp() {
// ARRANGE — creamos el objeto a probar
calculator = Calculator()
}
@Test
fun addition_shouldReturnCorrectSum() {
// ACT — ejecutamos la acción
val result = calculator.add(2, 3)
// ASSERT — verificamos el resultado
Assertions.assertEquals(5, result)
}
}
El nombre de la prueba debe describir qué se verifica y qué resultado se espera. Formato: [methodName]_[scenario]_[expectedResult]. Ejemplo: calculateTotal_whenDiscountApplied_shouldReturnDiscountedTotal. Un buen nombre de prueba reemplaza un comentario y, al fallar, indica inmediatamente qué funcionalidad está dañada. Evite nombres como test1, checkSomething o verify — no aportan información y dificultan el diagnóstico.
Para aislar el módulo probado de las dependencias externas se utilizan dobles de prueba (test doubles). Tipos principales: mocks — verifican que un método específico fue llamado con los parámetros esperados; stubs — devuelven valores predefinidos al llamar a un método; fakes — implementaciones simplificadas de componentes reales (por ejemplo, InMemoryUserRepository en lugar de UserRepository que trabaja con una BD). La elección depende de lo que se necesita verificar: estado (stub) o interacción (mock).
| Doble | Qué verifica | Ejemplo |
|---|---|---|
| Mock | Llamada al método con parámetros correctos | userRepository.save(user) fue llamado exactamente 1 vez |
| Stub | Valor de retorno | repository.findById(1) devuelve User(id=1, name="Test") |
| Fake | Lógica mediante implementación simplificada | InMemoryMapUserRepository con HashMap en lugar de BD |
| Spy | Mocking parcial de un objeto real | spy(repo).when(findById).thenReturn(user) |
Mockito es el framework de mocking más popular para Java y Kotlin. Permite crear mocks mediante mock(), configurar valores de retorno mediante when().thenReturn() y verificar llamadas mediante verify(). Las versiones modernas de Mockito (5.x) admiten mocks estáticos (mockStatic) y sintaxis simplificada a través de BDDMockito (given-willReturn). Una regla importante: no mockees lo que no es tuyo — no crees mocks para objetos de valor y bibliotecas estándar.
// Ejemplo de prueba unitaria con Mockito en Kotlin
class OrderServiceTest {
@Mock
private lateinit var paymentGateway: PaymentGateway
@Mock
private lateinit var userRepository: UserRepository
private lateinit var orderService: OrderService
@BeforeEach
fun init() {
MockitoAnnotations.openMocks(this)
orderService = OrderService(paymentGateway, userRepository)
}
@Test
fun processOrder_whenPaymentFails_shouldThrowException() {
// given
val user = User(id = 1, balance = 100.0)
val order = Order(amount = 200.0)
Mockito.`when`(paymentGateway.charge(any())).thenReturn(false)
Mockito.`when`(userRepository.findById(1)).thenReturn(user)
// when & then
assert Throws<PaymentException> {
orderService.processOrder(user.id, order)
}
// verify
Mockito.verify(paymentGateway).charge(any())
}
}
TDD (Test-Driven Development) es una metodología en la que la prueba se escribe antes de la implementación del código. El ciclo "Red-Green-Refactor": escribir una prueba que falla (Red), escribir el código mínimo para pasar la prueba (Green), mejorar el código sin cambiar el comportamiento (Refactor). TDD garantiza que todo el código esté cubierto por pruebas (cobertura = 100% para la funcionalidad implementada) y que el código sea comprobable — si el código es difícil de probar, la arquitectura necesita mejorar.
Según una investigación de IBM (2006-2026, estudio longitudinal), los equipos que usan TDD tienen entre un 40 y un 80% menos de defectos en producción en comparación con los equipos que escriben pruebas después del código. TDD también mejora la arquitectura: el desarrollador se ve obligado a pensar en el diseño de la API antes de la implementación, lo que conduce a un acoplamiento débil (loose coupling) y una alta cohesión (high cohesion). Un efecto adicional es la documentación con código vivo: las pruebas sirven como especificación del comportamiento del módulo, que siempre está actualizada.
TDD no siempre es óptimo. Los componentes de UI son difíciles de probar de forma aislada — para ellos son más efectivas las pruebas snapshot o las pruebas de regresión visual (Percy, Chromatic). La creación de prototipos y la investigación (spike solutions) no requieren pruebas. El código legacy sin pruebas es difícil de cubrir mediante TDD — aquí primero se necesitan characterization tests (pruebas que capturan el comportamiento actual antes de la refactorización). En estos casos, TDD no se abandona por completo sino que se adapta — se escriben pruebas para la funcionalidad modificada, no para todo el código legacy.
El desarrollo móvil tiene sus particularidades: la lógica de negocio a menudo se mezcla con el código de UI (Activity, ViewController, ViewModel), lo que dificulta las pruebas unitarias. La mejor práctica es Vistas delgadas, ViewModels gruesos: extraer toda la lógica de los componentes de UI a clases separadas (UseCase, Repository, ViewModel) que sean fáciles de probar sin emulador. Android e iOS tienen frameworks nativos de pruebas unitarias que se ejecutan en JVM/Native sin necesidad de iniciar un dispositivo.
Las pruebas unitarias de Android se ejecutan en una JVM local sin emulador, lo que proporciona velocidad de ejecución — una prueba típica tarda menos de 100 ms. JUnit 5 es el runner principal. Para pruebas de ViewModel, use kotlinx-coroutines-test para probar corrutinas y Turbine para probar StateFlow. Robolectric permite probar componentes dependientes de Android (Context, Resources) sin emulador, cargando shadow-classes. Para pruebas de Compose, use Compose UI Test — pero estas son pruebas de UI, no unitarias.
Las pruebas unitarias de iOS se escriben en Swift con XCTest (integrado en Xcode). Quick + Nimble son frameworks BDD para pruebas más legibles (describe/context/it). Para mocking, use Cuckoo (generación de mocks) o SwiftyMocky. Swift admite protocolos e inyección de dependencias, lo que facilita el reemplazo de dependencias. Punto clave: las pruebas unitarias de iOS se ejecutan en el simulador de macOS, no en un dispositivo real. Las pruebas que requieren funciones de hardware (cámara, Bluetooth) son pruebas de integración.
Las pruebas unitarias de Flutter usan el paquete flutter_test y se ejecutan en Dart VM sin emulador. Para mocking, use el paquete mockito con generación de código (build_runner). Las pruebas de widgets (en el mismo paquete) prueban widgets individuales pero requieren renderizado y son más lentas — úselas solo para verificar la lógica de UI. La lógica pura de Dart (modelos, repositorios, blocs) se prueba como pruebas Dart regulares sin importar flutter_test.
// Ejemplo de prueba unitaria en Flutter con mockito
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([ApiClient])
import 'user_repository_test.mocks.dart';
void main() {
late MockApiClient mockApi;
late UserRepository repository;
setUp(() {
mockApi = MockApiClient();
repository = UserRepository(mockApi);
});
test('fetchUser returns user when API succeeds', () async {
// Arrange
final expectedUser = User(id: 1, name: 'Test');
when(mockApi.getUser(1))
.thenAnswer((_) async => expectedUser);
// Act
final result = await repository.fetchUser(1);
// Assert
expect(result, expectedUser);
verify(mockApi.getUser(1)).called(1);
});
}
Las pruebas unitarias efectivas requieren disciplina. La regla principal: pruebe el comportamiento, no la implementación. La prueba no debe saber cómo está implementado el módulo internamente (qué métodos privados se llaman, en qué orden). Si una prueba está vinculada a la implementación, se rompe en cada refactorización y pierde valor. La prueba verifica el contrato: con entrada X, la salida debe ser Y. La excepción son las pruebas para algoritmos con rendimiento crítico, donde la secuencia de llamadas es importante.
La cobertura del 100% es un objetivo inalcanzable e innecesario. Según Google Testing Blog (2025), el nivel óptimo de cobertura para pruebas unitarias es del 70-80% de las líneas de código. La cobertura del 100% a menudo se logra probando getters, setters y constructores, lo que no aporta valor. Concéntrese en la lógica de negocio crítica: cálculos complejos, validación, manejo de errores, edge-cases. Use JaCoCo (Java), Coverage.py (Python), Istanbul (JS) para la medición y configure un umbral en CI — fallo de compilación con cobertura inferior al 60%.
Las pruebas unitarias son la primera etapa de cualquier pipeline de CI/CD. Se ejecutan en cada push al repositorio, antes de la compilación y el despliegue. El tiempo promedio de ejecución del conjunto de pruebas unitarias no debe exceder los 5 minutos — si es mayor, las pruebas dejan de ser "rápidas" y los desarrolladores dejan de ejecutarlas localmente. Separe las pruebas en rápidas (unitarias) y lentas (integración) y ejecútelas en diferentes etapas del pipeline. Use ejecución en paralelo y fail-fast para acelerar.
Preguntas frecuentes
Una prueba unitaria verifica un solo módulo de forma aislada, reemplazando las dependencias externas con mocks. Una prueba de integración verifica la interacción entre varios componentes reales (BD, API, sistema de archivos). Las pruebas unitarias se ejecutan en milisegundos, las de integración en segundos. En la pirámide de pruebas, las unitarias ocupan el 70%.
La elección depende de la plataforma: JUnit 5 para Java/Kotlin, XCTest para iOS/Swift, pytest para Python, Jest/Vitest para JavaScript/TypeScript, flutter_test para Flutter. Para mocking, use Mockito (Java), Cuckoo (iOS), unittest.mock (Python) o vitest.mock (JS). Todos los frameworks modernos admiten pruebas parametrizadas, assertions integradas y ejecución en paralelo.
Fast — la prueba se ejecuta en milisegundos. Isolated — no depende de otras pruebas ni sistemas externos. Repeatable — da el mismo resultado en cualquier máquina. Self-validating — verifica el resultado automáticamente. Timely — escrita antes o sincrónicamente con el código. Violar aunque sea un principio reduce la efectividad de las pruebas.
Sí, absolutamente. El ViewModel contiene lógica de negocio — manejo de eventos, transformación de datos, gestión de estado. En Android, use kotlinx-coroutines-test para corrutinas y Turbine para probar StateFlow. En iOS, pruebe Combine Publishers o async/await en ViewModel. Las pruebas de ViewModel son pruebas unitarias puras que se ejecutan en JVM/macOS sin emulador.
Las solicitudes de red en pruebas unitarias no se ejecutan — se reemplazan con mocks del cliente HTTP. En Android, use MockWebServer (OkHttp) — inicia un servidor HTTP local, que es preferible a los mocks porque reproduce la interacción real de red. MockWebServer proporciona aislamiento sin perder realismo. Para iOS — OHHTTPStubs o URLProtocol para interceptar y reemplazar respuestas.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también