Cuando escribimos pruebas unitarias, no alcanza con probar solo el caso más cómodo. Una unidad puede comportarse bien con entradas normales y fallar con entradas inválidas o valores justo en el límite de una regla.
Una forma práctica de organizar los casos es distinguir entre pruebas positivas, pruebas negativas y casos borde.
En este tema veremos qué significa cada categoría, cómo elegir ejemplos útiles y cómo evitar una cantidad excesiva de pruebas sin criterio.
Una prueba positiva verifica que la unidad funcione correctamente cuando recibe datos válidos o condiciones esperadas.
Ejemplos:
Las pruebas positivas confirman el camino esperado del comportamiento.
Supongamos que una persona puede registrarse si tiene al menos 18 años.
def puede_registrarse(edad):
return edad >= 18
def test_persona_de_20_anios_puede_registrarse():
assert puede_registrarse(20) == True
Esta es una prueba positiva porque usa una edad válida y espera que la operación sea aceptada.
Una prueba negativa verifica que la unidad rechace correctamente datos inválidos o condiciones no permitidas.
Ejemplos:
Las pruebas negativas son importantes porque el software no solo debe funcionar con datos correctos; también debe manejar datos incorrectos de forma segura y previsible.
Siguiendo con la regla de edad, una persona de 17 años no debería poder registrarse.
def test_persona_de_17_anios_no_puede_registrarse():
assert puede_registrarse(17) == False
Esta prueba comprueba que la unidad rechaza un caso no permitido.
Un caso borde, o caso límite, se encuentra justo alrededor del punto donde una regla cambia de resultado.
Si la regla dice "a partir de 18", los valores cercanos son 17, 18 y 19. Si una contraseña debe tener al menos 8 caracteres, los valores cercanos son 7, 8 y 9 caracteres.
Para la regla de edad mínima de 18 años, podemos probar los valores alrededor del límite.
def test_edad_17_no_puede_registrarse():
assert puede_registrarse(17) == False
def test_edad_18_puede_registrarse():
assert puede_registrarse(18) == True
def test_edad_19_puede_registrarse():
assert puede_registrarse(19) == True
La prueba más importante suele ser la del valor exacto del límite, en este caso 18. Los valores vecinos ayudan a confirmar la transición.
Un mismo conjunto de pruebas puede incluir las tres categorías.
| Caso | Tipo | Resultado esperado |
|---|---|---|
| Edad 20 | Positivo | Puede registrarse. |
| Edad 17 | Negativo y borde inferior cercano | No puede registrarse. |
| Edad 18 | Borde exacto y positivo | Puede registrarse. |
Las categorías no siempre son excluyentes. Un caso puede ser positivo y borde al mismo tiempo.
Supongamos que una contraseña debe tener al menos 8 caracteres.
def password_tiene_longitud_valida(password):
return len(password) >= 8
def test_password_de_7_caracteres_no_es_valido():
assert password_tiene_longitud_valida("abcdefg") == False
def test_password_de_8_caracteres_es_valido():
assert password_tiene_longitud_valida("abcdefgh") == True
def test_password_de_9_caracteres_es_valido():
assert password_tiene_longitud_valida("abcdefghi") == True
Estos casos prueban justo alrededor del límite de longitud. Si el código usa una comparación incorrecta, alguno de ellos fallará.
Ahora pensemos en una regla de envío gratis para compras de 5000 o más.
def tiene_envio_gratis(total):
return total >= 5000
def test_total_4999_no_tiene_envio_gratis():
assert tiene_envio_gratis(4999) == False
def test_total_5000_tiene_envio_gratis():
assert tiene_envio_gratis(5000) == True
def test_total_5001_tiene_envio_gratis():
assert tiene_envio_gratis(5001) == True
El valor 5000 es el borde exacto. Probarlo evita ambigüedades sobre si la regla incluye o excluye el límite.
Una prueba negativa puede esperar un valor de retorno o una excepción. Depende del diseño de la unidad.
import pytest
def retirar(saldo, importe):
if importe > saldo:
raise ValueError("Saldo insuficiente")
return saldo - importe
def test_retirar_mas_del_saldo_lanza_error():
with pytest.raises(ValueError):
retirar(100, 150)
Este caso negativo espera una excepción porque la operación no debe aceptarse.
En otros diseños, una entrada inválida devuelve un valor especial.
def obtener_descuento(porcentaje):
if porcentaje < 0:
return 0
return porcentaje
def test_descuento_negativo_devuelve_cero():
assert obtener_descuento(-10) == 0
La prueba sigue siendo negativa porque usa un dato inválido, aunque el comportamiento esperado no sea una excepción.
El "camino feliz" es el caso donde todo sale bien: datos válidos, condiciones correctas y resultado esperado. Es necesario probarlo, pero no alcanza.
Si solo probamos el camino feliz, pueden quedar defectos importantes en:
Una unidad robusta debe comportarse bien tanto cuando se la usa correctamente como cuando recibe datos problemáticos.
El extremo opuesto también es un problema. Intentar probar todas las combinaciones posibles puede generar una suite enorme, lenta y difícil de mantener.
La clave es elegir casos representativos:
No buscamos cantidad por cantidad. Buscamos cubrir riesgos relevantes.
Además de positivos, negativos y bordes, conviene prestar atención a valores especiales:
Estos valores suelen descubrir suposiciones ocultas en el código.
Una función que calcula el promedio de una lista debe definir qué ocurre si la lista está vacía.
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
def test_promedio_de_lista_con_numeros():
assert promedio([10, 20, 30]) == 20
El primer caso prueba una situación especial. El segundo prueba el comportamiento normal.
| Tipo de caso | Qué verifica | Ejemplo |
|---|---|---|
| Positivo | La unidad acepta datos válidos. | Edad 20 puede registrarse. |
| Negativo | La unidad rechaza datos inválidos. | Edad 17 no puede registrarse. |
| Borde inferior | Valor justo antes del límite. | 4999 para mínimo 5000. |
| Borde exacto | Valor del límite definido. | 5000 para mínimo 5000. |
| Borde superior cercano | Valor justo después del límite. | 5001 para mínimo 5000. |
| Especial | Valor que suele generar errores. | Lista vacía o texto vacío. |
El nombre de la prueba debe dejar claro qué tipo de caso estamos verificando.
def test_total_4999_no_tiene_envio_gratis():
assert tiene_envio_gratis(4999) == False
def test_total_5000_tiene_envio_gratis():
assert tiene_envio_gratis(5000) == True
Los valores aparecen en el nombre porque son importantes para entender el límite. Esto mejora la lectura y el diagnóstico cuando una prueba falla.
Al elegir casos para una unidad, revisa:
Clasificar casos en positivos, negativos y borde ayuda a diseñar pruebas unitarias más completas. Esta clasificación obliga a pensar no solo en lo que debería funcionar, sino también en lo que debería rechazarse y en los límites exactos de cada regla.
El objetivo no es escribir muchas pruebas, sino elegir casos que aporten información. Una buena selección de casos detecta errores importantes con una suite clara y mantenible.
En el próximo tema veremos cómo seleccionar casos de prueba relevantes de forma más general, considerando riesgo, intención y valor de cada prueba.