Anda menggunakan pytest
, yang memberi Anda banyak pilihan untuk berinteraksi dengan tes gagal. Ini memberi Anda opsi baris perintah dan dan beberapa kait untuk memungkinkan ini. Saya akan menjelaskan cara menggunakan masing-masing dan di mana Anda dapat membuat penyesuaian agar sesuai dengan kebutuhan debugging khusus Anda.
Saya juga akan membahas opsi-opsi yang lebih eksotis yang memungkinkan Anda untuk melewatkan pernyataan spesifik sepenuhnya, jika Anda benar-benar merasa harus melakukannya.
Tangani pengecualian, bukan menegaskan
Perhatikan bahwa tes gagal biasanya tidak menghentikan pytest; hanya jika Anda mengaktifkannya, katakan secara eksplisit untuk keluar setelah sejumlah kegagalan . Juga, tes gagal karena pengecualian dimunculkan; assert
menimbulkan AssertionError
tetapi itu bukan satu-satunya pengecualian yang akan menyebabkan tes gagal! Anda ingin mengontrol bagaimana pengecualian ditangani, bukan mengubah assert
.
Namun, pernyataan yang gagal akan mengakhiri tes individu. Itu karena sekali pengecualian dimunculkan di luar try...except
blok, Python membuka kerangka fungsi saat ini, dan tidak ada akan kembali pada itu.
Saya tidak berpikir bahwa itulah yang Anda inginkan, menilai dari deskripsi Anda tentang _assertCustom()
upaya Anda untuk menjalankan kembali pernyataan itu, tetapi saya akan membahas opsi Anda lebih jauh ke bawah.
Debugging post-mortem di pytest dengan pdb
Untuk berbagai opsi untuk menangani kegagalan dalam debugger, saya akan mulai dengan --pdb
saklar baris perintah , yang membuka prompt debugging standar ketika tes gagal (keluaran dielompokan untuk singkatnya):
$ mkdir demo
$ touch demo/__init__.py
$ cat << EOF > demo/test_foo.py
> def test_ham():
> assert 42 == 17
> def test_spam():
> int("Vikings")
> EOF
$ pytest demo/test_foo.py --pdb
[ ... ]
test_foo.py:2: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(2)test_ham()
-> assert 42 == 17
(Pdb) q
Exit: Quitting debugger
[ ... ]
Dengan saklar ini, ketika tes gagal, pytest memulai sesi debugging post-mortem . Ini pada dasarnya tepat seperti yang Anda inginkan; untuk menghentikan kode pada titik pengujian yang gagal dan buka debugger untuk melihat keadaan pengujian Anda. Anda dapat berinteraksi dengan variabel lokal tes, global, dan lokal dan global dari setiap frame di stack.
Di sini pytest memberi Anda kontrol penuh apakah akan keluar atau tidak setelah titik ini: jika Anda menggunakan q
perintah berhenti maka pytest juga keluar dari proses, menggunakan c
for continue akan mengembalikan kontrol ke pytest dan tes berikutnya dijalankan.
Menggunakan debugger alternatif
Anda tidak terikat dengan pdb
debugger untuk ini; Anda dapat mengatur debugger yang berbeda dengan --pdbcls
sakelar. Setiap implementasi yang pdb.Pdb()
kompatibel akan bekerja, termasuk implementasi debugger IPython , atau sebagian besar debugger Python lainnya ( debugger pudb mengharuskan -s
switch digunakan, atau plugin khusus ). Switch mengambil modul dan kelas, misalnya untuk menggunakan pudb
Anda bisa menggunakan:
$ pytest -s --pdb --pdbcls=pudb.debugger:Debugger
Anda bisa menggunakan fitur ini untuk menulis kelas wrapper sekitar Anda sendiri Pdb
yang hanya mengembalikan segera jika kegagalan tertentu bukanlah sesuatu yang Anda tertarik. pytest
Menggunakan Pdb()
persis seperti pdb.post_mortem()
melakukan :
p = Pdb()
p.reset()
p.interaction(None, t)
Di sini, t
adalah objek traceback . Ketika p.interaction(None, t)
kembali, pytest
lanjutkan dengan tes berikutnya, kecuali p.quitting
diatur ke True
(pada titik mana pytest kemudian keluar).
Berikut adalah contoh implementasi yang mencetak bahwa kami menolak untuk debug dan segera kembali, kecuali jika tes dinaikkan ValueError
, disimpan sebagai demo/custom_pdb.py
:
import pdb, sys
class CustomPdb(pdb.Pdb):
def interaction(self, frame, traceback):
if sys.last_type is not None and not issubclass(sys.last_type, ValueError):
print("Sorry, not interested in this failure")
return
return super().interaction(frame, traceback)
Ketika saya menggunakan ini dengan demo di atas, ini adalah output (sekali lagi, elided for brevity):
$ pytest test_foo.py -s --pdb --pdbcls=demo.custom_pdb:CustomPdb
[ ... ]
def test_ham():
> assert 42 == 17
E assert 42 == 17
test_foo.py:2: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Sorry, not interested in this failure
F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../test_foo.py(4)test_spam()
-> int("Vikings")
(Pdb)
Introspeksi di atas sys.last_type
untuk menentukan apakah kegagalan itu 'menarik'.
Namun, saya tidak bisa merekomendasikan opsi ini kecuali Anda ingin menulis debugger Anda sendiri menggunakan tkInter atau yang serupa. Perhatikan bahwa itu adalah tugas besar.
Kegagalan penyaringan; pilih dan pilih kapan harus membuka debugger
Tingkat berikutnya adalah pytest debugging dan interaksi kait ; ini adalah poin kait untuk penyesuaian perilaku, untuk menggantikan atau meningkatkan bagaimana pytest biasanya menangani hal-hal seperti menangani pengecualian atau memasukkan debugger melalui pdb.set_trace()
atau breakpoint()
(Python 3.7 atau yang lebih baru).
Implementasi internal dari hook ini juga bertanggung jawab untuk mencetak >>> entering PDB >>>
banner di atas, jadi menggunakan hook ini untuk mencegah debugger berjalan berarti Anda tidak akan melihat output ini sama sekali. Anda dapat memiliki hook sendiri kemudian mendelegasikan ke hook asli ketika kegagalan tes 'menarik', dan karenanya kegagalan pengujian filter terlepas dari debugger yang Anda gunakan! Anda dapat mengakses implementasi internal dengan mengaksesnya dengan nama ; plugin kait internal untuk ini bernama pdbinvoke
. Untuk mencegahnya berjalan Anda harus membatalkan registrasi tetapi menyimpan referensi apakah kami dapat memanggilnya langsung sesuai kebutuhan.
Berikut adalah contoh implementasi dari pengait tersebut; Anda dapat menempatkan ini di salah satu lokasi tempat plugin diambil ; Saya memasukkannya ke demo/conftest.py
:
import pytest
@pytest.hookimpl(trylast=True)
def pytest_configure(config):
# unregister returns the unregistered plugin
pdbinvoke = config.pluginmanager.unregister(name="pdbinvoke")
if pdbinvoke is None:
# no --pdb switch used, no debugging requested
return
# get the terminalreporter too, to write to the console
tr = config.pluginmanager.getplugin("terminalreporter")
# create or own plugin
plugin = ExceptionFilter(pdbinvoke, tr)
# register our plugin, pytest will then start calling our plugin hooks
config.pluginmanager.register(plugin, "exception_filter")
class ExceptionFilter:
def __init__(self, pdbinvoke, terminalreporter):
# provide the same functionality as pdbinvoke
self.pytest_internalerror = pdbinvoke.pytest_internalerror
self.orig_exception_interact = pdbinvoke.pytest_exception_interact
self.tr = terminalreporter
def pytest_exception_interact(self, node, call, report):
if not call.excinfo. errisinstance(ValueError):
self.tr.write_line("Sorry, not interested!")
return
return self.orig_exception_interact(node, call, report)
Plugin di atas menggunakan internal TerminalReporter
Plugin untuk menulis baris ke terminal; ini membuat output lebih bersih ketika menggunakan format status uji kompak standar, dan memungkinkan Anda menulis hal-hal ke terminal bahkan dengan menangkap keluaran diaktifkan.
Contoh mendaftarkan objek plugin dengan pytest_exception_interact
kait melalui kait lain pytest_configure()
, tetapi pastikan itu berjalan cukup terlambat (menggunakan @pytest.hookimpl(trylast=True)
) untuk dapat membatalkan pendaftaran pdbinvoke
plugin internal . Ketika kait disebut, contoh menguji terhadap call.exceptinfo
objek ; Anda juga dapat memeriksa simpul atau laporan juga.
Dengan kode contoh di atas diterapkan demo/conftest.py
, test_ham
kegagalan pengujian diabaikan, hanya test_spam
kegagalan pengujian, yang menimbulkan ValueError
, menghasilkan pembukaan prompt debug:
$ pytest demo/test_foo.py --pdb
[ ... ]
demo/test_foo.py F
Sorry, not interested!
demo/test_foo.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(4)test_spam()
-> int("Vikings")
(Pdb)
Untuk mengulanginya, pendekatan di atas memiliki keuntungan tambahan yang bisa Anda gabungkan dengan debugger apa pun yang bekerja dengan pytest , termasuk pudb, atau debugger IPython:
$ pytest demo/test_foo.py --pdb --pdbcls=IPython.core.debugger:Pdb
[ ... ]
demo/test_foo.py F
Sorry, not interested!
demo/test_foo.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(4)test_spam()
1 def test_ham():
2 assert 42 == 17
3 def test_spam():
----> 4 int("Vikings")
ipdb>
Ini juga memiliki lebih banyak konteks tentang tes apa yang sedang dijalankan (melalui node
argumen) dan akses langsung ke pengecualian yang diajukan (via call.excinfo
ExceptionInfo
instance).
Perhatikan bahwa plugin debugger pytest tertentu (seperti pytest-pudb
atau pytest-pycharm
) mendaftarkan pytest_exception_interact
hooksp mereka sendiri . Implementasi yang lebih lengkap harus mengulang semua plugin di manajer-plugin untuk mengganti plugin yang sewenang-wenang, secara otomatis, menggunakan config.pluginmanager.list_name_plugin
dan hasattr()
untuk menguji setiap plugin.
Membuat kegagalan hilang sama sekali
Meskipun ini memberi Anda kendali penuh atas debugging pengujian yang gagal, ini tetap meninggalkan pengujian sebagai gagal bahkan jika Anda memilih untuk tidak membuka debugger untuk tes yang diberikan. Jika Anda ingin membuat kegagalan pergi sama sekali, Anda dapat menggunakan hook yang berbeda: pytest_runtest_call()
.
Saat pytest menjalankan tes, itu akan menjalankan tes melalui hook di atas, yang diharapkan untuk mengembalikan None
atau menaikkan pengecualian. Dari sini laporan dibuat, secara opsional entri log dibuat, dan jika tes gagal, pytest_exception_interact()
kait yang disebut. Jadi yang perlu Anda lakukan adalah mengubah apa yang dihasilkan oleh hook ini; bukannya pengecualian itu seharusnya tidak mengembalikan apa-apa sama sekali.
Cara terbaik untuk melakukannya adalah dengan menggunakan pembungkus kait . Pembungkus kail tidak harus melakukan pekerjaan yang sebenarnya, tetapi sebaliknya diberi kesempatan untuk mengubah apa yang terjadi pada hasil kail. Yang harus Anda lakukan adalah menambahkan baris:
outcome = yield
dalam implementasi pembungkus kait Anda dan Anda mendapatkan akses ke hasil kait , termasuk pengecualian tes via outcome.excinfo
. Atribut ini diatur ke tuple of (type, instance, traceback) jika pengecualian dimunculkan dalam tes. Atau, Anda bisa menelepon outcome.get_result()
dan menggunakan try...except
penanganan standar .
Jadi, bagaimana Anda membuat lulus ujian yang gagal? Anda memiliki 3 opsi dasar:
- Anda dapat menandai tes sebagai kegagalan yang diharapkan , dengan memanggil
pytest.xfail()
bungkusnya.
- Anda dapat menandai item sebagai dilewati , yang berpura-pura bahwa tes tidak pernah berjalan sejak awal, dengan menelepon
pytest.skip()
.
- Anda dapat menghapus pengecualian, dengan menggunakan
outcome.force_result()
metode ini ; atur hasilnya ke daftar kosong di sini (artinya: kait terdaftar hanya menghasilkan apa-apa None
), dan pengecualian dihapus seluruhnya.
Apa yang Anda gunakan terserah Anda. Pastikan untuk memeriksa hasilnya untuk tes yang dilewati dan yang diperkirakan gagal terlebih dahulu karena Anda tidak perlu menangani kasus tersebut seolah-olah tes gagal. Anda dapat mengakses pengecualian khusus yang dimunculkan oleh opsi ini melalui pytest.skip.Exception
dan pytest.xfail.Exception
.
Berikut ini contoh implementasi yang menandai tes gagal yang tidak naik ValueError
, seperti yang dilewati :
import pytest
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
outcome = yield
try:
outcome.get_result()
except (pytest.xfail.Exception, pytest.skip.Exception, pytest.exit.Exception):
raise # already xfailed, skipped or explicit exit
except ValueError:
raise # not ignoring
except (pytest.fail.Exception, Exception):
# turn everything else into a skip
pytest.skip("[NOTRUN] ignoring everything but ValueError")
Ketika dimasukkan ke dalam conftest.py
output menjadi:
$ pytest -r a demo/test_foo.py
============================= test session starts =============================
platform darwin -- Python 3.8.0, pytest-3.10.0, py-1.7.0, pluggy-0.8.0
rootdir: ..., inifile:
collected 2 items
demo/test_foo.py sF [100%]
=================================== FAILURES ===================================
__________________________________ test_spam ___________________________________
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
=========================== short test summary info ============================
FAIL demo/test_foo.py::test_spam
SKIP [1] .../demo/conftest.py:12: [NOTRUN] ignoring everything but ValueError
===================== 1 failed, 1 skipped in 0.07 seconds ======================
Saya menggunakan -r a
bendera untuk membuatnya lebih jelas yang test_ham
dilewati sekarang.
Jika Anda mengganti pytest.skip()
panggilan dengan pytest.xfail("[XFAIL] ignoring everything but ValueError")
, tes ditandai sebagai kegagalan yang diharapkan:
[ ... ]
XFAIL demo/test_foo.py::test_ham
reason: [XFAIL] ignoring everything but ValueError
[ ... ]
dan menggunakan outcome.force_result([])
tanda itu sebagaimana diteruskan:
$ pytest -v demo/test_foo.py # verbose to see individual PASSED entries
[ ... ]
demo/test_foo.py::test_ham PASSED [ 50%]
Terserah Anda mana yang Anda rasa paling cocok untuk digunakan. Untuk skip()
dan xfail()
saya meniru format pesan standar (diawali dengan [NOTRUN]
atau [XFAIL]
) tetapi Anda bebas menggunakan format pesan lain yang Anda inginkan.
Dalam ketiga kasus, pytest tidak akan membuka debugger untuk pengujian yang hasilnya Anda ubah menggunakan metode ini.
Mengubah pernyataan pernyataan individu
Jika Anda ingin mengubah assert
tes dalam suatu tes , maka Anda mempersiapkan diri untuk pekerjaan yang jauh lebih banyak. Ya, ini secara teknis memungkinkan, tetapi hanya dengan menulis ulang kode yang akan dieksekusi oleh Python pada waktu kompilasi .
Ketika Anda menggunakan pytest
, ini sebenarnya sudah dilakukan . Pytest menulis ulang assert
pernyataan untuk memberi Anda lebih banyak konteks ketika pernyataan Anda gagal ; lihat posting blog ini untuk ikhtisar yang baik tentang apa yang sedang dilakukan, serta _pytest/assertion/rewrite.py
kode sumbernya . Perhatikan bahwa modul itu panjangnya lebih dari 1k, dan mengharuskan Anda memahami cara kerja sintaksis abstrak Python . Jika ya, Anda dapat melakukan monkeypatch pada modul tersebut untuk menambahkan modifikasi Anda sendiri di sana, termasuk mengelilingi assert
dengan try...except AssertionError:
handler.
Namun , Anda tidak bisa hanya menonaktifkan atau mengabaikan pernyataan secara selektif, karena pernyataan selanjutnya dapat dengan mudah bergantung pada status (pengaturan objek tertentu, variabel yang ditetapkan, dll.) Yang dinyatakan tidak dilewati untuk mencegahnya. Jika tes yang menyatakan foo
tidak None
, maka pernyataan yang kemudian bergantung pada foo.bar
ada, maka Anda hanya akan mengalami di AttributeError
sana, dll. Tetaplah untuk meningkatkan kembali pengecualian, jika Anda harus pergi rute ini.
Saya tidak akan masuk ke perincian lebih lanjut tentang penulisan ulang di asserts
sini, karena saya tidak berpikir ini layak untuk diusahakan, tidak diberi jumlah pekerjaan yang terlibat, dan dengan debugging post-mortem memberi Anda akses ke keadaan tes di titik kegagalan pernyataan pula .
Perhatikan bahwa jika Anda ingin melakukan ini, Anda tidak perlu menggunakan eval()
(yang tidak akan berhasil, assert
adalah pernyataan, jadi Anda harus menggunakan exec()
sebagai gantinya), atau Anda harus menjalankan pernyataan dua kali (yang dapat menyebabkan masalah jika ekspresi yang digunakan dalam pernyataan diubah diubah). Anda akan menanamkan ast.Assert
simpul di dalam ast.Try
simpul, dan melampirkan pengendali kecuali yang menggunakan ast.Raise
simpul kosong memunculkan kembali pengecualian yang tertangkap.
Menggunakan debugger untuk melewati pernyataan pernyataan.
Debugger Python sebenarnya memungkinkan Anda melewati pernyataan , menggunakan perintah j
/jump
. Jika Anda tahu di muka bahwa pernyataan spesifik akan gagal, Anda dapat menggunakan ini untuk memotongnya. Anda dapat menjalankan tes Anda dengan --trace
, yang membuka debugger di awal setiap tes , lalu mengeluarkan a j <line after assert>
untuk melewatinya ketika debugger dijeda sebelum pernyataan tersebut.
Anda bahkan dapat mengotomatisasi ini. Menggunakan teknik di atas Anda bisa membangun plugin kustom debugger itu
- menggunakan
pytest_testrun_call()
pengait untuk menangkap AssertionError
pengecualian
- mengekstrak nomor baris 'menyinggung' dari traceback, dan mungkin dengan beberapa analisis kode sumber menentukan nomor baris sebelum dan setelah pernyataan yang diperlukan untuk menjalankan lompatan yang berhasil
- menjalankan tes lagi , tetapi kali ini menggunakan
Pdb
subclass yang menetapkan breakpoint pada baris sebelum menegaskan, dan secara otomatis mengeksekusi lompatan ke yang kedua ketika breakpoint terkena, diikuti oleh c
melanjutkan.
Atau, alih-alih menunggu pernyataan gagal, Anda dapat mengotomatiskan pengaturan breakpoints untuk masing-masing yang assert
ditemukan dalam tes (sekali lagi menggunakan analisis kode sumber, Anda dapat dengan mudah mengekstraksi nomor baris untuk ast.Assert
node dalam AST tes), jalankan tes yang dinyatakan menggunakan perintah skrip debugger, dan gunakan jump
perintah untuk melewati pernyataan itu sendiri. Anda harus melakukan tradeoff; jalankan semua tes di bawah debugger (yang lambat karena penerjemah harus memanggil fungsi jejak untuk setiap pernyataan) atau hanya menerapkan ini pada tes yang gagal dan membayar harga menjalankan kembali tes-tes tersebut dari awal.
Plugin semacam itu akan banyak pekerjaan yang harus dibuat, saya tidak akan menulis contoh di sini, sebagian karena itu tidak cocok dengan jawaban, dan sebagian karena saya tidak berpikir itu sepadan dengan waktu . Saya baru saja membuka debugger dan melakukan lompatan secara manual. Pernyataan gagal menunjukkan bug baik dalam tes itu sendiri atau kode-dalam-tes, jadi Anda mungkin juga hanya fokus pada debugging masalah.