Kullanıyorsunuz pytest
, bu da başarısız testlerle etkileşime geçmek için geniş seçenekler sunuyor. Komut satırı seçenekleri ve bunu mümkün kılmak için birkaç kanca sunar. Her birini nasıl kullanacağınızı ve özel hata ayıklama gereksinimlerinize uyacak şekilde özelleştirmeler yapabileceğinizi açıklayacağım.
Ayrıca, gerçekten gerekli olduğunu düşünüyorsanız, belirli iddiaları tamamen atlamanıza izin verecek daha egzotik seçeneklere gideceğim.
İstisnaları ele alın, iddia değil
Başarısız bir testin normalde pytest'i durdurmadığını unutmayın; yalnızca belirli sayıda hatadan sonra çıkmasını açıkça etkinleştirdiyseniz . Ayrıca, istisna oluştuğu için testler başarısız olur; assert
yükseltir AssertionError
ama bu en test değil başarısız olmasına neden olacak tek istisna! İstisnaların nasıl değiştirileceğini değil, nasıl ele alınacağını kontrol etmek istersiniz assert
.
Ancak, başarısız assert olacak bireysel Testi bitirmek. Çünkü bir istisna bir try...except
bloğun dışına çıkarıldığında , Python geçerli fonksiyon çerçevesini açar ve buna geri dönüş yoktur.
Bunu istediğinizi düşünmüyorum _assertCustom()
, iddiayı yeniden çalıştırma girişimlerinizin açıklamasına bakarak , ama yine de seçeneklerinizi daha ayrıntılı tartışacağım.
PDB ile pytest mortem sonrası hata ayıklama
Bir hata ayıklayıcıdaki hataları işlemek için çeşitli seçenekler için , bir test başarısız olduğunda standart hata ayıklama istemini açan (satır kısaltması için çıktı) --pdb
komut satırı anahtarıyla başlayacağım :
$ 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
[ ... ]
Bu anahtarla, bir test başarısız olduğunda, protesto mortem sonrası bir hata ayıklama oturumu başlar . Bu aslında tam olarak istediğin şeydi; başarısız bir test noktasındaki kodu durdurmak ve testinizin durumuna bir göz atmak için hata ayıklayıcıyı açmak. Testin yerel değişkenleri, globaller ve yığındaki her karenin yerelleri ve globalleri ile etkileşime girebilirsiniz.
Burada pytest, bu noktadan sonra çıkıp çıkmayacağınız konusunda tam kontrol sağlar: q
quit komutunu kullanırsanız, pytest de çalışmadan çıkar, Continue için kullanmak, pytest c
için kontrolü döndürür ve bir sonraki test yürütülür.
Alternatif bir hata ayıklayıcı kullanma
Bunun için pdb
hata ayıklayıcıya bağlı değilsiniz ; --pdbcls
anahtarla farklı bir hata ayıklayıcı ayarlayabilirsiniz . IPython hata ayıklayıcı uygulaması veya diğer birçok Python hata ayıklayıcı ( pudb hata ayıklayıcı , anahtarın kullanılmasını veya özel bir eklentiyi gerektirir) dahil olmak üzere herhangi bir pdb.Pdb()
uyumlu uygulama çalışır . Anahtar bir modül ve sınıf alır, örneğin kullanmak için:-s
pudb
$ pytest -s --pdb --pdbcls=pudb.debugger:Debugger
Sen etrafında kendi sarıcı sınıf yazmak için bu özelliği kullanabilirsiniz Pdb
o basitçe hemen verir belirli başarısızlık ilgilenen edilir bir şey değilse. pytest
Kullanımları Pdb()
aynen böyle pdb.post_mortem()
yapar :
p = Pdb()
p.reset()
p.interaction(None, t)
İşte, t
bir geri izleme nesnesi . Zaman p.interaction(None, t)
döner, pytest
bir sonraki test ile birlikte devam eder, yoksa p.quitting
ayarlanır True
(nokta pytest sonra terk etme).
Burada test kaldırdı sürece baskılar dışarı biz hemen hatalarının giderilmesi ve döner azalıyorsa o bir örnek uygulamasıdır ValueError
olarak kaydedilir, 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)
Ben yukarıdaki demo ile bu kullandığınızda, bu çıktı (yine kısalık için elided):
$ 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)
Yukarıdaki sys.last_type
başarısızlığın 'ilginç' olup olmadığını belirlemek için içgözlemler .
Ancak, tkInter veya benzer bir şey kullanarak kendi hata ayıklayıcınızı yazmak istemiyorsanız bu seçeneği gerçekten öneremem. Bunun büyük bir girişim olduğunu unutmayın.
Filtreleme hataları; hata ayıklayıcının ne zaman açılacağını seç ve seç
Bir sonraki seviye, pytest hata ayıklama ve etkileşim kancalarıdır ; bunlar, pytest'in normalde bir istisna işleme veya hata ayıklayıcıya pdb.set_trace()
veya breakpoint()
(Python 3.7 veya daha yeni) yoluyla girme gibi şeyleri işleme biçimini değiştirmek veya geliştirmek için davranış özelleştirmeleri için kanca noktalarıdır .
Bu kancanın dahili uygulaması, >>> entering PDB >>>
yukarıdaki banner'ı da yazdırmaktan sorumludur , bu nedenle hata ayıklayıcının çalışmasını önlemek için bu kancayı kullanmak, bu çıktıyı hiç görmeyeceğiniz anlamına gelir. Bir test arızası 'ilginç' olduğunda kendi kancanıza sahip olabilir ve orijinal kancayı devredebilirsiniz, böylece kullandığınız hata ayıklayıcıdan bağımsız olarak filtre testi hataları ! Dahili uygulamaya adıyla erişerek erişebilirsiniz ; bunun için dahili kanca eklentisi adlandırılır pdbinvoke
. Çalışmasını önlemek için kaydını silmeniz gerekir, ancak bir referansı kaydetmeniz gerekir.
İşte böyle bir kancanın örnek bir uygulaması; bunu eklentilerin yüklendiği konumlardan herhangi birine koyabilirsiniz ; Ben koydum 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)
Yukarıdaki eklentisi, iç kullanan TerminalReporter
eklentisi terminali hatları yazmak için; bu, varsayılan kompakt test durumu formatını kullanırken çıkışı daha temiz hale getirir ve çıkış yakalama etkin olsa bile terminale bir şeyler yazmanıza olanak tanır.
Örnek, eklenti nesnesini pytest_exception_interact
başka bir kanca aracılığıyla kanca ile kaydeder pytest_configure()
, ancak @pytest.hookimpl(trylast=True)
dahili pdbinvoke
eklentinin kaydını kaldırabilecek kadar geç çalıştığından emin olur . Kanca çağrıldığında, örnek call.exceptinfo
nesneye karşı test edilir ; ayrıca düğümü veya raporu da kontrol edebilirsiniz .
Yukarıdaki örnek kod yerleştirildiğinde demo/conftest.py
, test_ham
test hatası yok sayılır, yalnızca test_spam
yükselen test hatası ValueError
hata ayıklama isteminin açılmasına neden olur:
$ 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)
Tekrarlamak gerekirse , yukarıdaki yaklaşımın bunu, pudb veya IPython hata ayıklayıcısı da dahil olmak üzere, pytest ile çalışan herhangi bir hata ayıklayıcı ile birleştirebileceğiniz ek bir avantajı vardır :
$ 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>
Ayrıca, hangi testin çalıştırıldığı ( node
argüman üzerinden ) ve (istisna yoluyla call.excinfo
ExceptionInfo
) istisnaya doğrudan erişim hakkında çok daha fazla içeriğe sahiptir .
Belirli pytest hata ayıklayıcı eklentilerinin ( pytest-pudb
veya pytest-pycharm
) kendi pytest_exception_interact
kancalarını kaydettiğini unutmayın. Daha eksiksiz bir uygulamanın, eklentileri otomatik olarak geçersiz kılmak config.pluginmanager.list_name_plugin
ve hasattr()
her eklentiyi test etmek için eklenti yöneticisindeki tüm eklentiler arasında döngü yapması gerekir .
Başarısızlıkları tamamen ortadan kaldırmak
Bu, başarısız test hata ayıklaması üzerinde tam kontrol sağlarken, belirli bir test için hata ayıklayıcıyı açmamayı seçmiş olsanız bile , test yine de başarısız olarak kalır. Eğer başarısızlıkları tamamen kaybolması için isterseniz, kullanacağım farklı bir kanca yapabilirsiniz: pytest_runtest_call()
.
Pytest testleri çalıştırdığında, testi None
bir istisna döndürmesi veya yükseltmesi beklenen yukarıdaki kanca yoluyla çalıştırır . Buradan bir rapor oluşturulur, isteğe bağlı olarak bir günlük girişi oluşturulur ve test başarısız olursa, yukarıda belirtilen pytest_exception_interact()
kanca çağrılır. Tek yapmanız gereken, bu kancanın ürettiği sonucu değiştirmek; bir istisna yerine, hiçbir şey döndürmemelidir.
Bunu yapmanın en iyi yolu bir kanca sargısı kullanmaktır . Kanca sargıları gerçek işi yapmak zorunda değildir, bunun yerine bir kancanın sonucuna ne olduğunu değiştirme şansı verilir. Tek yapmanız gereken satırı eklemektir:
outcome = yield
kanca sarmalayıcı uygulamanızda ve test istisnası da dahil olmak üzere kanca sonucuna erişebilirsiniz outcome.excinfo
. Testte bir istisna ortaya çıkarsa, bu öznitelik bir demet (tip, örnek, geri izleme) olarak ayarlanır. Alternatif olarak, outcome.get_result()
standart try...except
işlemeyi arayabilir ve kullanabilirsiniz .
Peki başarısız bir test geçişini nasıl yaparsınız? 3 temel seçeneğiniz vardır:
- Sarıcıyı arayarak testi beklenen bir hata olarak işaretleyebilirsiniz
pytest.xfail()
.
- Öğeyi atlanmış olarak işaretleyebilirsiniz, bu da arayarak testin hiçbir zaman ilk sırada yapılmadığını varsayar
pytest.skip()
.
outcome.force_result()
Yöntemi kullanarak kural dışı durumu kaldırabilirsiniz ; sonucu burada boş bir listeye ayarlayın (yani: kayıtlı kanca hiçbir şey üretmedi None
) ve istisna tamamen temizlendi.
Kullandığınız şey size kalmış. Testi başarısız olmuş gibi halletmeniz gerekmediğinden, önce atlanan ve beklenen başarısızlık testlerinin sonucunu kontrol ettiğinizden emin olun. Bu seçeneklerin pytest.skip.Exception
ve aracılığıyla özel istisnalara erişebilirsiniz pytest.xfail.Exception
.
Aşağıda ValueError
, atlanmadığı gibi yükseltilmeyen başarısız testleri işaretleyen örnek bir uygulama verilmiştir :
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")
Çıktıya konulduğunda conftest.py
:
$ 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 ======================
-r a
Bayrağı test_ham
şimdi atlanmış olanı daha net hale getirmek için kullandım .
pytest.skip()
Aramayı değiştirirseniz, pytest.xfail("[XFAIL] ignoring everything but ValueError")
test beklenen bir hata olarak işaretlenir:
[ ... ]
XFAIL demo/test_foo.py::test_ham
reason: [XFAIL] ignoring everything but ValueError
[ ... ]
ve outcome.force_result([])
işaretleri geçerken kullanarak :
$ pytest -v demo/test_foo.py # verbose to see individual PASSED entries
[ ... ]
demo/test_foo.py::test_ham PASSED [ 50%]
Hangisinin kullanım durumunuza en uygun olduğunu düşünüyorsunuz. İçin skip()
ve xfail()
standart ileti biçimini taklit ettim ( [NOTRUN]
veya ön ekine sahip [XFAIL]
), ancak istediğiniz diğer ileti biçimini kullanmakta serbestsiniz.
Her üç durumda da pytest, sonucunu bu yöntemi kullanarak değiştirdiğiniz testler için hata ayıklayıcıyı açmayacaktır.
Bireysel iddia beyanlarını değiştirme
Bir assert
test içindeki testleri değiştirmek istiyorsanız, kendinizi çok daha fazla iş için hazırlıyorsunuz. Evet, bu teknik olarak mümkündür, ancak yalnızca Python'un derleme zamanında yürüteceği kodu yeniden yazarak .
Kullandığınızda pytest
, bu aslında zaten yapılıyor . Pytest, açıklamalarınız başarısız olduğunda size daha fazla bağlam sağlamak için ifadeleri yeniden yazarassert
; kaynak kodunun yanı sıra tam olarak ne yapıldığına ilişkin iyi bir genel bakış için bu blog yayınına bakın . Bu modülün 1k satırdan uzun olduğunu ve Python'un soyut sözdizimi ağaçlarının nasıl çalıştığını anlamanız gerektiğini unutmayın . Eğer yaparsanız, olabilir çevreleyen dahil Orada kendi değişiklikler eklemek için bu modülü monkeypatch bir ile işleyici._pytest/assertion/rewrite.py
assert
try...except AssertionError:
Ancak , sonraki ifadeler kolayca atlanan bir vericinin karşı koymak istediği duruma (belirli nesne düzenlemeleri, değişkenler vb.) Bağlı olabileceğinden, seçimleri yalnızca seçerek devre dışı bırakamaz veya yok sayamazsınız. Bir assert testlerinde ise foo
edilmez None
, daha sonra bir sonraki assert dayanıyor foo.bar
varoldukları için, sonra sadece bir içine çalışacaktır AttributeError
bu yolu gitmek gerekiyorsa, yeniden yükselterek istisna vb orada Do sopa.
asserts
Burada yeniden yazma konusunda daha fazla ayrıntıya girmeyeceğim , çünkü bunun çalışmaya değer olduğunu düşünmüyorum, dahil edilen iş miktarı göz önüne alınmadı ve ölüm sonrası hata ayıklama ile testin durumuna erişmenizi sağlayın yine de iddia noktası başarısızlığı .
Bunu yapmak istiyorsanız, eval()
(yine de işe yaramaz assert
, bir deyimdir, bu yüzden exec()
bunun yerine kullanmanız gerekir ) kullanmanıza gerek yoktur ve ayrıca iddiayı iki kez çalıştırmanız gerekmez ( iddianın değiştirilme durumunda kullanıldığı ifade kullanılırsa sorunlara yol açabilir). Bunun yerine ast.Assert
düğümü bir düğümün içine gömer ve ast.Try
boş bir ast.Raise
düğüm kullanan bir dış işleyici eklersiniz ve yakalanan özel durumu yeniden yükseltirsiniz.
Onaylama ifadelerini atlamak için hata ayıklayıcıyı kullanma.
Python hata ayıklayıcısı aslında / komutunu kullanarak ifadeleri atlamanızı sağlar . Eğer biliyorsanız ön yukarı belirli bir iddiası olduğunu edecektir başarısız, bunu baypas için kullanabilir. Her bir testin başlangıcında hata ayıklayıcıyı açan testlerinizi şu şekilde çalıştırabilir , ardından hata ayıklayıcı onaylandıktan hemen önce duraklatıldığında atlamak için bir a verebilirsiniz.j
jump
--trace
j <line after assert>
Hatta bunu otomatikleştirebilirsiniz. Yukarıdaki teknikleri kullanarak özel bir hata ayıklayıcı eklentisi oluşturabilirsiniz.
- istisnayı
pytest_testrun_call()
yakalamak için kancayı kullanırAssertionError
- satırdaki 'rahatsız edici' satır numarasını izlemeden çıkarır ve belki de bazı kaynak kod analizi ile başarılı bir atlama gerçekleştirmek için gereken onaylamadan önce ve sonra satır numaralarını belirler
- testi tekrar çalıştırır , ancak bu kez
Pdb
, onaylamadan önceki satırda bir kesme noktası belirleyen ve kesme noktası vurulduğunda otomatik olarak ikinci bir atlama gerçekleştiren bir alt sınıf kullanarak c
devam eder.
Ya da bir iddianın başarısız olmasını beklemek yerine, assert
bir testte bulunan her biri için kesme noktalarını ayarlamayı otomatik hale getirebilirsiniz (yine kaynak kod analizini kullanarak, testin bir AST'sindeki ast.Assert
düğümler için önemsiz bir şekilde satır numaralarını çıkarabilirsiniz ), test edilen testi yürütebilirsiniz hata ayıklayıcı komut dosyası komutlarını kullanın jump
ve onayın kendisini atlamak için komutu kullanın. Bir ödün vermek zorunda kalacaksınız; bir hata ayıklayıcı altında tüm testleri çalıştırın (yorumlayıcı her deyim için bir izleme işlevini çağırması gerektiği için yavaştır) veya bunu yalnızca başarısız testlere uygular ve bu testleri sıfırdan tekrar çalıştırmanın bedelini öder.
Böyle bir eklenti oluşturmak için çok iş olurdu, burada bir örnek yazmayacağım, kısmen bir cevaba uymayacağı için ve kısmen de buna değdiğini düşünmediğim için . Hata ayıklayıcıyı açıp atlamayı elle yapardım. Başarısız bir iddia, testin kendisinde veya test altındaki kodda bir hata olduğunu gösterir; bu nedenle, sorunu ayıklamaya odaklanabilirsiniz.