assert y cómo leer informes de fallos
Cómo ve pytest el interior de un assert simple, cómo leer un informe de fallos línea por línea y qué comparación escribir para que el informe te aporte la mayor información.
¿Cómo quieres aprender?
El problema que vamos a resolver
Aquí tienes una comprobación escrita en Python puro, en un archivo llamado check.py:
def full_name(first, last):
return f"{first} {last}".title()
assert full_name("ada", "lovelace") == "Ada Lovelace "Traceback (most recent call last):
File "/home/you/shop/check.py", line 5, in <module>
assert full_name("ada", "lovelace") == "Ada Lovelace "
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionErrorPython te dice que la afirmación era falsa, y nada más. ¿Qué devolvió realmente full_name? Tendrías que añadir un print, ejecutarlo de nuevo y mirar.
Pon la misma línea en un test, test_names.py, y ejecuta pytest -q:
from names import full_name
def test_full_name():
assert full_name("ada", "lovelace") == "Ada Lovelace "F [100%]
=================================== FAILURES ===================================
________________________________ test_full_name ________________________________
def test_full_name():
> assert full_name("ada", "lovelace") == "Ada Lovelace "
E AssertionError: assert 'Ada Lovelace' == 'Ada Lovelace '
E
E - Ada Lovelace
E ? -
E + Ada Lovelace
test_names.py:5: AssertionError
=========================== short test summary info ============================
FAILED test_names.py::test_full_name - AssertionError: assert 'Ada Lovelace' ...
1 failed in 0.01sAhora puedes ver ambos valores y una marca debajo del único carácter que difiere: un espacio al final de la cadena esperada. La función era correcta; el test estaba mal.
Un test que falla solo es útil en la medida de lo que te dice. Este capítulo trata sobre cómo leer todo lo que pytest te dice y sobre cómo escribir asserts que le den algo que valga la pena decir.
Al terminar este capítulo podrás
- Explicar qué es la reescritura de aserciones y dónde se aplica y dónde no
- Leer un informe de fallos línea por línea:
>,E,where,file:liney el resumen corto - Elegir entre
==,in,is None,is Truey unassert xa secas - Leer diffs de listas, diccionarios y cadenas, y solicitar el diff completo con
-vv - Añadir un mensaje a un assert y evitar la tupla que siempre es verdadera
- Ajustar la cantidad de informe con
--tb=short,--tb=line,--tb=noy-l
Requisitos previos: instalar pytest y tu primer test.
Antes de escribir el test
Un assert es una promesa puesta por escrito. Antes de escribir uno, tres preguntas necesitan respuesta, y ninguna de ellas tiene que ver con pytest.
¿Qué se promete exactamente? Toma la función con la que concluye este capítulo, summarise(lines), que lee líneas de una factura como "pen, 12, 15.0". Su contrato en una sola frase: omite las líneas en blanco y devuelve un diccionario con la cantidad de artículos, sus nombres en el orden recibido y el total redondeado a dos decimales. Cada assert que escribirás más adelante es una cláusula de esa frase. Si no puedes formular la frase, no estás listo para escribir el test: terminarías probando cualquier cosa que el código haga por casualidad.
¿Qué debe estar preparado? Muy poco para este capítulo: el entorno virtual del capítulo uno con pytest instalado (lo compruebas con python -m pytest --version), y que el módulo bajo prueba se pueda importar desde donde ejecutes pytest —aquí invoice.py se encuentra junto a test_invoice.py y ejecutas pytest desde esa carpeta—. Sin archivos, sin red, sin fixtures.
¿Qué casos hay y qué espera cada uno? Calcula los valores esperados a mano, antes de ejecutar el código. 12 × 15.0 + 2 × 850.0 es 1880.0; ese número proviene de la aritmética, no de lo que haya impreso summarise. Si en su lugar copias la salida en el test, el test registrará el comportamiento de hoy, errores incluidos, y defenderá el error a partir de ese momento.
| Caso | Entrada | Esperado | Assert que escribirás | | --- | --- | --- | --- | | Una línea, espacios alrededor del nombre | " pen , 12, 15.0" | {"name": "pen", "qty": 12, "price": 15.0} | == sobre el diccionario completo | | Se omiten las líneas en blanco | ["pen, 12, 15.0", "", "bag, 2, 850.0"] | la cuenta es 2 | == | | El resumen completo | las mismas tres líneas | {"count": 2, "names": ["pen", "bag"], "total": 1880.0} | == sobre el diccionario completo | | Buscar un elemento que existe | "pen" | un registro, no None | is not None | | Buscar un elemento que no existe | lista vacía, "pen" | None | is None | | El total tiene sentido | las mismas tres líneas | mayor que 0 | > con un mensaje |
La última columna es de lo que trata este capítulo: para cada promesa, la comparación que la exprese con mayor precisión y que falle con el informe más útil.
Qué no probar. Que str.split divide cadenas, que round redondea, que int("12") es 12: Python ya prueba esas cosas. Tampoco te corresponde probar la redacción exacta de la salida de pytest. Una línea con formato incorrecto como "pen, twelve, 15.0" pertenece al plan, pero asegurar que se lance un error requiere pytest.raises, que corresponde al capítulo cuatro; anota la fila ahora y escribe el test entonces.
Cómo ve pytest el interior de un assert
Python estándar descarta los valores en el instante en que un assert falla. Pytest los conserva porque modifica tu archivo de test antes de ejecutarlo.
Cuando pytest importa un módulo de test, encuentra cada instrucción assert y la reescribe en código que almacena cada valor intermedio —el resultado de full_name(...), la cadena de la derecha— antes de comprobar la condición. Cuando la comprobación falla, esos valores almacenados se convierten en el informe. Esto es la reescritura de aserciones, y es la razón por la que pytest no necesita ningún assertEqual ni assertIn: un assert ordinario con un == ordinario es suficiente.
Desactívala para ver lo que hace por ti. pytest -q --assert=plain en el mismo test:
def test_full_name():
> assert full_name("ada", "lovelace") == "Ada Lovelace "
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AssertionError
test_names.py:5: AssertionError
=========================== short test summary info ============================
FAILED test_names.py::test_full_name - AssertionErrorDe vuelta a una sola palabra a secas. La reescritura se aplica a los archivos de test (test_*.py, *_test.py) y a conftest.py. No se aplica a tus otros módulos, lo cual cobra importancia en cuanto pones un assert en una función auxiliar —la sección «Cuando algo falla» muestra cómo se ve eso—.
Leer un informe de fallos, línea por línea
Un módulo pequeño, cart.py:
def subtotal(items):
return sum(price * qty for price, qty in items)
def total(items, discount):
return subtotal(items) - discountY test_cart.py, donde el segundo test espera un número incorrecto:
from cart import subtotal, total
def test_subtotal():
assert subtotal([(15.0, 2), (60.0, 1)]) == 90.0
def test_total_with_discount():
items = [(15.0, 2), (60.0, 1)]
assert total(items, 10) == 70.0Ejecuta pytest a secas:
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
collected 2 items
test_cart.py .F [100%]
=================================== FAILURES ===================================
___________________________ test_total_with_discount ___________________________
def test_total_with_discount():
items = [(15.0, 2), (60.0, 1)]
> assert total(items, 10) == 70.0
E assert 80.0 == 70.0
E + where 80.0 = total([(15.0, 2), (60.0, 1)], 10)
test_cart.py:10: AssertionError
=========================== short test summary info ============================
FAILED test_cart.py::test_total_with_discount - assert 80.0 == 70.0
========================= 1 failed, 1 passed in 0.01s ==========================Léelo desde arriba:
test_cart.py .F— un carácter por test, en orden..si pasó,Fsi falló.____ test_total_with_discount ____— el encabezado de un fallo. Cada test que falla recibe su propia sección.- Las líneas de código fuente — el cuerpo del test hasta la línea que falló.
>— la línea exacta que lanzó el error. Todo lo anterior se ejecutó sin problemas.E assert 80.0 == 70.0— el assert de nuevo, con cada expresión sustituida por su valor. El lado izquierdo fue80.0, el lado derecho70.0.E + where 80.0 = total(...)— de dónde provino un valor. En una llamada a función, pytest muestra la llamada con sus argumentos rellenados. Las llamadas anidadas producen líneaswhereanidadas, cada una con mayor sangría.test_cart.py:10: AssertionError— archivo y línea, listos para saltar a ellos en tu editor, y el tipo de excepción.short test summary info— una línea por fallo,FAILED ruta::nombre_del_test - primera línea del error. Con muchos fallos, lee esta lista primero.- La última línea — los recuentos y el tiempo.
¿Por qué leer en este orden? Porque cada línea estrecha la búsqueda. El resumen te indica qué tests; la línea > qué afirmación; la línea E qué lado está desviado; la línea where qué llamada lo produjo. Solo entonces abres el código, y lo abres en la función correcta.
A veces la primera línea E dice AssertionError: assert ... y a veces simplemente assert .... Es el mismo fallo; el prefijo se mantiene cuando los valores contienen comillas.
Las comparaciones que más escribirás
Cualquier expresión puede ir después de assert, y pytest explica las más habituales. users.py:
def find_user(users, email):
for user in users:
if user["email"] == email:
return user
return None
def active_emails(users):
return [u["email"] for u in users if u["active"]]Cuatro tests, cuatro tipos de comparación:
from users import active_emails, find_user
ADA = {"email": "[email protected]", "active": True}
ALAN = {"email": "[email protected]", "active": False}
def test_equal():
assert active_emails([ADA, ALAN]) == ["[email protected]", "[email protected]"]
def test_in():
assert "[email protected]" in active_emails([ADA, ALAN])
def test_is_not_none():
assert find_user([ADA], "[email protected]") is not None
def test_greater():
assert len(active_emails([ADA, ALAN])) > 1Los cuatro fallan. Las líneas > y E de cada uno, recortadas de una ejecución de pytest -q:
> assert active_emails([ADA, ALAN]) == ["[email protected]", "[email protected]"]
E AssertionError: assert ['[email protected]'] == ['ada@[email protected]']
E
E Right contains one more item: '[email protected]'
E Use -v to get more diff
> assert "[email protected]" in active_emails([ADA, ALAN])
E AssertionError: assert '[email protected]' in ['[email protected]']
E + where ['[email protected]'] = active_emails([{'email': '[email protected]', 'active': True}, {'email': '[email protected]', 'active': False}])
> assert find_user([ADA], "[email protected]") is not None
E AssertionError: assert None is not None
E + where None = find_user([{'email': '[email protected]', 'active': True}], '[email protected]')
> assert len(active_emails([ADA, ALAN])) > 1
E AssertionError: assert 1 > 1
E + where 1 = len(['[email protected]'])
E + where ['[email protected]'] = active_emails([{'email': '[email protected]', 'active': True}, {'email': '[email protected]', 'active': False}])Cada uno dice algo diferente:
==compara valores. En listas, pytest también indica cómo difieren: el lado derecho tiene un elemento más. (active_emailshace bien en dejar fuera a Alan; la expectativa de este test era incorrecta).incomprueba la pertenencia y muestra el contenedor completo para que puedas ver qué había en su lugar.is None/is not Nonecomprueba la identidad con el único objetoNone.find_userdevolvióNoneporque la búsqueda distingue entre mayúsculas y minúsculas.<,>,<=,>=funcionan tal como están escritos. Dos líneaswhere:1provino delen(...), y esa lista deactive_emails(...).
¿Por qué importa la elección, si a cualquier forma se le puede hacer fallar? Porque el informe se construye a partir de la expresión que escribiste. La forma incorrecta de preguntar «¿está Alan en la lista?»:
def test_count():
emails = ["[email protected]"]
assert emails.count("[email protected]") > 0> assert emails.count("[email protected]") > 0
E AssertionError: assert 0 > 0
E + where 0 = <built-in method count of list object at 0x7d9189cec640>('[email protected]')
E + where <built-in method count of list object at 0x7d9189cec640> = ['[email protected]'].countEl titular principal es assert 0 > 0, y la lista que te interesa queda enterrada al final del segundo where. La forma correcta, assert "[email protected]" in emails, pone la lista en el titular principal. Escribe el assert como la frase que tenías en mente, y pytest explicará el fallo en esos mismos términos.
Truthiness: assert x no es assert x == True
assert x pasa siempre que x sea truthy —cualquier cosa excepto False, None, 0, "" y contenedores vacíos—. A menudo eso es exactamente lo que quieres; a veces oculta un error.
rules.py tiene dos funciones. overdue es correcta; is_adult tiene un error: devuelve cadenas en lugar de booleanos:
def overdue(invoices):
return [i for i in invoices if i["days"] > 30]
def is_adult(age):
if age >= 18:
return "yes"
return "no"from rules import is_adult, overdue
def test_finds_overdue():
assert overdue([{"id": 1, "days": 12}, {"id": 2, "days": 30}])
def test_child_is_not_adult():
assert not is_adult(12)
def test_adult_truthy():
assert is_adult(40)
def test_adult_is_true():
assert is_adult(40) is Truepytest -q, con los tres fallos reducidos a sus líneas E:
FF.F [100%]
E AssertionError: assert []
E + where [] = overdue([{'id': 1, 'days': 12}, {'id': 2, 'days': 30}])
E AssertionError: assert not 'no'
E + where 'no' = is_adult(12)
E AssertionError: assert 'yes' is True
E + where 'yes' = is_adult(40)
=========================== short test summary info ============================
FAILED test_rules.py::test_finds_overdue - AssertionError: assert []
FAILED test_rules.py::test_child_is_not_adult - AssertionError: assert not 'no'
FAILED test_rules.py::test_adult_is_true - AssertionError: assert 'yes' is True
3 failed, 1 passed in 0.01sEl primer fallo: incluso un assert some_list a secas recibe un informe útil. assert [] dice que la lista vino vacía, no incorrecta. Treinta días no es más de treinta, por lo que el código es correcto y los datos del test eran los equivocados: aprendido a partir de una sola línea.
Ahora el punto en FF.F. test_adult_truthy pasó. is_adult devuelve la cadena "yes", que es truthy, por lo que una función defectuosa satisfizo a assert is_adult(40). "no" también es truthy, razón por la cual assert not is_adult(12) lo detectó, pero solo por suerte según el caso que se te ocurrió probar.
Entonces, ¿assert x == True? Detecta "yes", pero tiene su propia trampa: en Python 1 == True y 0 == False. stock.py:
def in_stock(qty):
return qty and qty > 0from stock import in_stock
def test_zero_equals_false():
assert in_stock(0) == False
def test_zero_is_false():
assert in_stock(0) is False.F [100%]
> assert in_stock(0) is False
E assert 0 is False
E + where 0 = in_stock(0)
1 failed, 1 passed in 0.01sqty and qty > 0 devuelve 0 cuando qty es 0, no False. El test con == pasó; el test con is se dio cuenta. La regla resultante:
assert x/assert not xcuando cualquier valor truthy o falsy sea aceptable: «la lista no está vacía», «no hay mensaje de error».assert x is True/assert x is Falsecuando la función prometa unboolreal.assert x == Truecasi nunca. Es más permisivo queis Truey dice menos que unassert xa secas; los linters lo señalan como advertencia.
Cuando los valores son grandes: diffs
Para valores cortos, assert 80.0 == 70.0 cuenta toda la historia. Para una lista de cuarenta elementos o un correo de tres líneas, pytest compara ambos lados y muestra solo lo que difiere.
def test_list():
got = ["pen", "notebook", "bag", "eraser"]
assert got == ["pen", "notebook", "ruler", "eraser"]
def test_dict():
got = {"id": 7, "name": "Ada", "role": "admin", "active": True}
assert got == {"id": 7, "name": "Ada", "role": "editor", "active": True}
def test_multiline_string():
got = "Dear Ada,\nYour order 7 has shipped.\nThanks!"
assert got == "Dear Ada,\nYour order 7 has been shipped.\nThanks!"Las líneas E de los tres fallos:
E AssertionError: assert ['pen', 'note...ag', 'eraser'] == ['pen', 'note...er', 'eraser']
E
E At index 2 diff: 'bag' != 'ruler'
E Use -v to get more diff
E AssertionError: assert {'id': 7, 'na...active': True} == {'id': 7, 'na...active': True}
E
E Omitting 3 identical items, use -vv to show
E Differing items:
E {'role': 'admin'} != {'role': 'editor'}
E Use -v to get more diff
E AssertionError: assert 'Dear Ada,\nY...ped.\nThanks!' == 'Dear Ada,\nY...ped.\nThanks!'
E
E Dear Ada,
E - Your order 7 has been shipped.
E ? -----
E + Your order 7 has shipped.
E Thanks!La primera línea E se acorta con ... para que quepa; las líneas debajo aportan el detalle:
- Listas — el primer índice donde difieren, o qué lado tiene elementos sobrantes.
- Diccionarios — las claves cuyos valores difieren. Las claves idénticas se cuentan, no se imprimen.
- Cadenas — un diff línea por línea. Las líneas que empiezan con dos espacios son iguales en ambos lados.
+marca el lado izquierdo de==,-el derecho. La línea?de abajo señala los caracteres exactos.
Por lo tanto, si siempre escribes assert actual == expected, + es lo que produjo tu código y - es lo que querías. Si mezclas el orden entre tests, tendrás que detenerte a averiguar cuál es cuál cada vez; si lo mantienes, nunca tendrás que hacerlo.
Una cadena larga de una sola línea recibe el mismo trato, omitiendo la parte idéntica:
def test_receipt_line():
got = "2026-10-07 | order 1042 | 3 items | paid by card | ships to Dhaka | total 2420.00"
assert got == "2026-10-07 | order 1042 | 3 items | paid by card | ships to Dhaka | total 2402.00"E AssertionError: assert '2026-10-07 |...total 2420.00' == '2026-10-07 |...total 2402.00'
E
E Skipping 66 identical leading characters in diff, use -v to show
E - | total 2402.00
E ? -
E + | total 2420.00
E ? +Dos dígitos transpuestos al final de una línea de 80 caracteres, encontrados para ti.
-v y -vv: pedir el diff completo
Observa los avisos: Use -v to get more diff, use -vv to show. De forma predeterminada, pytest trunca las explicaciones largas, y cada -v aumenta el nivel de detalle. El test del diccionario con pytest -vv:
E AssertionError: assert {'id': 7, 'name': 'Ada', 'role': 'admin', 'active': True} == {'id': 7, 'name': 'Ada', 'role': 'editor', 'active': True}
E
E Common items:
E {'active': True, 'id': 7, 'name': 'Ada'}
E Differing items:
E {'role': 'admin'} != {'role': 'editor'}
E
E Full diff:
E {
E 'id': 7,
E 'name': 'Ada',
E - 'role': 'editor',
E ? ^ ^^^
E + 'role': 'admin',
E ? ^ + ^
E 'active': True,
E }No se acorta nada: ambos diccionarios completos, los elementos comunes y un diff con una clave por línea. ¿Por qué no usar -vv siempre? Porque en una lista de doscientos registros el diff completo tendría doscientas líneas, y el resumen predeterminado —«en el índice 2»— era justo lo que necesitabas. Empieza con el valor predeterminado; añade -vv para el test concreto cuyo resumen no sea suficiente.
Tu propio mensaje: assert cond, "msg"
Después de una coma, un assert puede recibir un mensaje, el cual pytest imprime encima de su propia explicación:
def test_everything_in_stock():
stock = {"pen": 12, "bag": 0}
for name, qty in stock.items():
assert qty > 0, f"{name} is out of stock"> assert qty > 0, f"{name} is out of stock"
E AssertionError: bag is out of stock
E assert 0 > 0Sin el mensaje verías assert 0 > 0 y tendrías que averiguar qué artículo estaba a cero. Un mensaje se gana su lugar cuando los valores por sí solos no indican qué caso falló o por qué existe la regla. El uso incorrecto es repetir lo que pytest ya muestra: "expected 70.0 but got 80.0" no aporta nada a assert 80.0 == 70.0.
La trampa de la tupla
Cuando un mensaje hace que la línea sea larga, resulta tentador envolverlo todo entre paréntesis:
def test_total():
total = 80.0
assert (total == 70.0, "total is wrong"). [100%]
=============================== warnings summary ===============================
test_trap.py:3
/home/you/shop/test_trap.py:3: PytestAssertRewriteWarning: assertion is always true, perhaps remove parentheses?
assert (total == 70.0, "total is wrong")
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
1 passed, 1 warning in 0.01sEl test pasó, aunque total es 80.0. Unos paréntesis con una coma dentro construyen una tupla de dos elementos, y una tupla no vacía siempre es truthy: el assert comprueba la tupla, no la comparación. Pytest emite una advertencia, pero una advertencia al final de una ejecución larga es fácil de pasar por alto. Elimina los paréntesis:
def test_total():
total = 80.0
assert total == 70.0, "total is wrong"> assert total == 70.0, "total is wrong"
E AssertionError: total is wrong
E assert 80.0 == 70.0Si la línea es demasiado larga, pon paréntesis únicamente alrededor del mensaje:
def test_total():
total = 80.0
assert total == 70.0, (
"total is wrong after applying the loyalty discount"
)Un assert por comportamiento
Un test que comprueba campos uno tras otro se detiene en el primero que falla. orders.py tiene dos errores: el nombre no tiene los espacios recortados y la cantidad sigue siendo una cadena de texto:
def parse_order(line):
order_id, customer, qty, price = line.split(",")
return {"id": int(order_id), "customer": customer, "qty": qty, "price": float(price)}La forma incorrecta, y luego la forma correcta:
from orders import parse_order
def test_parse_order_fields():
order = parse_order("1042, Ada ,3,15.0")
assert order["id"] == 1042
assert order["customer"] == "Ada"
assert order["qty"] == 3
assert order["price"] == 15.0
def test_parse_order():
order = parse_order("1042, Ada ,3,15.0")
assert order == {"id": 1042, "customer": "Ada", "qty": 3, "price": 15.0}> assert order["customer"] == "Ada"
E AssertionError: assert ' Ada ' == 'Ada'
E
E - Ada
E + Ada
E ? + +
> assert order == {"id": 1042, "customer": "Ada", "qty": 3, "price": 15.0}
E AssertionError: assert {'id': 1042, ...'price': 15.0} == {'id': 1042, ...'price': 15.0}
E
E Omitting 2 identical items, use -vv to show
E Differing items:
E {'customer': ' Ada '} != {'customer': 'Ada'}
E {'qty': '3'} != {'qty': 3}
E Use -v to get more diffEl primer test informa de un solo error. Lo corriges, vuelves a ejecutar y solo entonces te encuentras con el segundo. El segundo test compara todo el resultado e informa de ambos a la vez.
La pauta es un comportamiento por test, no un assert por test. «Analizar una línea produce este registro» es un comportamiento, por lo que una comparación del valor completo resulta ideal. Varios asserts están bien cuando juntos describen un único comportamiento. Lo que se debe evitar es un test que compruebe el análisis sintáctico, luego el omitir líneas en blanco y luego el cálculo del total: tres comportamientos, donde el primer fallo oculta los otros dos y el nombre del test no puede decir qué se rompió.
Cuánto informe mostrar: --tb y -l
La parte entre el encabezado y file:line es el traceback, y --tb elige cuánto mostrar de él. El test de cart.py con pytest -q --tb=short:
___________________________ test_total_with_discount ___________________________
test_cart.py:10: in test_total_with_discount
assert total(items, 10) == 70.0
E assert 80.0 == 70.0
E + where 80.0 = total([(15.0, 2), (60.0, 1)], 10)Solo la línea que falla, no la función completa. --tb=line proporciona una única ubicación por fallo:
E assert 80.0 == 70.0
+ where 80.0 = total([(15.0, 2), (60.0, 1)], 10)
/home/you/shop/test_cart.py:10: assert 80.0 == 70.0Y --tb=no deja solo el resumen:
.F [100%]
=========================== short test summary info ============================
FAILED test_cart.py::test_total_with_discount - assert 80.0 == 70.0
1 failed, 1 passed in 0.01sLa opción predeterminada, --tb=auto, es el formato largo que has visto a lo largo del capítulo. Usa --tb=no o --tb=line para ver qué falló a lo largo de una ejecución grande; luego vuelve a ejecutar un test con la opción predeterminada para ver por qué.
En sentido contrario, -l (--showlocals) añade las variables locales de la función que falla. pytest -q -l:
> assert total(items, 10) == 70.0
E assert 80.0 == 70.0
E + where 80.0 = total([(15.0, 2), (60.0, 1)], 10)
items = [(15.0, 2), (60.0, 1)]
test_cart.py:10: AssertionErrorEn un test con un bucle o varias variables intermedias, ese bloque adicional es a menudo el que explica el fallo; lo verás en la solución al final.
Un ejemplo completo
invoice.py lee líneas como "pen, 12, 15.0":
def parse_line(line):
name, qty, price = line.split(",")
return {"name": name.strip(), "qty": int(qty), "price": float(price)}
def summarise(lines):
items = [parse_line(line) for line in lines if line.strip()]
total = sum(item["qty"] * item["price"] for item in items)
return {
"count": len(items),
"names": [item["name"] for item in items],
"total": round(total, 2),
}
def find_item(items, name):
for item in items:
if item["name"] == name:
return item
return Nonetest_invoice.py es el plan de «Antes de escribir el test», una fila por test, cada uno escrito con la comparación de la última columna de la tabla:
from invoice import find_item, parse_line, summarise
LINES = ["pen, 12, 15.0", "", "bag, 2, 850.0"]
def test_parse_line_strips_and_converts():
assert parse_line(" pen , 12, 15.0") == {"name": "pen", "qty": 12, "price": 15.0}
def test_blank_lines_are_skipped():
assert summarise(LINES)["count"] == 2
def test_summary():
assert summarise(LINES) == {"count": 2, "names": ["pen", "bag"], "total": 1880.0}
def test_find_item_present():
items = [parse_line("pen, 12, 15.0")]
assert find_item(items, "pen") is not None
def test_find_item_missing():
assert find_item([], "pen") is None
def test_total_is_positive():
total = summarise(LINES)["total"]
assert total > 0, f"total should be positive, got {total}"pytest -q:
...... [100%]
6 passed in 0.01sAhora alguien «limpia» summarise para que los nombres se devuelvan ordenados, cambiando una línea por "names": sorted(item["name"] for item in items),. pytest -q --tb=short:
..F... [100%]
=================================== FAILURES ===================================
_________________________________ test_summary _________________________________
test_invoice.py:15: in test_summary
assert summarise(LINES) == {"count": 2, "names": ["pen", "bag"], "total": 1880.0}
E AssertionError: assert {'count': 2, ...otal': 1880.0} == {'count': 2, ...otal': 1880.0}
E
E Omitting 2 identical items, use -vv to show
E Differing items:
E {'names': ['bag', 'pen']} != {'names': ['pen', 'bag']}
E Use -v to get more diff
=========================== short test summary info ============================
FAILED test_invoice.py::test_summary - AssertionError: assert {'count': 2, .....
1 failed, 5 passed in 0.01sLéelo de la forma en que este capítulo enseñó. Uno de los seis tests falló, test_summary, en la línea 15. La primera línea E se acorta, por lo que el detalle está abajo: dos claves coincidieron, y names difiere —['bag', 'pen'] del código (izquierda) frente a ['pen', 'bag'] en el test (derecha)—. El orden cambió, nada más. Si eso es un error o un cambio deliberado es ahora una decisión sobre las facturas, no una cacería a través del código.
Observa también qué tests no fallaron. El recuento, las búsquedas y el total siguen pasando, por lo que el informe reduce el cambio a una sola clave de un diccionario. Esa es la recompensa de un comportamiento por test: el patrón de aciertos y fallos es en sí mismo un diagnóstico.
Cuando algo falla
E AssertionError sin valores, desde un módulo auxiliar La reescritura solo cubre los archivos de test y conftest.py. Un assert dentro de, por ejemplo, checks.py produce un simple AssertionError. Registra el módulo antes de que se importe, en la parte superior de conftest.py: pytest.register_assert_rewrite("checks"). El informe mostrará entonces assert 0 > 0 como de costumbre.
PytestAssertRewriteWarning: assertion is always true, perhaps remove parentheses? Escribiste assert (condition, "message"). Eso es una tupla, y el test nunca puede fallar. Elimina los paréntesis exteriores; envuelve solo el mensaje si la línea es larga.
AssertionError: assert None == ... o + where None = ... La función a la que llamaste devolvió None. Por lo general, tiene un camino sin return, o modifica algo in situ y no devuelve nada —list.sort() es el caso clásico—.
Use -v to get more diff / ...Full output truncated (8 lines hidden), use '-vv' to show No es un error: pytest acortó la explicación. Vuelve a ejecutar ese test individual con -vv.
Un test que pasa cuando el valor es a todas luces incorrecto Busca assert x donde x sea una cadena no vacía como "no", assert x == True con x igual a 1, o la trampa de la tupla. Un test que nunca ha fallado nunca ha demostrado que funcione: rompe el código a propósito una vez y observa cómo se pone en rojo.
Paso 4 de 6 — Predice
Comprueba lo que has entendido
El test falla. ¿Qué línea de explicación imprime pytest debajo de la primera línea E?
def test_names():
got = ["pen", "bag"]
assert got == ["pen", "bag", "ink"]- Aassert 2 == 3
- BRight contains one more item: 'ink'
- CAt index 2 diff: 'ink' != None
- DLeft contains one more item: 'ink'
total es 80.0 y, aun así, este test pasa. ¿Por qué?
def test_total():
total = 80.0
assert (total == 70.0, "total is wrong")- ALa cadena del mensaje ocupa el lugar de la condición
- B`80.0 == 70.0` es verdadero para floats
- CLos paréntesis crean una tupla de dos elementos, y una tupla no vacía siempre es verdadera
- Dpytest omite los asserts que llevan un mensaje
Siempre escribes assert actual == expected. En un diff de cadenas, ¿qué líneas son cuáles: las que empiezan con + y las que empiezan con -?
- A`+` es el valor esperado, `-` es lo que produjo el código
- BDepende de qué valor sea más largo
- C`+` es lo que produjo el código (lado izquierdo), `-` es el valor esperado (lado derecho)
- D`+` marca los caracteres añadidos en ambos valores, `-` los eliminados en ambos
Para responder necesitas una cuenta
Inicia sesión para comprobar tus respuestas
Las preguntas están arriba, y resolverlas mentalmente es lo que de verdad importa. Inicia sesión para ver las respuestas, las explicaciones y las pistas de tres niveles.
Tu turno
Crea grades.py con cuatro funciones:
letter(score)—"A"a partir de 80,"B"a partir de 65,"C"a partir de 50, en caso contrario"F"passed(score)— unboolreal,Truea partir de 50report(scores)— toma un diccionario de nombres a puntuaciones, devuelve un diccionario de nombres a letrasbest(scores)— el nombre con la puntuación más alta, oNonesi el diccionario está vacío
Primero escribe el plan de tests en forma de tabla, tal como se hizo en este capítulo: caso, entrada, esperado y el assert que utilizarás. Luego escribe test_grades.py con al menos seis tests. Usa cada uno de estos al menos una vez: == en un diccionario completo, in, is None, is True o is False, una comparación como >= y un assert con un mensaje que nombre el caso que falló.
Luego rompe cosas a propósito y lee cada informe:
- Haz que
passeddevuelvascore >= 50 and scoreen lugar de un bool. ¿Qué tests lo notan? - Mueve el límite de
"B"de 65 a 66. ¿Qué tests lo notan, cuáles no —y por qué? - Envuelve uno de tus asserts entre paréntesis junto con su mensaje. Encuentra la advertencia.
- Con el fallo 2 aplicado, ejecuta el archivo con
--tb=line, y luego el test que falla con-l.
Anota, para cada fallo provocado, la primera línea del informe que te indicó qué estaba mal.
Solución
Primero el plan. Los límites son donde el código de calificaciones suele fallar, por lo que cada límite recibe ambos lados —80 y 79, 65 y 64, 50 y 49—:
| Caso | Entrada | Esperado | Assert | | --- | --- | --- | --- | | Cada límite, ambos lados | 80, 79, 65, 64, 50, 49 | A, B, B, C, C, F | == con un mensaje que indica la puntuación | | passed es un bool real | 50, 49 | True, False | is True, is False | | Informe completo | cuatro estudiantes | el diccionario completo de letras | == sobre el diccionario | | Alguien no aprobó | los mismos cuatro | "F" entre las letras | in | | best es realmente el mejor | los mismos cuatro | ninguna puntuación por encima de la del ganador | >= con un mensaje | | Nadie a quien calificar | {} | None | is None |
grades.py:
def letter(score):
if score >= 80:
return "A"
if score >= 65:
return "B"
if score >= 50:
return "C"
return "F"
def passed(score):
return score >= 50
def report(scores):
return {name: letter(score) for name, score in scores.items()}
def best(scores):
if not scores:
return None
return max(scores, key=scores.get)test_grades.py:
from grades import best, letter, passed, report
SCORES = {"ada": 91, "alan": 64, "grace": 50, "linus": 49}
def test_letter_boundaries():
cases = [(80, "A"), (79, "B"), (65, "B"), (64, "C"), (50, "C"), (49, "F")]
for score, expected in cases:
assert letter(score) == expected, f"letter({score})"
def test_passed_returns_a_real_bool():
assert passed(50) is True
assert passed(49) is False
def test_report():
assert report(SCORES) == {"ada": "A", "alan": "C", "grace": "C", "linus": "F"}
def test_a_failing_letter_appears():
assert "F" in report(SCORES).values()
def test_best_has_the_highest_score():
top = best(SCORES)
for name, score in SCORES.items():
assert SCORES[top] >= score, f"{name} scored more than {top}"
def test_best_of_nobody():
assert best({}) is Nonepytest -q:
...... [100%]
6 passed in 0.01sFallo 1 — passed devuelve score >= 50 and score:
.F.... [100%]
> assert passed(50) is True
E assert 50 is True
E + where 50 = passed(50)
FAILED test_grades.py::test_passed_returns_a_real_bool - assert 50 is True
1 failed, 5 passed in 0.01s50 es truthy, por lo que assert passed(50) lo habría dejado pasar. is True lo detectó, y assert 50 is True nombra el valor que debería haber sido un booleano.
Fallo 2 — el límite de "B" se movió a 66, con --tb=line:
F..... [100%]
=================================== FAILURES ===================================
E AssertionError: letter(65)
assert 'C' == 'B'
- B
+ C
/home/you/grades/test_grades.py:9: AssertionError: letter(65)
=========================== short test summary info ============================
FAILED test_grades.py::test_letter_boundaries - AssertionError: letter(65)
1 failed, 5 passed in 0.01sSolo el test del límite lo notó. test_report siguió pasando porque ningún estudiante en SCORES tiene exactamente 65: un test solo cubre los valores que pones en él, razón por la cual el plan enumeraba ambos lados de cada límite. El mensaje letter(65) aparece primero y es necesario: el bucle ejecuta seis casos en la misma línea, y sin él sabrías que una letra era incorrecta pero no para qué puntuación. + C es lo que devolvió el código, - B lo que esperaba el test.
El mismo fallo con pytest -q -l test_grades.py::test_letter_boundaries termina con las variables del bucle:
cases = [(80, 'A'), (79, 'B'), (65, 'B'), (64, 'C'), (50, 'C'), (49, 'F')]
expected = 'B'
score = 65Fallo 3 — assert (best({}) is None, "no scores, no best") en la línea 32:
...... [100%]
/home/you/grades/test_grades.py:32: PytestAssertRewriteWarning: assertion is always true, perhaps remove parentheses?
6 passed, 1 warning in 0.01sSeis pasaron, y uno de ellos ya no puede fallar, sin importar lo que devuelva best. La advertencia es la única señal.
Las primeras líneas que te indicaron qué estaba mal: assert 50 is True, AssertionError: letter(65) y assertion is always true. Líneas como estas son las que buscarás primero en cada fallo a partir de ahora.
Paso 6 de 6
Reto — el quiz del capítulo
Diez preguntas de fácil a difícil. Las últimas son difíciles a propósito.
Inicia sesión para hacer el quiz