Que Es Unit Testing?
Unit testing es la practica de probar las partes mas pequenas de una aplicacion de software — funciones individuales, metodos o clases — en completo aislamiento del resto del sistema. Cuando haces unit testing de una funcion, la llamas con entradas especificas y verificas que produce la salida esperada, sin llamadas a base de datos, sin peticiones de red, sin acceso al sistema de archivos y sin dependencia de otros componentes.
Considera una funcion que calcula el costo de envio:
function calculateShipping(weight, distance, isExpress):
if weight <= 0 or distance <= 0:
throw InvalidArgumentError
baseCost = weight * 0.5 + distance * 0.1
if isExpress:
return baseCost * 1.5
return baseCost
Un unit test para esta funcion la llamaria con entradas conocidas y verificaria la salida:
test "envio estandar para 2kg, 100km":
result = calculateShipping(2, 100, false)
assert result == 11.0 // (2 * 0.5) + (100 * 0.1) = 11.0
test "multiplicador de envio express":
result = calculateShipping(2, 100, true)
assert result == 16.5 // 11.0 * 1.5 = 16.5
test "peso negativo lanza error":
assertThrows InvalidArgumentError:
calculateShipping(-1, 100, false)
Estos tests verifican el comportamiento de la funcion sin iniciar un servidor web, conectarse a una base de datos o involucrar otra parte de la aplicacion.
Los Principios FIRST
Los buenos unit tests siguen los principios FIRST, un framework que define lo que separa unit tests efectivos de los problematicos.
Fast (Rapidos)
Los unit tests deben ejecutarse en milisegundos, no segundos. Un desarrollador debe poder ejecutar toda la suite despues de cada cambio de codigo sin dudarlo. Si tu suite de 500 tests toma 10 minutos, los desarrolladores dejaran de ejecutarlos. Si toma 3 segundos, los ejecutaran constantemente.
Que ralentiza los tests: Conexiones a base de datos, I/O de archivos, llamadas de red, sentencias sleep/wait, procedimientos de setup complejos.
Como mantener los tests rapidos: Usa test doubles (mocks, stubs) para eliminar dependencias externas. Prueba logica pura en aislamiento.
Independent (Independientes)
Cada test debe poder ejecutarse por si solo, en cualquier orden, sin depender del resultado de otro test. El Test A no deberia crear datos de los que depende el Test B. El Test C no deberia limpiar estado que el Test D necesita.
Patron malo:
test "crear usuario":
user = createUser("alice@test.com")
globalUser = user // guarda estado para el siguiente test
test "actualizar email de usuario":
globalUser.email = "new@test.com" // depende del test anterior
update(globalUser)
Patron bueno:
test "crear usuario":
user = createUser("alice@test.com")
assert user.email == "alice@test.com"
test "actualizar email de usuario":
user = createUser("bob@test.com") // crea sus propios datos
user.email = "new@test.com"
update(user)
assert user.email == "new@test.com"
Repeatable (Repetibles)
Ejecutar el mismo test 100 veces debe producir el mismo resultado cada vez. Sin aleatoriedad, sin dependencia de tiempo, sin dependencia de sistemas externos.
Si un test pasa el lunes pero falla el martes porque verifica fecha_de_hoy == "lunes", viola la repetibilidad. Si un test a veces falla porque una llamada de red tiene timeout, viola la repetibilidad.
Self-Validating (Auto-validables)
Cada test debe tener un resultado claro de aprobacion/fallo sin interpretacion humana. El test afirma un resultado esperado y el framework reporta si paso o fallo. Un test que imprime salida en la consola para que un humano la revise no es auto-validable.
Timely (Oportunos)
Los unit tests deben escribirse en el momento correcto — idealmente antes o junto con el codigo que prueban, no semanas o meses despues. En TDD (Test-Driven Development), los tests se escriben primero y luego se escribe codigo para hacerlos pasar.
Escribir tests tarde lleva a codigo no testeable — funciones con demasiadas dependencias, efectos secundarios ocultos y acoplamiento fuerte.
Test Doubles: Mocks, Stubs y Fakes
El codigo real rara vez existe en aislamiento. Una funcion puede llamar a una base de datos, enviar un email o consultar una API externa. Los unit tests no pueden usar estas dependencias reales (serian lentos, poco confiables y costosos), por lo que usamos test doubles — objetos sustitutos que reemplazan dependencias reales durante el testing.
Stubs
Un stub proporciona respuestas predeterminadas a llamadas de metodos. No verifica como fue llamado — simplemente retorna lo que le indicas.
// Dependencia real: PaymentGateway.charge(amount) -> boolean
// Stub: siempre retorna true (simula pago exitoso)
stubPaymentGateway.charge = () => return true
test "orden se confirma cuando el pago es exitoso":
order = new Order(items, stubPaymentGateway)
order.checkout()
assert order.status == "confirmed"
Usa stubs cuando necesitas controlar lo que retorna una dependencia pero no te importa como fue llamada.
Mocks
Un mock verifica que ocurrieron interacciones especificas. Registra llamadas a metodos y te permite verificar que el codigo probado llamo metodos especificos con argumentos especificos.
mockEmailService = new Mock(EmailService)
test "confirmacion de orden envia email":
order = new Order(items, mockEmailService)
order.confirm()
// Verifica que el mock fue llamado correctamente
mockEmailService.verify("sendEmail")
.wasCalledOnce()
.withArguments("user@test.com", "Order Confirmed")
Usa mocks cuando verificas comportamiento — que el codigo interactuo con una dependencia de la manera esperada.
Fakes
Un fake es una implementacion funcional que toma atajos. Una base de datos en memoria es un fake — implementa la misma interfaz que la base de datos real pero almacena datos en memoria en lugar de disco.
// Real: PostgresUserRepository (conecta a PostgreSQL)
// Fake: InMemoryUserRepository (almacena en un HashMap)
fakeRepo = new InMemoryUserRepository()
test "encontrar usuario por email":
fakeRepo.save(new User("alice@test.com"))
found = fakeRepo.findByEmail("alice@test.com")
assert found != null
assert found.email == "alice@test.com"
Usa fakes cuando necesitas comportamiento realista de una dependencia pero no puedes usar la real en tests.
Cuando Usar Cada Uno
| Test Double | Usar Cuando | Verifica |
|---|---|---|
| Stub | Necesitas controlar valores de retorno | Nada (solo provee datos) |
| Mock | Necesitas verificar interacciones | Llamadas a metodos, argumentos, conteo |
| Fake | Necesitas comportamiento realista en memoria | Nada (provee comportamiento real) |
Conceptos Basicos de Code Coverage
Code coverage mide cuanto de tu codigo fuente se ejecuta cuando corre la suite de tests. Se expresa como porcentaje:
- Cobertura de lineas: Que porcentaje de lineas se ejecutaron?
- Cobertura de ramas: Que porcentaje de ramas if/else se tomaron?
- Cobertura de funciones: Que porcentaje de funciones se llamaron?
Una funcion con 10 lineas y tests que ejecutan 8 de esas lineas tiene 80% de cobertura de lineas.
La trampa del 80%: Muchos equipos establecen un objetivo de cobertura (usualmente 80%) y lo tratan como puerta de calidad. Pero la cobertura solo mide si el codigo fue ejecutado, no si fue probado correctamente. Puedes lograr 100% de cobertura con tests que no afirman nada:
test "test malo con cobertura completa":
calculateShipping(2, 100, false) // ejecuta el codigo, no afirma nada
Este test logra cobertura pero no detecta ningun bug.
Guias utiles de cobertura:
- Usa cobertura para encontrar codigo no probado, no para demostrar que esta bien probado
- La cobertura de ramas es mas valiosa que la de lineas
- Enfocate en cubrir logica de negocio critica, no codigo utilitario
- 80-90% es un objetivo razonable; 100% usualmente no vale el esfuerzo
Quien Escribe Unit Tests?
Los desarrolladores escriben unit tests. Esta no es una actividad de QA. El desarrollador que escribe la funcion esta mejor posicionado para escribir sus unit tests porque entiende el comportamiento esperado, casos limite y detalles de implementacion.
Los ingenieros QA deberian:
- Revisar la calidad de unit tests durante code reviews
- Identificar escenarios de prueba faltantes que los desarrolladores podrian pasar por alto
- Promover cobertura adecuada de logica de negocio critica
- Entender reportes de unit tests para saber donde existe riesgo de calidad
Ejercicio: Escribe Escenarios de Unit Test para una Calculadora
Considera este modulo de calculadora con cuatro funciones:
function add(a, b):
return a + b
function divide(a, b):
if b == 0:
throw DivisionByZeroError
return a / b
function percentage(value, percent):
if percent < 0 or percent > 100:
throw InvalidPercentageError
return value * (percent / 100)
function compound(principal, rate, years):
if principal < 0 or rate < 0 or years < 0:
throw InvalidArgumentError
return principal * (1 + rate) ^ years
Escribe escenarios de prueba (nombre, entrada, salida esperada) para cada funcion. Cubre:
- Happy path (operacion normal)
- Casos limite (cero, valores frontera)
- Casos de error (entrada invalida)
- Valores especiales (numeros muy grandes, decimales)
Pista
Para cada funcion, piensa: Cual es la entrada valida mas simple? Que pasa en los limites (0, numeros negativos, numeros muy grandes)? Que entradas deberian causar errores? Hay problemas de precision con aritmetica decimal?Solucion
Tests para add(a, b):
add(2, 3)→5— numeros positivos basicosadd(0, 0)→0— valores ceroadd(-3, 5)→2— negativo y positivoadd(-3, -7)→-10— ambos negativosadd(0.1, 0.2)→0.3— precision decimal (cuidado con punto flotante!)add(999999999, 1)→1000000000— numeros grandesadd(MAX_INT, 1)→ verificar comportamiento de overflow
Tests para divide(a, b):
divide(10, 2)→5— division basicadivide(10, 3)→3.333...— resultado no enterodivide(0, 5)→0— numerador cerodivide(10, 0)→ lanzaDivisionByZeroError— division por cerodivide(-10, 2)→-5— numerador negativodivide(10, -2)→-5— denominador negativodivide(-10, -2)→5— ambos negativosdivide(1, 3)→0.333...— verificar precision decimal
Tests para percentage(value, percent):
percentage(200, 50)→100— porcentaje basicopercentage(100, 0)→0— cero por ciento (frontera)percentage(100, 100)→100— 100 por ciento (frontera)percentage(100, -1)→ lanzaInvalidPercentageError— debajo del rangopercentage(100, 101)→ lanzaInvalidPercentageError— encima del rangopercentage(0, 50)→0— valor ceropercentage(99.99, 33.33)→33.326667— precision decimal
Tests para compound(principal, rate, years):
compound(1000, 0.05, 10)→1628.89— interes compuesto basicocompound(1000, 0, 10)→1000— tasa cerocompound(1000, 0.05, 0)→1000— cero anoscompound(0, 0.05, 10)→0— principal cerocompound(-1, 0.05, 10)→ lanzaInvalidArgumentError— principal negativocompound(1000, -0.01, 10)→ lanzaInvalidArgumentError— tasa negativacompound(1000, 0.05, -1)→ lanzaInvalidArgumentError— anos negativoscompound(1000, 1.0, 30)→ numero muy grande — verificar sin overflow
Patrones Avanzados de Unit Testing
Arrange-Act-Assert (AAA)
Estructura cada unit test en tres fases claras:
test "cupon expirado es rechazado":
// Arrange — preparar datos y dependencias
coupon = new Coupon("SAVE10", expiryDate: "2024-01-01")
validator = new CouponValidator(clock: fixedClock("2024-06-15"))
// Act — ejecutar el comportamiento probado
result = validator.validate(coupon)
// Assert — verificar el resultado
assert result.isValid == false
assert result.reason == "Coupon has expired"
Este patron hace los tests legibles y mantenibles. Cualquiera puede ver un test e inmediatamente entender que se prepara, que accion se toma y que resultado se espera.
Tests Parametrizados
Cuando multiples casos de prueba comparten la misma logica pero difieren solo en entrada/salida, usa tests parametrizados para evitar duplicacion:
@parameterized([
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
test "add retorna la suma de dos numeros"(a, b, expected):
assert add(a, b) == expected
Una definicion de test, multiples conjuntos de datos. Mucho mas limpio que escribir cuatro funciones de test separadas.
Convenciones de Nombres de Tests
Buenos nombres de tests describen el escenario y el resultado esperado:
test_divide_by_zero_throws_error— claro y especificotest_expired_coupon_returns_invalid— describe comportamientotest_new_user_gets_welcome_email— se lee como un requisito
Malos nombres de tests no dicen nada util:
test1— sin significadotestDivide— que sobre divide?testIt— probar que?
Tips Profesionales
Tip 1: Una afirmacion por test (usualmente). Un test que afirma cinco cosas diferentes es realmente cinco tests metidos en uno. Cuando falla, no sabes que aspecto se rompio.
Tip 2: No pruebes detalles de implementacion. Si refactorizas una funcion sin cambiar su comportamiento, cero tests deberian romperse. Si tus tests se rompen porque renombraste una variable interna, estan probando lo incorrecto.
Tip 3: Usa test doubles con moderacion. Cada mock es una mentira sobre el sistema real. Demasiados mocks y tus tests verifican tu configuracion de mocking, no tu codigo.
Conclusiones Clave
- Los unit tests verifican funciones individuales en completo aislamiento
- Los principios FIRST (Fast, Independent, Repeatable, Self-validating, Timely) definen la calidad
- Los test doubles (stubs, mocks, fakes) reemplazan dependencias reales en unit tests
- Code coverage mide ejecucion, no correccion — usalo para encontrar brechas, no para demostrar calidad
- Los desarrolladores escriben unit tests; QA revisa e identifica escenarios faltantes
- El patron AAA (Arrange-Act-Assert) mantiene los tests limpios y legibles