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; assert
podnosi, AssertionError
ale 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...except
blok, 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 --pdb
przełą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 q
komendy quit, pytest również opuści bieg, użycie komendy c
kontynuuj spowoduje zwrócenie kontroli do pytest i wykonanie następnego testu.
Korzystanie z alternatywnego debugera
Nie jesteś do tego związany z pdb
debuggerem; za pomocą --pdbcls
przełą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 -s
przełącznika lub specjalnej wtyczki ). Przełącznik bierze moduł i klasę, np. Do użycia pudb
można użyć:
$ pytest -s --pdb --pdbcls=pudb.debugger:Debugger
Możesz użyć tej funkcji do napisania własnej klasy opakowania, Pdb
która po prostu zwraca natychmiast, jeśli konkretna awaria nie jest czymś, co Cię interesuje. pytest
Używa Pdb()
dokładnie tak, jak pdb.post_mortem()
robi :
p = Pdb()
p.reset()
p.interaction(None, t)
Tutaj t
jest obiekt śledzenia . Po p.interaction(None, t)
powrocie pytest
przechodzi 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_type
pozwalają 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 TerminalReporter
wtyczki 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_interact
pomocą 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ą pdbinvoke
wtyczkę. Po wywołaniu haka przykład testuje na call.exceptinfo
obiekcie ; możesz również sprawdzić węzeł lub raport .
Z powyższego kodu próbki w miejscu, w demo/conftest.py
The test_ham
niepowodzenie testu jest ignorowany, tylko test_spam
niepowodzenia 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 node
argumentu) i bezpośredni dostęp do zgłoszonego wyjątku (za pośrednictwem call.excinfo
ExceptionInfo
instancji).
Zauważ, że określone wtyczki debuggera (takie jak pytest-pudb
lub pytest-pycharm
) rejestrują własne pytest_exception_interact
punkty 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_plugin
i 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ć None
lub 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...except
obsł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.Exception
i 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.py
wyjś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 a
flagi, aby wyjaśnić, że test_ham
teraz 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ć assert
testy 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 assert
instrukcje, 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.py
kod ź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 assert
ztry...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 foo
nie jest spełniony None
, to późniejsze stwierdzenie opiera się foo.bar
na istnieniu, po prostu natkniesz się na AttributeError
tam, itp. Trzymaj się ponownie zgłaszając wyjątek, jeśli musisz iść tą drogą.
Nie będę się asserts
tutaj 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, assert
jest 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.Assert
węzeł w ast.Try
węźle i dołączyć moduł obsługi wyjątków, który używa pustego ast.Raise
wę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ć AssertionError
wyją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
Pdb
podklasy, która ustawia punkt przerwania na linii przed potwierdzeniem i automatycznie wykonuje skok do drugiej po trafieniu punktu przerwania, a następnie c
kontynuuje.
Lub zamiast czekać na niepowodzenie twierdzenia, możesz zautomatyzować ustawianie punktów przerwania dla każdego assert
znalezionego w teście (ponownie używając analizy kodu źródłowego, możesz w prosty sposób wyodrębnić numery linii dla ast.Assert
węzłów w AST testu), wykonaj potwierdzony test za pomocą poleceń skryptowych debugera i użyj jump
polecenia, 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.