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ó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.

Fuentes y Lectura Adicional

Ver También