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ónCosto Relativo
Unit testing1x
Integration testing10x
System testing40x
Producción100x+

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

  1. Happy path: Comportamiento normal esperado
  2. Edge cases: Condiciones límite
  3. 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ódigoCobertura Objetivo
Lógica de negocio80-90%
Utilidades90%+
Componentes UI60-70%
Código generado0% (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

TipoQué TesteaVelocidadAislamiento
UnitUna funciónMás rápidoCompleto
IntegrationInteracciones de componentesMedioParcial
E2EFlujos completos de usuarioMás lentoNinguno

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