TL;DR
- Unit testing: Testear funciones/métodos individuales de forma aislada
- Por qué importa: Detecta bugs temprano, permite refactoring seguro, documenta comportamiento
- Principio clave: Cada test verifica UNA cosa funciona correctamente
- Frameworks populares: Jest (JavaScript), pytest (Python), JUnit (Java)
- Mejor práctica: Escribe tests antes de arreglar bugs
- ROI: Bugs detectados a nivel unit cuestan 10-100x menos
Tiempo de lectura: 10 minutos
Unit testing es la base de la calidad del software. Testea piezas individuales de código de forma aislada, detectando bugs antes de que se propaguen al resto del sistema.
¿Qué es Unit Testing?
Unit testing testea las partes más pequeñas testeables de tu código — funciones y métodos. Cada unit test verifica que una pieza específica de código produce el output esperado para inputs dados.
Test de Integración: Login → API → Base de datos → Response
(Muchos componentes trabajando juntos)
Unit Test: validateEmail("test@example.com") → true
(Una función, aislada)
Los unit tests corren rápido porque no involucran bases de datos, red o servicios externos.
Por qué Unit Testing Importa
1. Detecta Bugs Temprano
Bugs encontrados en unit testing cuestan mucho menos:
| Etapa de Detección | Costo Relativo |
|---|---|
| Unit testing | 1x |
| Integration testing | 10x |
| System testing | 40x |
| Producción | 100x+ |
Encontrar un bug en unit test toma minutos. Encontrarlo en producción toma días.
2. Permite Refactoring Seguro
Con unit tests, puedes refactorizar con confianza:
// Función original
function calculatePrice(price, quantity) {
return price * quantity;
}
// Tests protegen el refactoring
test('calculates total price', () => {
expect(calculatePrice(10, 3)).toBe(30);
});
// Seguro de refactorizar - tests detectarán errores
function calculatePrice(price, quantity, discount = 0) {
return (price * quantity) * (1 - discount);
}
Los tests verifican que la función sigue funcionando después de cambios.
3. Documenta Comportamiento del Código
Tests muestran exactamente cómo el código debe usarse:
// Tests documentan comportamiento esperado
test('returns empty array for null input', () => {
expect(filterUsers(null)).toEqual([]);
});
test('filters users by active status', () => {
const users = [
{ name: 'John', active: true },
{ name: 'Jane', active: false }
];
expect(filterUsers(users)).toEqual([{ name: 'John', active: true }]);
});
Nuevos desarrolladores entienden el comportamiento leyendo tests.
4. Acelera el Desarrollo
Contraintuitivamente, escribir tests acelera el desarrollo:
Sin tests: Código → Test manual → Bug → Debug → Fix → Test manual
Con tests: Código → Correr tests → Bug → Fix → Correr tests (segundos)
Tests automatizados dan feedback instantáneo.
Estructura de Unit Test
El Patrón AAA
Todo unit test sigue tres pasos:
test('adds two numbers correctly', () => {
// Arrange: Preparar datos de test
const a = 5;
const b = 3;
// Act: Llamar la función
const result = add(a, b);
// Assert: Verificar el resultado
expect(result).toBe(8);
});
Este patrón hace los tests legibles y mantenibles.
Qué Testear
- Happy path: Comportamiento normal esperado
- Edge cases: Condiciones límite
- Manejo de errores: Inputs inválidos
describe('divide function', () => {
// Happy path
test('divides two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
// Edge case
test('handles decimal results', () => {
expect(divide(10, 3)).toBeCloseTo(3.33, 2);
});
// Manejo de errores
test('throws error for division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
Tu Primer Unit Test
JavaScript (Jest)
// math.js
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = { add, multiply };
// math.test.js
const { add, multiply } = require('./math');
describe('Math functions', () => {
test('adds positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('adds negative numbers', () => {
expect(add(-2, -3)).toBe(-5);
});
test('multiplies numbers', () => {
expect(multiply(4, 5)).toBe(20);
});
test('multiplies by zero', () => {
expect(multiply(4, 0)).toBe(0);
});
});
Ejecutar con npm test.
Python (pytest)
# calculator.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# test_calculator.py
import pytest
from calculator import add, divide
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-2, -3) == -5
def test_divide():
assert divide(10, 2) == 5
def test_divide_by_zero():
with pytest.raises(ValueError):
divide(10, 0)
Ejecutar con pytest.
Java (JUnit)
// Calculator.java
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Division by zero");
}
return a / b;
}
}
// CalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
private Calculator calc = new Calculator();
@Test
void addPositiveNumbers() {
assertEquals(5, calc.add(2, 3));
}
@Test
void addNegativeNumbers() {
assertEquals(-5, calc.add(-2, -3));
}
@Test
void divideNumbers() {
assertEquals(5, calc.divide(10, 2));
}
@Test
void divideByZeroThrows() {
assertThrows(IllegalArgumentException.class,
() -> calc.divide(10, 0));
}
}
Mocking de Dependencias
Unit tests deben estar aislados. Mockea dependencias externas:
// userService.js
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// userService.test.js
jest.mock('node-fetch');
test('fetches user by id', async () => {
// Mockear la respuesta de fetch
fetch.mockResolvedValue({
json: () => Promise.resolve({ id: 1, name: 'John' })
});
const user = await getUser(1);
expect(user.name).toBe('John');
expect(fetch).toHaveBeenCalledWith('/api/users/1');
});
Mocking asegura que tests corran rápido y no dependan de servicios externos.
Cobertura de Tests
Entendiendo Cobertura
Cobertura mide cuánto código ejercitan los tests:
Line coverage: Qué % de líneas ejecutadas
Branch coverage: Qué % de ramas if/else testeadas
Function coverage: Qué % de funciones llamadas
Metas Prácticas de Cobertura
| Tipo de Código | Cobertura Objetivo |
|---|---|
| Lógica de negocio | 80-90% |
| Utilidades | 90%+ |
| Componentes UI | 60-70% |
| Código generado | 0% (omitir) |
Cobertura es una guía, no una meta. Alta cobertura no significa buenos tests.
// 100% cobertura pero test inútil
test('covers the function', () => {
const result = complexCalculation(1, 2, 3);
expect(result).toBeDefined(); // Aserción débil
});
// Menor cobertura pero test valioso
test('calculates discount correctly', () => {
expect(calculateDiscount(100, 0.1)).toBe(90);
expect(calculateDiscount(100, 0.5)).toBe(50);
});
Testea comportamiento, no números de cobertura.
Mejores Prácticas
1. Testea Una Cosa
Cada test debe verificar un comportamiento:
// Malo: Testeando múltiples cosas
test('user validation', () => {
expect(validateEmail('test@example.com')).toBe(true);
expect(validateEmail('')).toBe(false);
expect(validatePassword('abc')).toBe(false);
expect(validatePassword('abcd1234')).toBe(true);
});
// Bueno: Un test por comportamiento
test('validates correct email', () => {
expect(validateEmail('test@example.com')).toBe(true);
});
test('rejects empty email', () => {
expect(validateEmail('')).toBe(false);
});
2. Usa Nombres Descriptivos
Nombres de tests deben describir comportamiento esperado:
// Malos nombres
test('test1', () => { ... });
test('validateEmail', () => { ... });
// Buenos nombres
test('rejects email without @ symbol', () => { ... });
test('accepts valid email with subdomain', () => { ... });
3. Mantén Tests Independientes
Tests no deben depender uno del otro:
// Malo: Tests dependen de estado compartido
let counter = 0;
test('increments counter', () => {
counter++;
expect(counter).toBe(1);
});
test('counter is one', () => {
expect(counter).toBe(1); // Falla si primer test no corre
});
// Bueno: Cada test es independiente
test('increments counter', () => {
const counter = new Counter();
counter.increment();
expect(counter.value).toBe(1);
});
4. Escribe Tests Antes de Arreglar Bugs
Al arreglar bugs, escribe un test que lo reproduzca:
// Bug: calculateTax retorna NaN para valores negativos
// Paso 1: Escribir test que falla
test('handles negative values', () => {
expect(calculateTax(-100)).toBe(0);
});
// Paso 2: Arreglar el bug
function calculateTax(amount) {
if (amount < 0) return 0;
return amount * 0.1;
}
// Paso 3: Test pasa, bug no regresará
Unit Testing vs Otro Testing
| Tipo | Qué Testea | Velocidad | Aislamiento |
|---|---|---|---|
| Unit | Una función | Más rápido | Completo |
| Integration | Interacciones de componentes | Medio | Parcial |
| E2E | Flujos completos de usuario | Más lento | Ninguno |
Unit tests forman la base de la pirámide de testing:
/\
/ \ E2E (pocos)
/----\
/ \ Integration (algunos)
/--------\
/ \ Unit (muchos)
/____________\
Más unit tests, menos integration tests, menos E2E tests.
FAQ
¿Qué es unit testing?
Unit testing testea funciones o métodos individuales en completo aislamiento del resto del sistema. Cada test se enfoca en una pequeña pieza de código — típicamente una función — y verifica que produce el output correcto para inputs dados. La característica clave es el aislamiento: unit tests no involucran bases de datos, APIs u otras dependencias externas. Este aislamiento los hace rápidos (milisegundos) y confiables.
¿Por qué es importante unit testing?
Unit testing detecta bugs en la etapa más temprana y barata del desarrollo. Un bug encontrado durante unit testing cuesta aproximadamente 10-100x menos que uno encontrado en producción. Más allá de detectar bugs, unit tests permiten refactoring confiado (cambia código sabiendo que tests detectarán errores), sirven como documentación viva (mostrando exactamente cómo el código debe comportarse), y aceleran desarrollo a través de loops de feedback instantáneo.
¿Qué hace un buen unit test?
Buenos unit tests siguen los principios FIRST: Fast (rápidos, milisegundos), Isolated (aislados, sin dependencias externas), Repeatable (repetibles, mismo resultado cada vez), Self-validating (auto-validados, pass/fail claro), y Timely (oportunos, escritos cerca del código). También siguen el patrón AAA: Arrange datos de test, Act llamando la función, Assert el resultado esperado. Cada test debe verificar un comportamiento específico con nombre descriptivo.
¿Cuánta cobertura de tests necesito?
Apunta a 70-80% cobertura en lógica de negocio crítica, 90%+ en utilidades, y 60-70% en componentes UI. Sin embargo, cobertura es guía, no meta. 100% cobertura no significa buenos tests — puedes tener cobertura completa con aserciones débiles. Enfócate en testear comportamientos significativos en lugar de alcanzar números de cobertura. Omite testear código generado, getters/setters y boilerplate de framework.
Ver También
- Qué es API Testing - Fundamentos de API testing
- Jest vs Mocha - Comparación de frameworks JavaScript
- TestNG vs JUnit - Frameworks de testing Java
- Qué es Regression Testing - Prevención de regresión de bugs
