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.
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.
Antes de elegir datos concretos, debemos identificar el comportamiento que queremos verificar.
Preguntas útiles:
Elegir casos sin entender el comportamiento lleva a pruebas débiles o redundantes.
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:
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.
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.
Una regla crítica es aquella cuyo error tendría impacto importante. Por ejemplo:
Si una regla es crítica, merece una selección de casos más cuidadosa. No necesariamente muchas pruebas, sino pruebas bien elegidas.
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:
Una prueba en una zona cambiante puede ahorrar mucho trabajo de verificación manual.
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.
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.
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.
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:
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.
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:
Si la unidad debe rechazar esas entradas, conviene probarlo. Si no es responsabilidad de esa unidad, quizá corresponda a otra capa.
| 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. |
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.
Un contrato define qué espera una unidad y qué promete devolver o producir. Los casos relevantes deben proteger ese contrato.
Ejemplo de contrato:
Los casos relevantes salen directamente de ese contrato: lista con números y lista vacía.
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.
Antes de agregar una prueba, revisa:
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.