16. Clases de equivalencia aplicadas a pruebas unitarias

16.1 Introducción

Cuando una unidad acepta muchos valores posibles, no podemos probarlos todos. Las clases de equivalencia nos ayudan a agrupar valores que deberían comportarse de la misma manera.

La idea es elegir uno o pocos valores representativos de cada grupo. Si todos los valores de una clase deberían producir el mismo tipo de resultado, probar un representante puede ser suficiente para esa clase.

Esta técnica permite diseñar pruebas unitarias más inteligentes: menos casos, pero mejor seleccionados.

16.2 Qué es una clase de equivalencia

Una clase de equivalencia es un conjunto de valores que, según la regla que estamos probando, deberían recibir el mismo tratamiento.

Por ejemplo, si una persona puede registrarse desde los 18 años, podemos agrupar edades así:

  • Edades menores a 18: no pueden registrarse.
  • Edades iguales o mayores a 18: pueden registrarse.

Dentro de la primera clase, 10, 15 y 17 deberían comportarse igual. Dentro de la segunda, 18, 25 y 60 también deberían comportarse igual.

16.3 Ejemplo básico con edad

La función podría ser:

def puede_registrarse(edad):
    return edad >= 18

Podemos seleccionar un representante de cada clase:

def test_edad_menor_a_18_no_puede_registrarse():
    assert puede_registrarse(15) == False


def test_edad_mayor_o_igual_a_18_puede_registrarse():
    assert puede_registrarse(25) == True

Estas pruebas cubren las dos clases generales. Luego podemos agregar casos límite como 17 y 18, que estudiaremos más en el próximo tema.

16.4 Clases válidas e inválidas

Las clases de equivalencia pueden ser válidas o inválidas.

Tipo Significado Ejemplo
Clase válida Conjunto de valores que la unidad debe aceptar. Edad 18 o mayor.
Clase inválida Conjunto de valores que la unidad debe rechazar o manejar como error. Edad negativa o edad menor a 18, según la regla.

Es importante cubrir ambos tipos. Probar solo clases válidas deja sin verificar cómo se manejan datos incorrectos.

16.5 Identificar clases desde la regla

Para encontrar clases de equivalencia, primero leemos la regla. Luego preguntamos qué grupos de datos deberían comportarse igual.

Ejemplo de regla: "una contraseña es válida si tiene al menos 8 caracteres".

Clases posibles:

  • Contraseñas con menos de 8 caracteres: inválidas.
  • Contraseñas con 8 o más caracteres: válidas.

Con esta clasificación ya podemos elegir representantes.

16.6 Ejemplo con contraseña

def password_tiene_longitud_valida(password):
    return len(password) >= 8


def test_password_corta_no_es_valida():
    assert password_tiene_longitud_valida("abc") == False


def test_password_larga_es_valida():
    assert password_tiene_longitud_valida("abcdefghi") == True

La prueba con "abc" representa la clase de contraseñas cortas. La prueba con "abcdefghi" representa la clase de contraseñas válidas.

Después podemos agregar los valores de borde 7 y 8 si queremos comprobar el límite exacto.

16.7 No confundir clases con valores individuales

Una clase de equivalencia no es un único valor. Es un grupo de valores que se consideran similares para la regla.

Por ejemplo, si la regla solo distingue si una edad es menor o mayor a 18, entonces 20, 30 y 40 pertenecen a la misma clase válida. No hace falta probar todos esos valores si no hay otra condición que los diferencie.

La técnica nos ayuda a evitar pruebas duplicadas.

16.8 Ejemplo con descuentos por tipo de cliente

Supongamos esta regla:

  • Cliente común: 0% de descuento.
  • Cliente VIP: 15% de descuento.
  • Cliente empleado: 20% de descuento.
  • Tipo desconocido: 0% de descuento.
def obtener_descuento(tipo_cliente):
    if tipo_cliente == "vip":
        return 15
    if tipo_cliente == "empleado":
        return 20
    return 0

Aquí cada tipo de cliente con resultado diferente puede considerarse una clase relevante.

16.9 Pruebas para clases de cliente

def test_cliente_comun_no_tiene_descuento():
    assert obtener_descuento("comun") == 0


def test_cliente_vip_tiene_descuento_15():
    assert obtener_descuento("vip") == 15


def test_cliente_empleado_tiene_descuento_20():
    assert obtener_descuento("empleado") == 20


def test_tipo_de_cliente_desconocido_no_tiene_descuento():
    assert obtener_descuento("invitado") == 0

Cada prueba representa una clase de comportamiento. No necesitamos probar muchos textos desconocidos si todos se tratan igual, salvo que existan reglas adicionales.

16.10 Clases por rangos

Muchas reglas dividen valores numéricos en rangos. Cada rango puede ser una clase de equivalencia.

def clasificar_monto(monto):
    if monto < 1000:
        return "bajo"
    if monto <= 10000:
        return "medio"
    return "alto"

Clases:

  • Montos menores a 1000: bajo.
  • Montos entre 1000 y 10000: medio.
  • Montos mayores a 10000: alto.

16.11 Pruebas para rangos

