Używasz pytest, co daje wiele możliwości interakcji z testami zakończonymi niepowodzeniem. Daje ci opcje wiersza poleceń i kilka zaczepów, aby to umożliwić. Wyjaśnię, jak korzystać z każdego z nich i gdzie można dokonać dostosowań, aby dopasować je do konkretnych potrzeb debugowania.
Przejdę również do bardziej egzotycznych opcji, które pozwolą ci całkowicie pominąć określone twierdzenia, jeśli naprawdę czujesz, że musisz.
Obsługuj wyjątki, a nie stwierdzaj
Zauważ, że test zakończony niepowodzeniem zwykle nie zatrzymuje zapytania; tylko jeśli włączysz jawnie polecenie zakończenia po określonej liczbie awarii . Ponadto testy nie udają się, ponieważ zgłoszony jest wyjątek; assertpodnosi, AssertionErrorale to nie jedyny wyjątek, który spowoduje niepowodzenie testu! Chcesz kontrolować sposób obsługi wyjątków, a nie zmieniać assert.
Jednak w przypadku braku assert będzie zakończyć indywidualny test. Dzieje się tak, ponieważ po zgłoszeniu wyjątku poza try...exceptblok, Python odwija bieżącą ramkę funkcji i nie ma już do niej powrotu.
Nie sądzę, że tego właśnie chcesz, sądząc po twoim opisie _assertCustom() prób wznowienia twierdzenia, ale omówię twoje opcje dalej.
Debugowanie pośmiertne w pytest z pdb
Aby wyświetlić różne opcje obsługi błędów w debuggerze, zacznę od --pdbprzełącznika wiersza polecenia , który otwiera standardowe okno debugowania, gdy test się nie powiedzie (dane wyjściowe są pobierane dla zwięzłości):
$ 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
[ ... ]
Za pomocą tego przełącznika, gdy test się nie powiedzie, pytest rozpoczyna sesję debugowania pośmiertnego . To jest dokładnie to, czego chciałeś; aby zatrzymać kod w momencie nieudanego testu i otworzyć debugger, aby sprawdzić stan testu. Możesz wchodzić w interakcje z lokalnymi zmiennymi testu, globalsami oraz locals i globalsami każdej ramki w stosie.
Tutaj pytest daje pełną kontrolę nad tym, czy wyjść po tym punkcie: jeśli użyjesz qkomendy quit, pytest również opuści bieg, użycie komendy ckontynuuj spowoduje zwrócenie kontroli do pytest i wykonanie następnego testu.
Korzystanie z alternatywnego debugera
Nie jesteś do tego związany z pdbdebuggerem; za pomocą --pdbclsprzełącznika możesz ustawić inny debugger . Działa dowolna pdb.Pdb()kompatybilna implementacja, w tym implementacja debugera IPython lub większość innych debuggerów Python ( debugger pudb wymaga użycia -sprzełącznika lub specjalnej wtyczki ). Przełącznik bierze moduł i klasę, np. Do użycia pudbmożna użyć:
$ pytest -s --pdb --pdbcls=pudb.debugger:Debugger
Możesz użyć tej funkcji do napisania własnej klasy opakowania, Pdbktóra po prostu zwraca natychmiast, jeśli konkretna awaria nie jest czymś, co Cię interesuje. pytestUżywa Pdb()dokładnie tak, jak pdb.post_mortem()robi :
p = Pdb()
p.reset()
p.interaction(None, t)
Tutaj tjest obiekt śledzenia . Po p.interaction(None, t)powrocie pytestprzechodzi do następnego testu, chyba że p.quitting jest ustawiony na True(w którym momencie pytest następnie kończy działanie).
Oto przykładowa implementacja, która drukuje, że odmawiamy debugowania i natychmiast wraca, chyba że test został zgłoszony ValueError, zapisany jako 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)
Kiedy używam tego z powyższą wersją demonstracyjną, jest to wyjście (ponownie, chcąc zwięzłości):
$ 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)
Powyższe introspekty sys.last_typepozwalają ustalić, czy awaria jest „interesująca”.
Jednak nie mogę naprawdę polecić tej opcji, chyba że chcesz napisać własny debugger za pomocą tkInter lub czegoś podobnego. Pamiętaj, że to duże przedsięwzięcie.
Błędy filtrowania; wybierz i wybierz, kiedy otworzyć debugger
Poziom Następną opcją jest pytest debugowania i interakcji haczyków ; są to punkty zaczepienia do dostosowywania zachowania, które zastępują lub poprawiają sposób, w jaki pytest zwykle obsługuje takie rzeczy, jak obsługa wyjątku lub wejście do debuggera za pomocą pdb.set_trace()lub breakpoint()(Python 3.7 lub nowszy).
Wewnętrzna implementacja tego haka jest również odpowiedzialna za wydrukowanie >>> entering PDB >>>banera powyżej, więc użycie tego haka, aby zapobiec uruchomieniu debuggera, oznacza, że nie zobaczysz w ogóle tego wyniku. Możesz mieć własny hak, a następnie delegować go do oryginalnego haka, gdy błąd testu jest „interesujący”, a więc błędy testu filtra są niezależne od używanego debugera! Możesz uzyskać dostęp do wewnętrznej implementacji, otwierając ją według nazwy ; wewnętrzna wtyczka haka do tego jest nazwana pdbinvoke. Aby uniemożliwić jego uruchomienie, musisz go wyrejestrować , ale zapisz referencję, czy możemy wywołać ją bezpośrednio w razie potrzeby.
Oto przykładowa implementacja takiego haka; możesz umieścić to w dowolnej lokalizacji, z której ładowane są wtyczki ; Wkładam to 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)
Powyższa wtyczka używa wewnętrznej TerminalReporterwtyczki do wypisywania linii do terminala; To sprawia, że dane wyjściowe są czystsze, gdy używany jest domyślny kompaktowy format statusu testu, i pozwala zapisywać rzeczy na terminalu nawet przy włączonym przechwytywaniu danych wyjściowych.
Przykład rejestruje obiekt wtyczki za pytest_exception_interactpomocą haka za pomocą innego haka, pytest_configure()ale upewniając się, że działa wystarczająco późno (za pomocą @pytest.hookimpl(trylast=True)), aby móc wyrejestrować wewnętrzną pdbinvokewtyczkę. Po wywołaniu haka przykład testuje na call.exceptinfoobiekcie ; możesz również sprawdzić węzeł lub raport .
Z powyższego kodu próbki w miejscu, w demo/conftest.pyThe test_hamniepowodzenie testu jest ignorowany, tylko test_spamniepowodzenia testu, który podnosi ValueError, prowadzi do debugowania szybkiego otwarcia:
$ 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)
Aby ponownie wykonać iterację, powyższe podejście ma tę dodatkową zaletę, że można połączyć to z dowolnym debuggerem, który działa z pytestem , w tym z pakietem Pudb lub debugerem 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>
Ma również znacznie szerszy kontekst na temat tego, jaki test był uruchamiany (za pośrednictwem nodeargumentu) i bezpośredni dostęp do zgłoszonego wyjątku (za pośrednictwem call.excinfo ExceptionInfoinstancji).
Zauważ, że określone wtyczki debuggera (takie jak pytest-pudblub pytest-pycharm) rejestrują własne pytest_exception_interactpunkty zaczepienia. Bardziej kompletna implementacja musiałaby zapętlać wszystkie wtyczki w menedżerze wtyczek, aby automatycznie zastępować dowolne wtyczki przy użyciu config.pluginmanager.list_name_plugini hasattr()testować każdą wtyczkę.
Sprawianie, że niepowodzenia całkowicie znikają
Chociaż daje to pełną kontrolę nad nieudanym debugowaniem testu, nadal pozostawia to test jako nieudany, nawet jeśli zdecydujesz się nie otwierać debugera dla danego testu. Jeśli chcesz, aby awarie odejść całkowicie, można wykorzystać inny haczyk: pytest_runtest_call().
Kiedy pytest uruchamia testy, przeprowadzi test poprzez powyższy zaczep, który powinien zwrócić Nonelub zgłosić wyjątek. Na podstawie tego tworzony jest raport, opcjonalnie tworzony jest wpis dziennika, a jeśli test się nie powiedzie, pytest_exception_interact()wywoływany jest wspomniany wyżej hook. Więc wszystko, co musisz zrobić, to zmienić wynik tego haka; zamiast wyjątku nie powinien po prostu niczego zwracać.
Najlepszym sposobem na to jest użycie owijarki hakowej . Owijarki haczyków nie muszą wykonywać rzeczywistej pracy, ale zamiast tego mają szansę zmienić to, co dzieje się z wynikiem haka. Wszystko, co musisz zrobić, to dodać wiersz:
outcome = yield
w implementacji otoki haka i uzyskujesz dostęp do wyniku haka , w tym wyjątku testowego przez outcome.excinfo. Ten atrybut jest ustawiony na krotkę (typ, instancja, traceback), jeśli w teście został zgłoszony wyjątek. Możesz też zadzwonić outcome.get_result()i skorzystać ze standardowej try...exceptobsługi.
Jak więc zaliczyć nieudany test? Masz 3 podstawowe opcje:
- Możesz oznaczyć test jako oczekiwany błąd, wywołując
pytest.xfail()opakowanie.
- Możesz oznaczyć element jako pominięty , co udaje, że test nigdy nie został uruchomiony, dzwoniąc
pytest.skip().
- Możesz usunąć wyjątek, używając
outcome.force_result()metody ; ustaw tutaj wynik na pustą listę (co oznacza, że zarejestrowany hak nie wygenerował nic oprócz None), a wyjątek zostanie całkowicie usunięty.
To, czego używasz, zależy od Ciebie. Najpierw sprawdź wynik pominiętych i oczekiwanych testów, ponieważ nie musisz zajmować się tymi przypadkami, jakby test się nie powiódł. Możesz uzyskać dostęp do specjalnych wyjątków zgłaszanych przez te opcje za pośrednictwem pytest.skip.Exceptioni pytest.xfail.Exception.
Oto przykładowa implementacja, która oznacza, że testy zakończone niepowodzeniem nie zostały podniesione ValueError, ponieważ zostały pominięte :
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")
Po umieszczeniu w conftest.pywyjściu staje się:
$ 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 ======================
Użyłem -r aflagi, aby wyjaśnić, że test_hamteraz została pominięta.
Jeśli wymienisz pytest.skip() połączenie pytest.xfail("[XFAIL] ignoring everything but ValueError"), test zostanie oznaczony jako oczekiwany błąd:
[ ... ]
XFAIL demo/test_foo.py::test_ham
reason: [XFAIL] ignoring everything but ValueError
[ ... ]
i za pomocą outcome.force_result([]) oznacza to jako przekazane:
$ pytest -v demo/test_foo.py # verbose to see individual PASSED entries
[ ... ]
demo/test_foo.py::test_ham PASSED [ 50%]
Od Ciebie zależy, który z nich najlepiej pasuje do Twojego przypadku użycia. Dlaskip() i xfail()naśladowałem standardowy format wiadomości (z prefiksem [NOTRUN]lub [XFAIL]), ale możesz swobodnie używać dowolnego innego formatu wiadomości.
We wszystkich trzech przypadkach pytest nie otworzy debugera dla testów, których wynik został zmieniony za pomocą tej metody.
Zmiana poszczególnych instrukcji asercji
Jeśli chcesz zmienić asserttesty w teście , musisz przygotować się na znacznie więcej pracy. Tak, jest to technicznie możliwe, ale tylko przez przepisanie samego kodu, który Python zamierza wykonać w czasie kompilacji .
Kiedy używasz pytest, jest to już zrobione . Pytest przepisuje assertinstrukcje, aby dać ci większy kontekst, gdy twoje twierdzenia zawiodą ; zobacz ten post na blogu, aby uzyskać dobry przegląd tego, co jest robione, a także _pytest/assertion/rewrite.pykod źródłowy . Zauważ, że ten moduł ma ponad 1 000 linii i wymaga zrozumienia, jak działają abstrakcyjne drzewa składniowe Pythona . Jeśli tak, to mógłby monkeypatch ten moduł aby dodać własne modyfikacje tam, w tym otaczającej assertztry...except AssertionError: obsługi.
Jednak nie można po prostu wyłączać lub ignorować twierdzeń wybiórczo, ponieważ kolejne instrukcje mogą łatwo zależeć od stanu (specyficzne rozmieszczenie obiektów, zestaw zmiennych itp.), Przed którym miało zabezpieczyć pominięte twierdzenie. Jeśli test potwierdzający, który foonie jest spełniony None, to późniejsze stwierdzenie opiera się foo.barna istnieniu, po prostu natkniesz się na AttributeErrortam, itp. Trzymaj się ponownie zgłaszając wyjątek, jeśli musisz iść tą drogą.
Nie będę się assertstutaj zajmował bardziej szczegółowymi informacjami na temat przepisywania , ponieważ nie sądzę, że warto to kontynuować, nie biorąc pod uwagę nakładu pracy, a debugowanie pośmiertne daje dostęp do stanu testu na i tak brak dowodu .
Zauważ, że jeśli chcesz to zrobić, nie musisz używać eval()(co i tak by nie działało, assertjest instrukcją, więc musisz użyć exec()zamiast tego), ani nie musiałbyś uruchamiać asercji dwa razy (co może prowadzić do problemów, jeśli wyrażenie użyte w asercji zmieniło stan). Zamiast tego należy osadzić ast.Assertwęzeł w ast.Trywęźle i dołączyć moduł obsługi wyjątków, który używa pustego ast.Raisewęzła, aby ponownie zgłosić przechwycony wyjątek.
Używanie debugera do pomijania instrukcji asercji.
Debuger w języku Python umożliwia pomijanie instrukcji za pomocą polecenia j/jump . Jeśli wiesz z góry, że określone twierdzenie się nie powiedzie, możesz użyć tego, aby je ominąć. Możesz uruchomić swoje testy --trace, które otwierają debugger na początku każdego testu , a następnie wydać j <line after assert>polecenie pominięcia go, gdy debuger zostanie wstrzymany tuż przed potwierdzeniem.
Możesz to nawet zautomatyzować. Korzystając z powyższych technik, możesz zbudować niestandardową wtyczkę debugera
- używa
pytest_testrun_call()haka, aby złapać AssertionErrorwyjątek
- wyodrębnia wiersz „obrażający” numer linii ze śledzenia, i być może przy pewnej analizie kodu źródłowego określa numery linii przed i po stwierdzeniu wymaganym do wykonania pomyślnego skoku
- ponownie uruchamia test , ale tym razem przy użyciu
Pdbpodklasy, która ustawia punkt przerwania na linii przed potwierdzeniem i automatycznie wykonuje skok do drugiej po trafieniu punktu przerwania, a następnie ckontynuuje.
Lub zamiast czekać na niepowodzenie twierdzenia, możesz zautomatyzować ustawianie punktów przerwania dla każdego assertznalezionego w teście (ponownie używając analizy kodu źródłowego, możesz w prosty sposób wyodrębnić numery linii dla ast.Assertwęzłów w AST testu), wykonaj potwierdzony test za pomocą poleceń skryptowych debugera i użyj jumppolecenia, aby pominąć samo potwierdzenie. Musisz dokonać kompromisu; uruchom wszystkie testy w debuggerze (co jest wolne, ponieważ interpreter musi wywoływać funkcję śledzenia dla każdej instrukcji) lub zastosuj to tylko do testów zakończonych niepowodzeniem i zapłać cenę za ponowne uruchomienie tych testów od zera.
Taka wtyczka byłaby bardzo pracochłonna do stworzenia, nie zamierzam tutaj pisać przykładu, częściowo dlatego, że i tak nie zmieściłby się w odpowiedzi, a częściowo dlatego , że nie uważam, że warto . Po prostu otworzyłem debugger i wykonałem skok ręcznie. Niepowodzenie twierdzenia oznacza błąd w samym teście lub w testowanym kodzie, więc równie dobrze możesz po prostu skupić się na debugowaniu problemu.