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 práctica de testear funciones o métodos individuales en completo aislamiento del resto del sistema, verificando que cada pequeña pieza de código produce el output correcto para los inputs dados. Según el StackOverflow Developer Survey 2024, el unit testing es la práctica de testing más ampliamente adoptada entre todas las categorías de desarrolladores, con un 74% de los desarrolladores profesionales escribiendo unit tests regularmente. Martin Fowler define un unit test como “un test que ejecuta una pequeña pieza de código en aislamiento del resto del código”. El argumento financiero es convincente: según ISTQB, los bugs encontrados en la etapa de unit testing cuestan aproximadamente 10-100 veces menos de corregir que los descubiertos en producción. Los unit tests también sirven como documentación viva — muestran exactamente cómo deben comportarse las funciones, haciendo el onboarding de nuevos desarrolladores más rápido y el refactoring más seguro. En JavaScript (Jest), Python (pytest) y Java (JUnit), escribir el primer unit test lleva minutos, y el ciclo de retroalimentación — desde el cambio de código hasta el resultado del test — corre en segundos.
¿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
“El mejor momento para escribir un unit test es justo antes de escribir la función. El segundo mejor momento es justo después de descubrir un bug. Los unit tests son cómo construís confianza en que el refactoring no va a romper nada — sin ellos, cada cambio es una apuesta.” — Yuri Kan, Senior QA Lead
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.
Fuentes y Lectura Adicional
- Martin Fowler sobre Unit Testing — Definiciones fundamentales y filosofía de unit tests
- ISTQB Foundation Level Syllabus — Estándares de certificación de la industria para conocimiento de testing
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
