assert e come leggere i report di fallimento
Come pytest vede all'interno di un semplice assert, come leggere un report di fallimento riga per riga e quale confronto scrivere affinché il report ti dica il più possibile.
Come vuoi imparare?
Il problema che stiamo risolvendo
Ecco un controllo scritto in Python standard, in un file chiamato 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 ti dice solo che l'affermazione era falsa, e nient'altro. Cosa ha effettivamente restituito full_name? Dovresti aggiungere un print, eseguirlo di nuovo e guardare.
Inserisci la stessa riga in un test, test_names.py, ed esegui 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.01sOra puoi vedere entrambi i valori e un indicatore sotto l'unico carattere che differisce: uno spazio finale nella stringa attesa. La funzione aveva ragione; il test era sbagliato.
Un test che fallisce è utile solo in base a ciò che ti dice. Questo capitolo spiega come leggere tutto ciò che pytest ti comunica e come scrivere assert che gli diano qualcosa di utile da dire.
Alla fine di questo capitolo sarai in grado di
- Spiegare cos'è l'assertion rewriting e dove si applica e non si applica
- Leggere un report di fallimento riga per riga:
>,E,where,file:linee il riepilogo breve - Scegliere tra
==,in,is None,is Truee un sempliceassert x - Leggere i diff di liste, dizionari e stringhe, e richiedere il diff completo con
-vv - Aggiungere un messaggio a un assert ed evitare la tupla che risulta sempre vera
- Configurare il report con
--tb=short,--tb=line,--tb=noe-l
Prerequisiti: installare pytest e il tuo primo test.
Prima di scrivere il test
Un assert è una promessa messa per iscritto. Prima di digitarne uno, ci sono tre domande a cui rispondere, e nessuna riguarda pytest.
Cosa viene promesso esattamente? Prendi la funzione con cui si conclude questo capitolo, summarise(lines), che legge righe di fattura come "pen, 12, 15.0". Il suo contratto in una frase: salta le righe vuote e restituisce un dizionario con il numero di articoli, i loro nomi nell'ordine fornito e il totale arrotondato a due decimali. Ogni assert che scriverai più avanti è una clausola di quella frase. Se non riesci a formulare quella frase, non sei ancora pronto per scrivere il test — finiresti per testare qualsiasi cosa il codice faccia per caso.
Cosa deve essere pronto? Molto poco per questo capitolo: l'ambiente virtuale del capitolo uno con pytest installato (verificabile con python -m pytest --version), e il modulo sotto test importabile da dove esegui pytest — qui invoice.py si trova accanto a test_invoice.py ed esegui pytest da quella cartella. Nessun file, nessuna rete, nessuna fixture.
Quali casi, e cosa si aspetta ciascuno? Calcola i valori attesi a mano, prima di eseguire il codice. 12 × 15.0 + 2 × 850.0 fa 1880.0; quel numero viene dall'aritmetica, non da ciò che summarise ha stampato. Se invece copi l'output nel test, il test registra il comportamento odierno, inclusi i bug, e difenderà quel bug da quel momento in poi.
| Caso | Input | Atteso | Assert che scriverai | | --- | --- | --- | --- | | Una riga, spazi attorno al nome | " pen , 12, 15.0" | {"name": "pen", "qty": 12, "price": 15.0} | == sull'intero dizionario | | Le righe vuote vengono saltate | ["pen, 12, 15.0", "", "bag, 2, 850.0"] | il conteggio è 2 | == | | L'intero riepilogo | le stesse tre righe | {"count": 2, "names": ["pen", "bag"], "total": 1880.0} | == sull'intero dizionario | | Cercare un articolo che esiste | "pen" | un record, non None | is not None | | Cercare un articolo che non esiste | lista vuota, "pen" | None | is None | | Il totale ha senso | le stesse tre righe | maggiore di 0 | > con un messaggio |
L'ultima colonna è l'argomento di questo capitolo: per ogni promessa, il confronto che la esprime con la massima precisione e che fallisce producendo il report più utile.
Cosa non testare. Che str.split divida le stringhe, che round arrotondi, che int("12") sia 12 — Python testa già queste cose. Nemmeno la formulazione esatta dell'output di pytest spetta a te testarla. Una riga malformata come "pen, twelve, 15.0" appartiene al piano, ma verificare che venga sollevato un errore richiede pytest.raises, che vedremo nel capitolo quattro; scrivi subito la riga nel piano e il test in seguito.
Come pytest vede all'interno di un assert
Python standard scarta i valori nel momento in cui un assert fallisce. Pytest li conserva perché modifica il file di test prima di eseguirlo.
Quando pytest importa un modulo di test, trova ogni istruzione assert e la riscrive in codice che memorizza ciascun valore intermedio — il risultato di full_name(...), la stringa a destra — prima di controllare la condizione. Quando il controllo fallisce, quei valori memorizzati diventano il report. Questa operazione si chiama assertion rewriting, ed è il motivo per cui pytest non ha bisogno di assertEqual o assertIn: un comune assert con un comune == è sufficiente.
Disattivala per vedere cosa fa per te. Esegui pytest -q --assert=plain sullo stesso 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 - AssertionErrorSi torna a una sola parola spoglia. La riscrittura si applica ai file di test (test_*.py, *_test.py) e a conftest.py. Non si applica agli altri tuoi moduli, il che diventa rilevante non appena inserisci un assert in una funzione helper — la sezione "Quando qualcosa va storto" mostra come appare questa situazione.
Leggere un report di fallimento, riga per riga
Un piccolo modulo, cart.py:
def subtotal(items):
return sum(price * qty for price, qty in items)
def total(items, discount):
return subtotal(items) - discountE test_cart.py, dove il secondo test si aspetta il numero sbagliato:
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.0Esegui pytest senza opzioni:
============================= 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 ==========================Leggilo dall'alto:
test_cart.py .F— un carattere per ciascun test, in ordine..indica superato,Ffallito.____ test_total_with_discount ____— l'intestazione di un singolo fallimento. Ogni test che fallisce riceve una sezione dedicata.- Le righe di codice sorgente — il corpo del test fino alla riga che è fallita.
>— la riga esatta che ha sollevato l'eccezione. Tutto ciò che si trova sopra è stato eseguito senza problemi.E assert 80.0 == 70.0— di nuovo l'assert, con ciascuna espressione sostituita dal rispettivo valore. Il lato sinistro era80.0, il lato destro70.0.E + where 80.0 = total(...)— da dove proviene un valore. Per una chiamata a funzione, pytest mostra la chiamata con i suoi argomenti valorizzati. Chiamate annidate producono righewhereannidate, ciascuna con un rientro maggiore.test_cart.py:10: AssertionError— file e riga, pronti per passare direttamente all'editor, e il tipo di eccezione.short test summary info— una riga per ciascun fallimento,FAILED path::test_name - first line of the error. In presenza di molti fallimenti, leggi prima questo elenco.- L'ultima riga — i conteggi e il tempo impiegato.
Perché leggere in questo ordine? Perché ogni riga restringe la ricerca. Il riepilogo ti dice quali test; la riga > quale affermazione; la riga E quale lato non torna; la riga where quale chiamata lo ha prodotto. Solo a quel punto apri il codice — e lo apri direttamente sulla funzione giusta.
A volte la prima riga E riporta AssertionError: assert ... e altre volte semplicemente assert .... Si tratta dello stesso fallimento; il prefisso rimane quando i valori contengono caratteri di virgolette.
I confronti che scriverai più spesso
Qualsiasi espressione può seguire assert, e pytest spiega quelle più comuni. 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"]]Quattro test, quattro tipi di confronto:
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])) > 1Tutti e quattro falliscono. Le righe > ed E di ciascuno, estratte da un'esecuzione con 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}])Ciascuno dice qualcosa di diverso:
==confronta i valori. Per le liste, pytest dice anche in che modo differiscono — il lato destro ha un elemento in più. (active_emailsfa bene a escludere Alan; l'aspettativa di questo test era sbagliata.)inverifica l'appartenenza e mostra l'intero contenitore in modo da poter vedere cosa c'era invece al suo posto.is None/is not Noneverifica l'identità con l'unico oggettoNone.find_userha restituitoNoneperché la ricerca distingue tra maiuscole e minuscole.<,>,<=,>=funzionano esattamente come scritti. Due righewhere:1proviene dalen(...), e quella lista daactive_emails(...).
Perché la scelta è importante, se ogni forma può essere fatta fallire? Perché il report viene costruito a partire dall'espressione che hai scritto. Il modo sbagliato di chiedere "Alan è nella 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]'].countL'intestazione è assert 0 > 0, e la lista che ti interessa è sepolta alla fine del secondo where. Il modo corretto, assert "[email protected]" in emails, mette la lista nell'intestazione. Scrivi l'assert come la frase che avevi in mente, e pytest spiegherà il fallimento negli stessi termini.
Truthiness: assert x non è assert x == True
assert x passa ogni volta che x è truthy — qualsiasi cosa tranne False, None, 0, "" e contenitori vuoti. Spesso è esattamente ciò che vuoi; a volte nasconde un bug.
rules.py contiene due funzioni. overdue è corretta; is_adult ha un bug — restituisce stringhe anziché booleani:
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 i tre fallimenti ridotti alle sole righe 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.01sIl primo fallimento: persino un semplice assert some_list ottiene un report utile. assert [] indica che la lista è risultata vuota, non errata. Trenta giorni non è più di trenta, quindi il codice è corretto ed erano errati i dati del test — appreso da una sola riga.
Ora il punto in FF.F. test_adult_truthy è passato. is_adult restituisce la stringa "yes", che è truthy, quindi assert is_adult(40) è stato soddisfatto da una funzione affetta da un bug. Anche "no" è truthy, ed è per questo che assert not is_adult(12) lo ha intercettato — ma solo per la fortuna di aver testato proprio quel caso.
Quindi, assert x == True? Rileva "yes", ma presenta una falla tutta sua: in Python 1 == True e 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 restituisce 0 quando qty è 0, non False. Il test con == è passato; il test con is se n'è accorto. La regola che ne consegue:
assert x/assert not xquando qualsiasi valore truthy o falsy è accettabile — "la lista non è vuota", "non c'è alcun messaggio di errore".assert x is True/assert x is Falsequando la funzione promette un verobool.assert x == Truequasi mai. È meno rigoroso diis Truee dice meno di un sempliceassert x; i linter lo segnalano.
Quando i valori sono grandi: i diff
Per valori brevi, assert 80.0 == 70.0 dice già tutto. Per una lista di quaranta elementi o un'email di tre righe, pytest confronta i due lati e mostra solo ciò che differisce.
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!"Le righe E dei tre fallimenti:
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 prima riga E è abbreviata con ... per rientrare nello spazio; le righe sottostanti contengono i dettagli:
- Liste — il primo indice in cui differiscono, oppure quale lato contiene elementi in più.
- Dizionari — le chiavi i cui valori differiscono. Le chiavi identiche vengono conteggiate, non stampate.
- Stringhe — un diff riga per riga. Le righe che iniziano con due spazi sono identiche su entrambi i lati.
+contrassegna il lato sinistro di==,-quello destro. La riga con?sottostante indica i caratteri esatti.
Quindi se scrivi sempre assert actual == expected, + è ciò che il tuo codice ha prodotto e - è ciò che desideravi. Se inverti l'ordine da un test all'altro dovrai fermarti ogni volta a capire quale sia quale; mantienilo coerente e non dovrai farlo mai.
Una lunga stringa su una sola riga riceve lo stesso trattamento, con la parte identica che viene saltata:
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 ? +Due cifre invertite alla fine di una riga di 80 caratteri, individuate al posto tuo.
-v e -vv: richiedere il diff completo
Nota i suggerimenti: Use -v to get more diff, use -vv to show. Per impostazione predefinita pytest tronca le spiegazioni lunghe, e ogni -v aumenta la verbosità. Il test del dizionario 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 }Nulla viene abbreviato: entrambi i dizionari per intero, gli elementi in comune e un diff con una chiave per riga. Perché non usare sempre -vv? Perché su una lista di duecento record il diff completo è di duecento righe, e il riepilogo predefinito — "at index 2" — è esattamente ciò di cui avevi bisogno. Inizia con l'impostazione predefinita; aggiungi -vv per quel singolo test in cui il riepilogo non basta.
Un messaggio personalizzato: assert cond, "msg"
Dopo una virgola, un assert può accettare un messaggio, che pytest stampa sopra la propria spiegazione:
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 > 0Senza il messaggio vedresti assert 0 > 0 e dovresti capire quale articolo fosse a zero. Un messaggio si guadagna il suo posto quando i soli valori non dicono quale caso sia fallito o perché la regola esista. L'uso sbagliato consiste nel ripetere ciò che pytest mostra già: "expected 70.0 but got 80.0" non aggiunge nulla a assert 80.0 == 70.0.
La trappola della tupla
Quando un messaggio rende la riga lunga, si è tentati di racchiudere l'intera istruzione tra parentesi:
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.01sIl test è passato, sebbene total sia 80.0. Le parentesi con una virgola all'interno creano una tupla di due elementi, e una tupla non vuota è sempre truthy — l'assert controlla la tupla, non il confronto. Pytest avvisa, ma un avviso in fondo a una lunga esecuzione passa facilmente inosservato. Rimuovi le parentesi:
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.0Se la riga è troppo lunga, metti le parentesi solo attorno al messaggio:
def test_total():
total = 80.0
assert total == 70.0, (
"total is wrong after applying the loyalty discount"
)Un assert per comportamento
Un test che controlla i campi uno dopo l'altro si ferma al primo che fallisce. orders.py contiene due bug — gli spazi attorno al nome non vengono rimossi e la quantità rimane una stringa:
def parse_order(line):
order_id, customer, qty, price = line.split(",")
return {"id": int(order_id), "customer": customer, "qty": qty, "price": float(price)}Il modo sbagliato, poi quello corretto:
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 diffIl primo test segnala un solo bug. Lo correggi, esegui di nuovo e solo allora incontri il secondo. Il secondo test confronta l'intero risultato e segnala entrambi contemporaneamente.
La linea guida è un comportamento per test, non un solo assert per test. "Il parsing di una riga produce questo record" è un unico comportamento, quindi un confronto sull'intero valore è l'ideale. Più assert vanno benissimo quando insieme descrivono un unico comportamento. Ciò che va evitato è un unico test che controlla il parsing, poi il fatto che le righe vuote vengano saltate, e infine il calcolo del totale — tre comportamenti diversi, in cui il primo fallimento nasconde gli altri due e il nome del test non riesce a dire cosa si sia rotto.
Quanto report visualizzare: --tb e -l
La parte compresa tra l'intestazione e file:line è il traceback, e --tb seleziona quanto mostrarne. Il test di 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 riga che fallisce, non l'intera funzione. --tb=line fornisce una sola posizione per fallimento:
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.0E --tb=no lascia soltanto il riepilogo:
.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.01sL'impostazione predefinita, --tb=auto, è la forma estesa che hai visto per tutto il capitolo. Usa --tb=no o --tb=line per vedere cosa è fallito in una grande esecuzione; poi riesegui un singolo test con l'impostazione predefinita per vedere il perché.
Nel verso opposto, -l (--showlocals) aggiunge le variabili locali della funzione che fallisce. 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: AssertionErrorIn un test con un ciclo o con diverse variabili intermedie, quel blocco aggiuntivo è spesso quello che spiega il fallimento — lo vedrai nella soluzione alla fine.
Un esempio completo
invoice.py legge righe come "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 è il piano di "Prima di scrivere il test", una riga della tabella per test, ciascuno scritto con il confronto dell'ultima colonna della tabella:
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.01sOra qualcuno "ripulisce" summarise in modo che i nomi vengano restituiti ordinati, modificando una riga in "names": sorted(item["name"] for item in items),. Esegui 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.01sLeggilo nel modo che questo capitolo ha spiegato. Un test su sei è fallito, test_summary, alla riga 15. La prima riga E è abbreviata, quindi il dettaglio si trova sotto: due chiavi coincidevano e names differisce — ['bag', 'pen'] dal codice (a sinistra) contro ['pen', 'bag'] nel test (a destra). È cambiato l'ordine, nient'altro. Se questo sia un bug o una modifica intenzionale è ora una decisione di business sulle fatture, non una caccia nel codice.
Nota anche quali test non sono falliti. Il conteggio, le ricerche e il totale passano ancora, quindi il report restringe la modifica a una sola chiave di un solo dizionario. Questo è il vantaggio di avere un solo comportamento per test: la combinazione di test superati e falliti costituisce essa stessa una diagnosi.
Quando qualcosa va storto
E AssertionError senza valori, da un modulo helper La riscrittura riguarda solo i file di test e conftest.py. Un assert all'interno, ad esempio, di checks.py restituisce un semplice AssertionError. Registra il modulo prima che venga importato, in cima a conftest.py: pytest.register_assert_rewrite("checks"). Il report mostrerà quindi assert 0 > 0 come di consueto.
PytestAssertRewriteWarning: assertion is always true, perhaps remove parentheses? Hai scritto assert (condition, "message"). Quella è una tupla, e il test non potrà mai fallire. Rimuovi le parentesi esterne; racchiudi tra parentesi solo il messaggio se la riga è troppo lunga.
AssertionError: assert None == ... o + where None = ... La funzione che hai chiamato ha restituito None. Di solito contiene un percorso di esecuzione senza return, oppure modifica qualcosa sul posto e non restituisce nulla — list.sort() è il caso classico.
Use -v to get more diff / ...Full output truncated (8 lines hidden), use '-vv' to show Non è un errore — pytest ha semplicemente abbreviato la spiegazione. Esegui nuovamente quel singolo test con -vv.
Un test che passa quando il valore è palesemente errato Cerca un assert x in cui x è una stringa non vuota come "no", assert x == True con x pari a 1, oppure la trappola della tupla. Un test che non è mai fallito non ha mai dimostrato di funzionare — rompi il codice di proposito una volta e guardalo diventare rosso.
Passo 4 di 6 — Prevedi
Verifica cosa hai capito
Il test fallisce. Quale riga di spiegazione stampa pytest sotto la prima riga 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 è 80.0, eppure questo test passa. Perché?
def test_total():
total = 80.0
assert (total == 70.0, "total is wrong")- ALa stringa del messaggio prende il posto della condizione
- B`80.0 == 70.0` è vero per i float
- CLe parentesi creano una tupla di due elementi, e una tupla non vuota è sempre vera
- Dpytest salta gli assert che contengono un messaggio
Scrivi sempre assert actual == expected. In un diff tra stringhe, a cosa corrispondono le righe: quelle che iniziano con + e quelle che iniziano con -?
- A`+` è il valore atteso, `-` è ciò che il codice ha prodotto
- BDipende da quale valore è più lungo
- C`+` è ciò che il codice ha prodotto (lato sinistro), `-` è il valore atteso (lato destro)
- D`+` contrassegna i caratteri aggiunti in entrambi i valori, `-` quelli rimossi in entrambi
Per rispondere serve un account
Accedi per verificare le tue risposte
Le domande sono qui sopra, e ragionarci a mente è la parte che conta. Accedi per vedere le risposte, le spiegazioni e i suggerimenti a tre livelli.
Tocca a te
Crea grades.py con quattro funzioni:
letter(score)—"A"da 80 in su,"B"da 65,"C"da 50, altrimenti"F"passed(score)— un verobool,Trueda 50 in sureport(scores)— accetta un dizionario che associa nomi a punteggi, restituisce un dizionario che associa nomi a letterebest(scores)— il nome con il punteggio più alto, oppureNoneper un dizionario vuoto
Prima scrivi il piano dei test sotto forma di tabella, come fatto in questo capitolo: caso, input, atteso e l'assert che utilizzerai. Poi scrivi test_grades.py con almeno sei test. Usa ciascuno dei seguenti almeno una volta: == su un intero dizionario, in, is None, is True o is False, un confronto come >=, e un assert con un messaggio che indica il caso che fallisce.
Poi rompi le cose di proposito e leggi ciascun report:
- Fai in modo che
passedrestituiscascore >= 50 and scoreanziché un booleano. Quali test se ne accorgono? - Sposta il limite per
"B"da 65 a 66. Quali test se ne accorgono, quali no — e perché? - Racchiudi uno dei tuoi assert tra parentesi insieme al relativo messaggio. Trova l'avviso.
- Con la rottura 2 introdotta, esegui il file con
--tb=line, poi il test che fallisce con-l.
Annota, per ciascuna rottura, la prima riga del report che ti ha comunicato cosa c'era di sbagliato.
Soluzione
Prima di tutto il piano. I valori limite sono il punto in cui il codice di valutazione di solito sbaglia, quindi ogni limite copre entrambi i lati — 80 e 79, 65 e 64, 50 e 49:
| Caso | Input | Atteso | Assert | | --- | --- | --- | --- | | Ciascun limite, entrambi i lati | 80, 79, 65, 64, 50, 49 | A, B, B, C, C, F | == con un messaggio che indica il punteggio | | passed è un vero bool | 50, 49 | True, False | is True, is False | | L'intero report | quattro studenti | l'intero dizionario di lettere | == sul dizionario | | Qualcuno non ha superato | gli stessi quattro | "F" tra le lettere | in | | best è davvero il migliore | gli stessi quattro | nessun punteggio superiore a quello del vincitore | >= con un messaggio | | Nessuno da classificare | {} | 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.01sRottura 1 — passed restituisce 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 è truthy, quindi assert passed(50) lo avrebbe lasciato passare. is True lo ha rilevato, e assert 50 is True indica il valore che avrebbe dovuto essere un bool.
Rottura 2 — il limite di "B" spostato 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 il test del limite se n'è accorto. test_report è passato comunque perché nessuno studente in SCORES ha esattamente 65 — un test copre solo i valori che ci inserisci, motivo per cui il piano elencava entrambi i lati di ogni limite. Il messaggio letter(65) compare per primo ed è necessario: il ciclo esegue sei casi sulla stessa riga e, senza di esso, sapresti che una lettera era sbagliata ma non per quale punteggio. + C è ciò che il codice ha restituito, - B ciò che il test si aspettava.
Lo stesso fallimento con pytest -q -l test_grades.py::test_letter_boundaries termina con le variabili del ciclo:
cases = [(80, 'A'), (79, 'B'), (65, 'B'), (64, 'C'), (50, 'C'), (49, 'F')]
expected = 'B'
score = 65Rottura 3 — assert (best({}) is None, "no scores, no best") alla riga 32:
...... [100%]
/home/you/grades/test_grades.py:32: PytestAssertRewriteWarning: assertion is always true, perhaps remove parentheses?
6 passed, 1 warning in 0.01sSei passati — e uno di essi non può più fallire, qualunque cosa restituisca best. L'avviso è l'unico segnale.
Le prime righe che ti hanno detto cosa c'era di sbagliato: assert 50 is True, AssertionError: letter(65) e assertion is always true. Righe come queste sono quelle che cercherai per prime in ogni fallimento da qui in avanti.
Passo 6 di 6
Sfida — il quiz del capitolo
Dieci domande dalla più facile alla più difficile. Le ultime sono difficili di proposito.
Accedi per fare il quiz