def test_monto_bajo():
    assert clasificar_monto(500) == "bajo"


def test_monto_medio():
    assert clasificar_monto(5000) == "medio"


def test_monto_alto():
    assert clasificar_monto(12000) == "alto"

Estos casos representan cada rango. Los valores exactos de borde, como 999, 1000, 10000 y 10001, se analizan con la técnica de valores límite.

16.12 Clases para colecciones

Las colecciones también pueden dividirse en clases. Por ejemplo, una función que calcula el promedio puede tener estas clases:

  • Lista vacía.
  • Lista con un elemento.
  • Lista con varios elementos.
def promedio(numeros):
    if len(numeros) == 0:
        return 0
    return sum(numeros) / len(numeros)

Estas clases son relevantes porque la lista vacía requiere un tratamiento distinto.

16.13 Pruebas para colecciones

def test_promedio_de_lista_vacia_es_cero():
    assert promedio([]) == 0


def test_promedio_de_lista_con_un_elemento():
    assert promedio([10]) == 10


def test_promedio_de_lista_con_varios_elementos():
    assert promedio([10, 20, 30]) == 20

Cada prueba representa una clase diferente de entrada. Esto da más información que probar varias listas de tres elementos sin una razón específica.

16.14 Clases inválidas

Además de entradas válidas, debemos identificar clases inválidas si la unidad tiene responsabilidad de rechazarlas.

Ejemplo: una cantidad debe ser positiva.

def validar_cantidad(cantidad):
    if cantidad <= 0:
        raise ValueError("La cantidad debe ser mayor que cero")
    return True

Clases:

  • Cantidades positivas: válidas.
  • Cero: inválido.
  • Cantidades negativas: inválidas.

16.15 Pruebas para clases inválidas

import pytest


def test_cantidad_positiva_es_valida():
    assert validar_cantidad(5) == True


def test_cantidad_cero_lanza_error():
    with pytest.raises(ValueError):
        validar_cantidad(0)


def test_cantidad_negativa_lanza_error():
    with pytest.raises(ValueError):
        validar_cantidad(-3)

Estas pruebas cubren la clase válida y dos clases inválidas. Cero y negativo pueden separarse porque muchas reglas tratan el cero como un caso especial.

16.16 Pasos para aplicar la técnica

  1. Identificar la regla que se quiere probar.
  2. Listar entradas o condiciones relevantes.
  3. Agrupar valores que deberían comportarse igual.
  4. Separar clases válidas e inválidas.
  5. Elegir un representante de cada clase.
  6. Agregar valores límite cuando el borde sea importante.
  7. Escribir pruebas con nombres que indiquen la clase cubierta.

Estos pasos ayudan a evitar tanto la falta de pruebas como el exceso de pruebas repetidas.

16.17 Tabla de ejemplos

Regla Clases de equivalencia Representantes
Edad mínima 18 Menor a 18, mayor o igual a 18 15, 25
Password mínimo 8 caracteres Corta, válida "abc", "abcdefghi"
Monto bajo, medio, alto <1000, 1000-10000, >10000 500, 5000, 12000
Promedio de lista Vacía, un elemento, varios elementos [], [10], [10, 20, 30]
Cantidad positiva Positiva, cero, negativa 5, 0, -3

16.18 Errores comunes

Al aplicar clases de equivalencia, conviene evitar estos errores:

  • Elegir muchos valores de la misma clase sin necesidad.
  • Olvidar clases inválidas.
  • No separar valores especiales como cero o lista vacía.
  • Confundir representantes con valores límite.
  • Agrupar valores que en realidad tienen reglas diferentes.
  • Usar nombres de prueba que no indiquen qué clase se cubre.

16.19 Lista de comprobación

Antes de finalizar la selección de casos, revisa:

  • ¿Identificaste las clases válidas?
  • ¿Identificaste las clases inválidas?
  • ¿Cada clase tiene al menos un representante?
  • ¿Hay clases que deberían separarse por tener reglas distintas?
  • ¿Hay valores especiales que merecen una clase propia?
  • ¿Los valores límite se cubrirán en pruebas adicionales si son importantes?

16.20 Qué debes recordar de este tema

  • Una clase de equivalencia agrupa valores que deberían comportarse igual.
  • La técnica permite elegir casos representativos.
  • Debemos considerar clases válidas e inválidas.
  • No hace falta probar muchos valores de la misma clase si no aportan información nueva.
  • Los valores especiales pueden necesitar su propia clase.
  • Las clases de equivalencia ayudan a reducir pruebas redundantes.
  • Los valores límite complementan esta técnica, no la reemplazan.

16.21 Conclusión

Las clases de equivalencia ayudan a diseñar pruebas unitarias con criterio. En lugar de elegir valores al azar, agrupamos entradas similares y seleccionamos representantes que cubran comportamientos importantes.

Esta técnica reduce pruebas repetidas y mejora la claridad de la suite. Es especialmente útil cuando una unidad acepta muchos valores posibles pero los trata en pocos grupos de comportamiento.

En el próximo tema estudiaremos valores límite, una técnica complementaria que se enfoca en los bordes exactos donde las reglas cambian.