Discovery ed esecuzione dei test — cosa esegue pytest e come scegliere
Quali file, funzioni e classi pytest considera come test, come importare il codice da una struttura con src/ e tests/, i node ID ed eseguire esattamente ciò di cui hai bisogno con -k, -x, --lf, --ff e --co.
Come vuoi imparare?
Il problema che stiamo risolvendo
Finora ogni test si trovava in un unico file, accanto al codice che verifica, e lanciare semplicemente pytest era sufficiente. I progetti reali non rimangono così piccoli. Il codice viene spostato in un pacchetto, i test finiscono in una propria cartella e l'esecuzione successiva si presenta così:
$ pytest -q
==================================== ERRORS ====================================
_____________________ ERROR collecting tests/test_cart.py ______________________
ImportError while importing test module '/home/you/shop/tests/test_cart.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/usr/lib/python3.12/importlib/__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_cart.py:1: in <module>
from shop.cart import Cart
E ModuleNotFoundError: No module named 'shop'
=========================== short test summary info ============================
ERROR tests/test_cart.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01sNon è stato eseguito nemmeno un test. Il codice è corretto; pytest semplicemente non è riuscito a trovarlo.
Una volta risolto questo problema, se ne presenta un secondo: ora ci sono decine di test, uno di essi fallisce e a ogni riesecuzione vengono lanciati di nuovo tutti. Tu vuoi eseguire solo quel test — oppure solo quelli che sono falliti l'ultima volta.
Entrambi i problemi hanno la stessa origine. Prima di eseguire qualsiasi cosa, pytest effettua la collection (la raccolta): attraversa le cartelle, importa i file e costruisce un elenco di test. Questo capitolo spiega come viene costruito questo elenco e come selezionare i test da eseguire.
Alla fine di questo capitolo sarai in grado di
- Enunciare le regole utilizzate da
pytestper decidere cosa è un test - Organizzare la struttura di un progetto con
src/etests/, permettendo ai test di importare il codice - Raggruppare i test in classi e leggere un node ID come
tests/test_cart.py::TestCart::test_total_adds_tax - Eseguire un singolo file, una singola classe, un singolo test o una selezione con
-k - Usare
-x,--maxfail,--lf,--ff,--coe--durationsnel lavoro quotidiano - Leggere il codice di uscita di
pyteste spiegare cosa significa
Prerequisiti: assert e report di fallimento.
Prima di scrivere il test
In questo capitolo la domanda prima di scrivere la prima riga di codice di test non è solo cosa testare, ma dove risiederà il test e come farà `pytest` a trovarlo. Un test che non viene mai raccolto è peggio di nessun test: sembra una protezione, ma non lo è.
1. Conosci la promessa. Il codice che testeremo è un piccolo pacchetto per un negozio:
apply_discount(price, percent)restituisce il prezzo scontato delpercentpercento, arrotondato a 2 decimali. Una percentuale al di fuori di 0–100 solleva unValueError.add_tax(price, rate=0.15)restituisce il prezzo con l'imposta aggiunta, arrotondato a 2 decimali.Cartraccoglie elementi(name, price, quantity);subtotal()moltiplica il prezzo per la quantità;total()è il subtotale comprensivo di imposta.
2. Prepara l'ambiente. Un ambiente virtuale con pytest installato; il codice organizzato in src/shop/; e — la parte centrale di questo capitolo — un modo per permettere ai test di eseguire import shop. Se manca quest'ultima parte, nient'altro ha importanza.
3. Pianifica i casi e decidi dove risiede ciascuno. Un file di test per modulo (tests/test_pricing.py rispecchia src/shop/pricing.py), e i test del carrello raggruppati in una classe. Scrivere il node ID nel piano ti costringe a deciderlo fin da subito:
| Caso | Input | Atteso | Node ID | |---|---|---|---| | Sconto normale | apply_discount(200.0, 10) | 180.0 | tests/test_pricing.py::test_discount_ten_percent | | Valore limite: nessuno sconto | apply_discount(200.0, 0) | 200.0 | tests/test_pricing.py::test_discount_zero | | Imposta predefinita | add_tax(100.0) | 115.0 | tests/test_pricing.py::test_add_tax_default_rate | | Caso limite: carrello vuoto | Cart().subtotal() | 0 | tests/test_cart.py::TestCart::test_empty_cart_subtotal | | Conteggio della quantità | 3 penne a 15.0 | 45.0 | tests/test_cart.py::TestCart::test_subtotal_counts_quantity | | Il totale include l'imposta | 1 borsa a 100.0 | 115.0 | tests/test_cart.py::TestCart::test_total_adds_tax | | Percentuale non valida | apply_discount(200.0, 150) | ValueError | più avanti — il test delle eccezioni ha un capitolo dedicato |
4. Decidi cosa non testare. Non testare round() o list.append: il comportamento nativo di Python non è una tua promessa. Non verificare che Cart.items sia una lista di tuple; quello è un dettaglio interno che potresti cambiare domani. Testa ciò che vede chi chiama il codice: subtotal() e total().
Ora il capitolo sviluppa esattamente questo piano, e ogni problema lungo il percorso è un problema di discovery.
Le regole del discovery
pytest non esegue tutto ciò che trova. Segue quattro regole, e qualsiasi cosa al di fuori di esse viene ignorata senza fiatare:
- File con nome
test_*.pyo*_test.py - Funzioni in quei file il cui nome inizia con
test - Classi il cui nome inizia con
Test— e che non hanno un metodo__init__ - Metodi all'interno di tali classi il cui nome inizia con
test
Perché avere delle regole, invece di "eseguire ogni funzione"? Perché il tuo progetto è pieno di funzioni che non sono test — funzioni helper, il codice stesso sotto test — e pytest ha bisogno di un modo per distinguerle senza che tu debba registrare nulla.
Il modo più rapido per convincersi delle regole è violarle di proposito. test_rules.py:
def test_found():
assert True
def check_not_found():
assert False
class TestGroup:
def test_method_found(self):
assert True
def helper(self):
assert False
class CartTests:
def test_ignored(self):
assert False
class TestWithInit:
def __init__(self):
self.value = 1
def test_never_runs(self):
assert self.value == 1Accanto ad esso, rules_test.py contiene un test_suffix_style che passa, e helpers.py contiene un test_in_helpers con assert False. Ora esegui pytest -v:
collecting ... collected 3 items
rules_test.py::test_suffix_style PASSED [ 33%]
test_rules.py::test_found PASSED [ 66%]
test_rules.py::TestGroup::test_method_found PASSED [100%]
=============================== warnings summary ===============================
test_rules.py:22
/home/you/disc/test_rules.py:22: PytestCollectionWarning: cannot collect test class 'TestWithInit' because it has a __init__ constructor (from: test_rules.py)
class TestWithInit:
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
========================= 3 passed, 1 warning in 0.01s =========================Ci sono cinque righe con assert False in quella cartella, e l'esecuzione è verde. Cosa è stato saltato, e perché:
check_not_found— il nome non inizia contest.TestGroup.helper— si trova all'interno di una classe di test, ma il suo nome non inizia contest. I metodi helper sono permessi; semplicemente non sono test.CartTests— il nome della classe deve iniziare conTest.TestWithInit—pytestdeve creare la classe da solo, senza argomenti, una volta per ciascun test. Non si può fare affidamento su una classe con un proprio__init__per consentirlo, quindi viene saltata — con un warning, l'unico rumore di tutta l'esecuzione.helpers.py— il nome del file non corrisponde a nessuno dei due pattern, quindi non viene nemmeno importato.
La parte silenziosa è quella pericolosa. Se scrivi per sbagliodef tset_total():, la suite rimane verde per sempre. Quando un nuovo test "passa" con sospetta facilità, verifica che sia stato effettivamente raccolto —--co, presentato più avanti, è lo strumento giusto.
Una struttura reale: src/ e tests/
shop/
├── pyproject.toml
├── src/
│ └── shop/
│ ├── __init__.py
│ ├── cart.py
│ └── pricing.py
└── tests/
├── test_cart.py
└── test_pricing.pyIl codice risiede in src/shop/; i test si trovano in tests/ e non vengono mai distribuiti insieme al pacchetto. Questo è il cosiddetto src layout. Il suo scopo è evitare che il codice possa essere importato "per sbaglio" solo perché ci si trova nella cartella del progetto — in questo modo i test lo importano nello stesso identico modo in cui farebbe un utente reale.
src/shop/pricing.py:
def apply_discount(price, percent):
if not 0 <= percent <= 100:
raise ValueError(f"percent must be between 0 and 100: {percent}")
return round(price * (100 - percent) / 100, 2)
def add_tax(price, rate=0.15):
return round(price * (1 + rate), 2)src/shop/cart.py:
from shop.pricing import add_tax
class Cart:
def __init__(self):
self.items = []
def add(self, name, price, quantity=1):
self.items.append((name, price, quantity))
def subtotal(self):
return sum(price * quantity for _, price, quantity in self.items)
def total(self):
return add_tax(self.subtotal())tests/test_pricing.py — le prime tre righe del piano:
from shop.pricing import add_tax, apply_discount
def test_discount_ten_percent():
assert apply_discount(200.0, 10) == 180.0
def test_discount_zero():
assert apply_discount(200.0, 0) == 200.0
def test_add_tax_default_rate():
assert add_tax(100.0) == 115.0Quel from shop.pricing import ... è esattamente ciò che ha generato il ModuleNotFoundError all'inizio del capitolo.
Perché shop non viene trovato
import shop ha successo solo se una qualche cartella presente in sys.path contiene una cartella shop. Il pacchetto si trova in src/, e src/ non è presente in sys.path. pytest aggiunge la cartella di ciascun file di test (in questo caso tests/) in modo che un test possa importare i file vicini — ma nulla aggiunge src/.
Il modo sbagliato è modificare sys.path all'interno del file di test:
import sys
sys.path.insert(0, "src")
from shop.cart import Cart
def test_empty_cart_subtotal():
assert Cart().subtotal() == 0Dalla cartella del progetto il test passa. Poi qualcuno lo esegue dall'interno di tests/:
$ cd tests
$ pytest -q
==================================== ERRORS ====================================
________________________ ERROR collecting test_cart.py _________________________
ImportError while importing test module '/home/you/shop/tests/test_cart.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/usr/lib/python3.12/importlib/__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
test_cart.py:5: in <module>
from shop.cart import Cart
E ModuleNotFoundError: No module named 'shop'
=========================== short test summary info ============================
ERROR test_cart.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01s"src" è un percorso relativo a ovunque tu ti trovi in quel momento, quindi il test ora dipende dalla cartella in cui digiti il comando. Inoltre, ogni file di test richiederebbe le stesse tre righe. Esistono due modi corretti.
Modo corretto 1: indicare a pytest dove si trova il codice. In pyproject.toml:
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]pythonpath aggiunge src/ a sys.path prima che venga importato qualsiasi test — risolto a partire dalla radice del progetto, non da dove ti trovi. testpaths specifica dove cercare quando digiti semplicemente pytest. Ora:
$ pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
configfile: pyproject.toml
testpaths: tests
collected 6 items
tests/test_cart.py ... [ 50%]
tests/test_pricing.py ... [100%]
============================== 6 passed in 0.01s ===============================configfile: pyproject.toml nell'intestazione è la prova che pytest ha letto le tue impostazioni. I file di configurazione avranno un capitolo dedicato più avanti; queste due righe sono tutto ciò che ti serve per il momento.
Modo corretto 2: installare il pacchetto. Se pyproject.toml descrive un pacchetto reale (con una tabella [build-system] e una tabella [project]), esegui una volta pip install -e .. Questo installa il pacchetto in modalità editable: import shop ora funziona ovunque — test, REPL, script — e le modifiche a src/ vengono recepite senza dover reinstallare. Dopo averlo fatto, pytest -q restituisce lo stesso 6 passed senza bisogno di alcun pythonpath. La maggior parte dei progetti strutturati come pacchetto adotta questo approccio; pythonpath è la scelta più rapida quando il progetto non è un pacchetto.
Raggruppare i test in classi
tests/test_cart.py inserisce le righe del carrello previste dal piano in un'unica classe:
from shop.cart import Cart
class TestCart:
def test_empty_cart_subtotal(self):
assert Cart().subtotal() == 0
def test_subtotal_counts_quantity(self):
cart = Cart()
cart.add("pen", 15.0, quantity=3)
assert cart.subtotal() == 45.0
def test_total_adds_tax(self):
cart = Cart()
cart.add("bag", 100.0)
assert cart.total() == 115.0$ pytest -q tests/test_cart.py
... [100%]
3 passed in 0.01sNon c'è alcuna classe base da cui ereditare: TestCart è una normale classe e self è solo il primo parametro richiesto. Perché farlo allora? Perché la classe assegna un unico nome a test correlati, e quel nome diventa qualcosa che puoi selezionare: eseguire "tutti i test del carrello" con un solo argomento.
Ciò a cui la classe non serve è condividere lo stato. pytest crea una nuova istanza per ogni metodo di test, quindi un valore memorizzato su self in un test non esiste più nel successivo. È una scelta deliberata: i test che dipendono dai residui di altri test passano o falliscono a seconda dell'ordine in cui vengono eseguiti. Ciascun test sopra crea la propria istanza di Cart(). Le fixture, trattate in un capitolo successivo, sono lo strumento appropriato per la configurazione condivisa.
Node ID: l'indirizzo di un test
Chiedi a pytest di elencare ciò che ha raccolto senza eseguire nulla — con --collect-only, o in breve --co:
$ pytest --co -q
tests/test_cart.py::TestCart::test_empty_cart_subtotal
tests/test_cart.py::TestCart::test_subtotal_counts_quantity
tests/test_cart.py::TestCart::test_total_adds_tax
tests/test_pricing.py::test_discount_ten_percent
tests/test_pricing.py::test_discount_zero
tests/test_pricing.py::test_add_tax_default_rate
6 tests collected in 0.01sConfrontalo con l'ultima colonna del piano: ogni riga è presente, all'indirizzo che hai scelto. Ciascuna riga è un node ID — il percorso del file, poi ::, poi la classe se presente, quindi :: e il nome del test. È la stessa stringa che compare dopo FAILED nel riepilogo dei fallimenti, quindi puoi copiarla da un fallimento e incollarla direttamente nella riga di comando.
Scegliere cosa eseguire
Per percorso o node ID
Il modo sbagliato di eseguire un singolo test è commentare gli altri, oppure rinominarli in modo che pytest li salti — per poi dimenticarsi di annullare la modifica. Il modo corretto è fornire un indirizzo a pytest:
$ pytest -q tests/test_pricing.py
... [100%]
3 passed in 0.01s
$ pytest -q tests/test_cart.py::TestCart
... [100%]
3 passed in 0.01s
$ pytest -q tests/test_cart.py::TestCart::test_total_adds_tax
. [100%]
1 passed in 0.01sUna cartella, un file, una classe, un singolo test — ogni livello del node ID restringe la selezione, senza toccare minimamente il codice.
Per nome: -k
-k accetta un'espressione e mantiene i test i cui nomi corrispondono ad essa:
$ pytest -q -k discount
.. [100%]
2 passed, 4 deselected in 0.01sLa corrispondenza è una ricerca di sottostringhe che non distingue tra maiuscole e minuscole, e i termini si combinano con and, or, not e parentesi. Racchiudi l'espressione tra virgolette in modo che la shell la passi come un unico argomento:
$ pytest -q -k "discount or tax"
.... [100%]
4 passed, 2 deselected in 0.01s
$ pytest -q -k "TestCart and not tax"
.. [100%]
2 passed, 4 deselected in 0.01sEcco perché vale la pena scegliere con cura i nomi dei test: test_discount_zero può essere trovato tramite discount, tramite zero, o tramite entrambi. Un nome come test_2 non può essere trovato da nulla.
Un dettaglio coglie tutti di sorpresa almeno una volta. -k confronta il nome del test e i nomi che si trovano sopra di esso — la sua classe e il suo file:
$ pytest --co -q -k "not cart"
tests/test_pricing.py::test_discount_ten_percent
tests/test_pricing.py::test_discount_zero
tests/test_pricing.py::test_add_tax_default_rate
3/6 tests collected (3 deselected) in 0.01sNessuno dei tre test di TestCart contiene cart nel proprio nome, eppure tutti e tre sono stati scartati — risiedono infatti in test_cart.py. Quando una selezione con -k ti sorprende, aggiungi --co e controlla prima di eseguire.
Selezionare tramite un'etichetta assegnata manualmente, anziché per nome, è lo scopo dei marker e di -m, trattati nel capitolo dodici.
Quando qualcosa fallisce
Rompiamo il codice di proposito: modifica il valore predefinito di rate in add_tax da 0.15 a 0.18. Ora due test falliscono. (--tb=no nasconde i report di fallimento così che la struttura dell'esecuzione sia visibile; hai imparato a leggere quei report nello scorso capitolo).
$ pytest -q --tb=no
..F..F [100%]
=========================== short test summary info ============================
FAILED tests/test_cart.py::TestCart::test_total_adds_tax - assert 118.0 == 115.0
FAILED tests/test_pricing.py::test_add_tax_default_rate - assert 118.0 == 115.0
2 failed, 4 passed in 0.01s-x si ferma al primo fallimento. Quando una funzione rotta fa fallire dieci test, il primo report è quello che vale la pena leggere; gli altri nove sono solo un'eco:
$ pytest -q -x --tb=no
..F
=========================== short test summary info ============================
FAILED tests/test_cart.py::TestCart::test_total_adds_tax - assert 118.0 == 115.0
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 2 passed in 0.01s--maxfail=N si ferma dopo N fallimenti. -x equivale esattamente a --maxfail=1.
--lf (last failed) riesegue solo ciò che è fallito l'ultima volta. pytest memorizza i fallimenti in una cartella .pytest_cache all'interno del progetto:
$ pytest -q --lf --tb=no
FF [100%]
=========================== short test summary info ============================
FAILED tests/test_cart.py::TestCart::test_total_adds_tax - assert 118.0 == 115.0
FAILED tests/test_pricing.py::test_add_tax_default_rate - assert 118.0 == 115.0
2 failed, 2 deselected in 0.01sSono stati eseguiti solo i due fallimenti. Ripristina l'aliquota corretta, ed eseguendo --lf verranno rilanciati quei due test, che ora passeranno. Esegui --lf un'altra volta senza più alcun fallimento residuo, e pytest stamperà run-last-failure: no previously failed tests, not deselecting items. ed eseguirà tutto.
--ff (failed first) esegue prima i fallimenti, poi tutto il resto. La risposta rapida arriva subito in cima, seguita dal controllo di sicurezza completo:
$ pytest -q --ff --tb=no
FF.... [100%]
=========================== short test summary info ============================
FAILED tests/test_cart.py::TestCart::test_total_adds_tax - assert 118.0 == 115.0
FAILED tests/test_pricing.py::test_add_tax_default_rate - assert 118.0 == 115.0
2 failed, 4 passed in 0.01sFF.... — i due fallimenti sono stati eseguiti per primi, prima dei quattro che sono passati.
Un buon ciclo di lavoro durante la correzione di un bug: pytest --lf -x finché non diventa verde, poi pytest da solo una volta. Perché quest'ultimo passaggio? Perché --lf riesegue solo ciò che era rosso — non può dirti se la tua correzione ha rotto qualcosa che prima era verde.
Output dai tuoi test: capturing e -s
Una chiamata a print all'interno di un test sembra non fare nulla. (Qui sotto --tb=short serve solo a rendere più breve il report di fallimento). pytest cattura tutto ciò che un test scrive sullo schermo e lo mostra solo se quel test fallisce:
def test_quiet():
print("subtotal is", 45.0)
assert 45.0 == 45.0
def test_loud():
print("subtotal is", 40.0)
assert 40.0 == 45.0$ pytest -q --tb=short
.F [100%]
=================================== FAILURES ===================================
__________________________________ test_loud ___________________________________
test_print.py:8: in test_loud
assert 40.0 == 45.0
E assert 40.0 == 45.0
----------------------------- Captured stdout call -----------------------------
subtotal is 40.0
=========================== short test summary info ============================
FAILED test_print.py::test_loud - assert 40.0 == 45.0
1 failed, 1 passed in 0.01sL'output del test superato è svanito; l'output del test fallito è allegato al suo report sotto la voce Captured stdout call. Con centinaia di test, è proprio questo che vuoi: silenzio quando tutto va bene, prove quando qualcosa non va. Per vedere tutto man mano che accade, -s disattiva la cattura:
$ pytest -q -s --tb=no
subtotal is 45.0
.subtotal is 40.0
F
=========================== short test summary info ============================
FAILED test_print.py::test_loud - assert 40.0 == 45.0
1 failed, 1 passed in 0.01sCodici di uscita
Quando pytest termina, restituisce un numero alla shell. I sistemi di CI decidono se il risultato è "verde" o "rosso" basandosi unicamente su quel numero:
| Codice | Significato | |---|---| | 0 | Tutti i test raccolti sono stati superati | | 1 | I test sono stati eseguiti e almeno uno è fallito | | 2 | L'esecuzione è stata interrotta — un errore durante la raccolta, o Ctrl+C | | 3 | Un errore interno all'interno di pytest o di un plugin | | 4 | Un errore di utilizzo — un'opzione sconosciuta, o un percorso o node ID non esistente | | 5 | Non è stato raccolto alcun test |
Il codice 5 merita attenzione. Un -k che non trova alcuna corrispondenza, o una cartella senza file con i nomi corretti, non è un successo — e la CI lo considera un fallimento, il che è assolutamente corretto:
$ pytest -q -k refund
6 deselected in 0.01s
$ echo $?
5Il ModuleNotFoundError all'inizio del capitolo si concludeva con Interrupted: 1 error during collection — codice di uscita 2. Un errore di raccolta non è un test che fallisce; pytest non è nemmeno arrivato al punto di eseguire i test.
Un esempio completo
Il piano è implementato ed è verde. Ora un test lento si aggiunge a tests/test_cart.py, in una propria classe:
from shop.cart import Cart
class TestBigCart:
def test_many_items(self):
cart = Cart()
for _ in range(300_000):
cart.add("pen", 1.0)
assert cart.subtotal() == 300_000.0Il file contiene ancora TestCart sopra questa classe. Per prima cosa controlla cosa vede pytest, prima di eseguire qualsiasi cosa:
$ pytest --co -q
tests/test_cart.py::TestCart::test_empty_cart_subtotal
tests/test_cart.py::TestCart::test_subtotal_counts_quantity
tests/test_cart.py::TestCart::test_total_adds_tax
tests/test_cart.py::TestBigCart::test_many_items
tests/test_pricing.py::test_discount_ten_percent
tests/test_pricing.py::test_discount_zero
tests/test_pricing.py::test_add_tax_default_rate
7 tests collected in 0.01sSette test, ciascuno con un indirizzo. Eseguili tutti e richiedi i tre più lenti:
$ pytest -q --durations=3
....... [100%]
============================= slowest 3 durations ==============================
0.04s call tests/test_cart.py::TestBigCart::test_many_items
(2 durations < 0.005s hidden. Use -vv to show these durations.)
7 passed in 0.12sSolo un test è stato abbastanza lento da comparire nell'elenco. In un progetto reale, --durations=10 è il modo in cui trovi quella manciata di test che rende lenta l'intera suite. Mentre lavori sul carrello, tralascia quello lento:
$ pytest -q -k "cart and not many"
... [100%]
3 passed, 4 deselected in 0.01sTre fattori hanno permesso a tutto questo di funzionare. pythonpath = ["src"] permette a from shop.cart import Cart di funzionare. TestBigCart viene raccolto perché il suo nome inizia con Test e non ha un metodo __init__. E -k "cart and not many" ha mantenuto esattamente i test di TestCart: cart corrispondeva a tutto ciò che si trova in test_cart.py attraverso il nome del file, e not many ha rimosso quello lento.
Quando qualcosa va storto
ModuleNotFoundError: No module named 'shop' durante la raccolta Il codice sotto test non si trova in sys.path. Con una struttura src/, aggiungi pythonpath = ["src"] sotto [tool.pytest.ini_options], oppure installa il pacchetto con pip install -e .. L'esecuzione termina con Interrupted: 1 error during collection e codice di uscita 2.
Funziona con python -m pytest ma non con pytest Anche python -m pytest inserisce la cartella corrente in sys.path; pytest da solo non lo fa. Di conseguenza, un modulo nella radice del progetto è importabile in un modo ma non nell'altro. Non fare affidamento su questa differenza: imposta pythonpath, oppure installa il pacchetto.
PytestCollectionWarning: cannot collect test class 'TestCart' because it has a __init__ constructor Le classi di test non devono definire __init__. Rimuovilo e crea ciò di cui ciascun test ha bisogno all'interno del test stesso.
no tests ran, codice di uscita 5 Nulla corrispondeva alle regole del discovery. Controlla il nome del file (test_*.py o *_test.py), i nomi delle funzioni (test...), i nomi delle classi (Test...). Se hai usato -k, potrebbe non aver trovato alcuna corrispondenza.
ERROR: not found: .../tests/test_cart.py::test_total_adds_tax Il node ID è errato. Quel test si trova all'interno di TestCart, quindi il suo indirizzo è tests/test_cart.py::TestCart::test_total_adds_tax. Copia i node ID da pytest --co -q invece di digitarli a mano. Codice di uscita 4.
ERROR: file or directory not found: tests/test_carts.py Un refuso nel percorso. Codice di uscita 4.
pytest: error: unrecognized arguments: --maxfial=2 Un'opzione scritta male; pytest --help elenca quelle valide. Codice di uscita 4.
Passo 4 di 6 — Prevedi
Verifica cosa hai capito
Questo file è salvato come test_rules.py. Quando esegui pytest -q, quanti test superati riporta l'ultima riga?
def test_found():
assert True
def check_total():
assert True
class TestGroup:
def test_method(self):
assert True
def helper(self):
assert True
class CartTests:
def test_ignored(self):
assert True- A2 passed
- B1 passed
- C3 passed
- D4 passed
Il codice si trova in src/shop/pricing.py. pyproject.toml contiene pythonpath = ["shop"] sotto [tool.pytest.ini_options]. Eppure pytest segnala ModuleNotFoundError: No module named 'shop'. Cosa c'è che non va?
# tests/test_pricing.py
from shop.pricing import add_tax
def test_add_tax_default_rate():
assert add_tax(100.0) == 115.0- AIl nome della funzione deve iniziare con `Test`
- B`pythonpath` deve puntare alla cartella che *contiene* il pacchetto `shop`: `["src"]`
- CIl nome del file di test deve terminare con `_test.py`
- D`tests/` ha bisogno di un `__init__.py` prima di poter importare qualsiasi cosa
Due test fallivano e hai corretto il bug. Quale comando riesegue solo quei due test?
- Apytest --co
- Bpytest -x
- Cpytest --ff
- Dpytest --lf
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
Una biblioteca applica tariffe per la restituzione in ritardo dei libri. Crea questo progetto:
library/
├── src/
│ └── library/
│ ├── __init__.py
│ └── fees.py
└── tests/
└── test_fees.pysrc/library/fees.py:
def late_fee(days_late, daily_rate=5):
if days_late < 0:
raise ValueError(f"days_late cannot be negative: {days_late}")
return min(days_late * daily_rate, 100)- Scrivi prima un piano di test: puntuale, qualche giorno di ritardo, esattamente al massimale, oltre il massimale, tariffa giornaliera personalizzata.
- Scrivi
tests/test_fees.pyinserendo questi casi come metodi di un'unica classe,TestLateFee. Eseguipytestsenza unpyproject.toml, leggi l'errore e controllaecho $?. - Aggiungi
pyproject.tomlin modo che i test possano importarelibrary. Porta tutto a verde. - Elenca i node ID con
--co -q. Esegui solo il test "oltre il massimale" tramite il suo node ID. - Usa
-kper eseguire solo i test relativi al massimale, poi tutto tranne loro. - Modifica il massimale in
fees.pyda100a90. Esegui la suite, poi--lf. Ripristinalo ed esegui--lf, poi tutto quanto.
Soluzione
Il piano:
| Caso | Input | Atteso | |---|---|---| | Puntuale | late_fee(0) | 0 | | Qualche giorno di ritardo | late_fee(3) | 15 | | Valore limite: esattamente al massimale | late_fee(20) | 100 | | Oltre il massimale | late_fee(30) | 100 | | Tariffa personalizzata | late_fee(3, daily_rate=2) | 6 | | Giorni negativi | late_fee(-1) | ValueError — più avanti, con pytest.raises |
tests/test_fees.py:
from library.fees import late_fee
class TestLateFee:
def test_on_time_costs_nothing(self):
assert late_fee(0) == 0
def test_three_days_late(self):
assert late_fee(3) == 15
def test_fee_reaches_cap_exactly(self):
assert late_fee(20) == 100
def test_fee_never_exceeds_cap(self):
assert late_fee(30) == 100
def test_custom_daily_rate(self):
assert late_fee(3, daily_rate=2) == 6Senza configurazione, l'esecuzione si interrompe alla raccolta con ModuleNotFoundError: No module named 'library', Interrupted: 1 error during collection, e echo $? stampa 2. Poi aggiungi pyproject.toml:
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]$ pytest -q
..... [100%]
5 passed in 0.01s
$ pytest --co -q
tests/test_fees.py::TestLateFee::test_on_time_costs_nothing
tests/test_fees.py::TestLateFee::test_three_days_late
tests/test_fees.py::TestLateFee::test_fee_reaches_cap_exactly
tests/test_fees.py::TestLateFee::test_fee_never_exceeds_cap
tests/test_fees.py::TestLateFee::test_custom_daily_rate
5 tests collected in 0.01s
$ pytest -q tests/test_fees.py::TestLateFee::test_fee_never_exceeds_cap
. [100%]
1 passed in 0.01s
$ pytest -q -k cap
.. [100%]
2 passed, 3 deselected in 0.01s
$ pytest -q -k "not cap"
... [100%]
3 passed, 2 deselected in 0.01sCon il massimale modificato a 90:
$ pytest -q --tb=no
..FF. [100%]
=========================== short test summary info ============================
FAILED tests/test_fees.py::TestLateFee::test_fee_reaches_cap_exactly - assert...
FAILED tests/test_fees.py::TestLateFee::test_fee_never_exceeds_cap - assert 9...
2 failed, 3 passed in 0.01s
$ pytest -q --lf --tb=no
FF [100%]
=========================== short test summary info ============================
FAILED tests/test_fees.py::TestLateFee::test_fee_reaches_cap_exactly - assert...
FAILED tests/test_fees.py::TestLateFee::test_fee_never_exceeds_cap - assert 9...
2 failed, 3 deselected in 0.01sRiportando il massimale a 100, pytest -q --lf restituisce 2 passed, 3 deselected, e un pytest -q completo restituisce 5 passed.
Perché queste decisioni:
- Due test per il massimale, non uno solo.
late_fee(20)si trova esattamente sul valore limite, il primo giorno in cui la tariffa raggiunge il massimale;late_fee(30)è ampiamente oltre. Gli errori off-by-one si annidano sui valori limite, quindi il limite ha un proprio test dedicato — e il secondo test dimostra che il massimale continua a valere anche oltre. - La parola
capcompare in entrambi i nomi dei test sul massimale, e solo lì. È questo che ha permesso a-k cape-k "not cap"di selezionare i test in modo netto — i nomi sono stati scelti pensando alla selezione. - Una sola classe, nessun
__init__, nessuno stato condiviso. Ciascun metodo chiama direttamentelate_fee; la classe assegna semplicemente un unico indirizzo al gruppo,tests/test_fees.py::TestLateFee. pythonpathinvece di una riga consys.pathnel file di test, così che i test passino da qualunque cartella vengano eseguiti.- L'esecuzione completa dopo
--lf.--lfdimostra che i due test rossi sono tornati verdi; solo l'esecuzione completa dimostra che gli altri tre sono ancora verdi.
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