15. Selección de casos de prueba relevantes

15.1 Introducción

Una de las habilidades más importantes al escribir pruebas unitarias es elegir qué casos probar. No podemos probar todas las combinaciones posibles, y tampoco conviene escribir pruebas sin intención solo para aumentar la cantidad.

Un caso de prueba relevante es aquel que aporta información útil: verifica una regla importante, cubre un riesgo, documenta una decisión o protege un comportamiento que podría romperse.

En este tema veremos criterios prácticos para seleccionar casos con valor y evitar suites llenas de pruebas repetidas o poco significativas.

15.2 No todas las pruebas aportan el mismo valor

Dos pruebas pueden tener el mismo costo de ejecución, pero aportar valor muy diferente. Una puede cubrir una regla crítica de negocio y otra puede verificar un detalle trivial.

Por ejemplo, probar que un impuesto se calcula correctamente suele tener más valor que probar un método que solo devuelve una propiedad sin lógica.

La pregunta clave no es "¿puedo probar esto?", sino "¿qué riesgo o comportamiento importante cubre esta prueba?".

15.3 Empezar por el comportamiento

Antes de elegir datos concretos, debemos identificar el comportamiento que queremos verificar.

Preguntas útiles:

  • ¿Qué regla debe cumplir esta unidad?
  • ¿Qué resultado espera quien usa esta función o clase?
  • ¿Qué condiciones cambian la salida?
  • ¿Qué datos pueden ser inválidos?
  • ¿Qué error sería importante detectar temprano?

Elegir casos sin entender el comportamiento lleva a pruebas débiles o redundantes.

15.4 Ejemplo inicial

Supongamos una regla: una compra recibe descuento si el cliente es VIP o si el monto supera 10000.

def tiene_descuento(es_vip, monto):
    return es_vip or monto > 10000

Podríamos probar muchos valores, pero no todos aportan lo mismo. Lo relevante es cubrir las condiciones que cambian el resultado:

  • Cliente VIP con monto bajo.
  • Cliente no VIP con monto alto.
  • Cliente no VIP con monto bajo.
  • El límite exacto del monto.

15.5 Casos representativos

Un caso representativo cubre un grupo de situaciones similares. No necesitamos probar diez montos altos si todos siguen la misma regla.

def test_cliente_vip_tiene_descuento_con_monto_bajo():
    assert tiene_descuento(True, 1000) == True


def test_cliente_no_vip_con_monto_alto_tiene_descuento():
    assert tiene_descuento(False, 12000) == True


def test_cliente_no_vip_con_monto_bajo_no_tiene_descuento():
    assert tiene_descuento(False, 1000) == False

Estos tres casos cubren las decisiones principales sin multiplicar pruebas innecesarias.

15.6 Casos límite

Cuando una regla tiene un límite, el valor exacto del límite suele ser muy relevante. En el ejemplo anterior, la condición dice monto > 10000, no monto >= 10000.

def test_cliente_no_vip_con_monto_10000_no_tiene_descuento():
    assert tiene_descuento(False, 10000) == False


def test_cliente_no_vip_con_monto_10001_tiene_descuento():
    assert tiene_descuento(False, 10001) == True

Estos casos protegen una decisión precisa. Si alguien cambia accidentalmente el operador, la prueba puede detectarlo.

15.7 Priorizar reglas críticas

Una regla crítica es aquella cuyo error tendría impacto importante. Por ejemplo:

  • Cálculos de dinero.
  • Permisos y accesos.
  • Validaciones de seguridad.
  • Reglas de aprobación o rechazo.
  • Transformaciones de datos usadas por otros módulos.

Si una regla es crítica, merece una selección de casos más cuidadosa. No necesariamente muchas pruebas, sino pruebas bien elegidas.

15.8 Priorizar código que cambia

El código que cambia con frecuencia tiene más riesgo de regresiones. Si una unidad se modifica a menudo, conviene proteger sus comportamientos importantes con pruebas.

Ejemplos:

  • Reglas comerciales que se ajustan por promociones.
  • Validadores que cambian por requisitos nuevos.
  • Cálculos de comisiones o impuestos.
  • Transformadores de datos que reciben nuevos formatos.

Una prueba en una zona cambiante puede ahorrar mucho trabajo de verificación manual.

15.9 Priorizar defectos históricos

Si una unidad ya tuvo defectos, conviene agregar pruebas que cubran esos casos. Los defectos históricos muestran zonas donde el código o la regla pueden ser delicados.

Ejemplo: si una función de promedio falló con listas vacías, ese caso debe quedar protegido.

def promedio(numeros):
    if len(numeros) == 0:
        return 0
    return sum(numeros) / len(numeros)


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

Una prueba nacida de un defecto real suele tener alto valor, porque evita que el problema vuelva.

15.10 Evitar pruebas duplicadas

Dos pruebas son duplicadas cuando verifican prácticamente lo mismo con datos que no cambian la regla.

def test_monto_12000_tiene_descuento():
    assert tiene_descuento(False, 12000) == True


def test_monto_13000_tiene_descuento():
    assert tiene_descuento(False, 13000) == True


def test_monto_14000_tiene_descuento():
    assert tiene_descuento(False, 14000) == True

Si todos esos montos pertenecen a la misma categoría, quizá una sola prueba representativa sea suficiente. Podemos reservar más casos para límites o condiciones diferentes.

15.11 Evitar pruebas triviales

Una prueba trivial verifica algo tan simple que no aporta información real. Por ejemplo, probar un método que solo devuelve una propiedad sin lógica puede no justificar el costo.

class Usuario:
    def __init__(self, nombre):
        self.nombre = nombre

    def obtener_nombre(self):
        return self.nombre

Probar obtener_nombre puede ser innecesario si no hay regla, transformación ni riesgo. Conviene concentrarse en comportamientos con decisiones o impacto.

15.12 Elegir casos por ramas de decisión

Si una unidad tiene condicionales, cada rama importante suele necesitar al menos un caso representativo.

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

Casos relevantes:

  • Un monto bajo, por ejemplo 500.
  • Un monto medio, por ejemplo 5000.
  • Un monto alto, por ejemplo 12000.
  • Límites como 999, 1000, 10000 y 10001 si la regla es crítica.

15.13 Ejemplo con ramas

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


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


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

Estos casos cubren las tres salidas posibles. Luego podemos agregar límites si queremos más precisión.

15.14 Elegir casos por entradas inválidas

Las entradas inválidas relevantes dependen del contrato de la unidad. No todas las entradas inválidas merecen la misma atención, pero algunas son frecuentes:

  • Números negativos donde solo se aceptan positivos.
  • Texto vacío donde se requiere un valor.
  • Listas vacías.
  • Valores nulos.
  • Formatos incorrectos.

Si la unidad debe rechazar esas entradas, conviene probarlo. Si no es responsabilidad de esa unidad, quizá corresponda a otra capa.

15.15 Tabla de criterios de relevancia

Criterio Pregunta Ejemplo
Riesgo ¿Qué tan grave sería una falla? Cálculo de impuestos.
Frecuencia de cambio ¿Esta regla cambia seguido? Promociones comerciales.
Complejidad ¿Hay varias condiciones o ramas? Clasificación por monto.
Historial de defectos ¿Ya falló antes? Promedio de lista vacía.
Límites ¿Hay valores donde cambia la regla? Edad mínima 18.
Contrato ¿Qué entradas válidas e inválidas promete manejar? Contraseña de al menos 8 caracteres.

15.16 Casos que documentan decisiones

Algunos casos son relevantes porque documentan una decisión que podría no ser obvia.

Por ejemplo, si el promedio de una lista vacía debe ser 0, esa decisión merece una prueba aunque el código sea simple. Otra persona podría suponer que debería lanzarse una excepción o devolverse None.

Las pruebas también sirven para dejar explícitas decisiones del dominio.

15.17 Casos que protegen contratos

Un contrato define qué espera una unidad y qué promete devolver o producir. Los casos relevantes deben proteger ese contrato.

Ejemplo de contrato:

  • La función recibe una lista de números.
  • Si la lista tiene elementos, devuelve el promedio.
  • Si la lista está vacía, devuelve 0.

Los casos relevantes salen directamente de ese contrato: lista con números y lista vacía.

15.18 No confundir cobertura con relevancia

La cobertura de código puede indicar qué líneas se ejecutaron, pero no garantiza que los casos sean relevantes.

Una prueba puede ejecutar una función sin verificar una expectativa importante. También puede cubrir una línea con un caso poco representativo.

La cobertura es una señal útil, pero la selección de casos requiere criterio: entender reglas, riesgos, límites y contratos.

15.19 Lista de comprobación

Antes de agregar una prueba, revisa:

  • ¿Qué comportamiento importante verifica?
  • ¿Qué riesgo reduce?
  • ¿El caso representa una clase de situaciones?
  • ¿Cubre un límite, rama o entrada inválida relevante?
  • ¿Evita duplicar otra prueba existente?
  • ¿Será útil si falla en el futuro?
  • ¿El costo de mantenerla es razonable para el valor que aporta?

15.20 Qué debes recordar de este tema

  • No podemos ni debemos probar todas las combinaciones posibles.
  • Un caso relevante protege un comportamiento, riesgo o decisión importante.
  • Conviene priorizar reglas críticas, código cambiante y defectos históricos.
  • Los casos límite suelen tener mucho valor.
  • Las pruebas duplicadas aumentan mantenimiento sin aportar información nueva.
  • La cobertura no reemplaza el criterio de selección.
  • Una buena prueba debe justificar su existencia.

15.21 Conclusión

Seleccionar buenos casos de prueba es una habilidad central en pruebas unitarias. Una suite útil no se mide solo por cantidad, sino por la calidad de los comportamientos que protege.

Elegir casos relevantes implica pensar en reglas, riesgos, límites, contratos, cambios frecuentes y defectos reales. Esa selección mantiene la suite enfocada y valiosa.

En el próximo tema estudiaremos clases de equivalencia aplicadas a pruebas unitarias, una técnica que ayuda a seleccionar casos representativos sin probar combinaciones innecesarias.