Czy można zmienić zachowanie instrukcji aser instrukcji PyTest w Pythonie

18

Korzystam z instrukcji asercji Python w celu dopasowania rzeczywistego i oczekiwanego zachowania. Nie mam nad nimi kontroli, tak jakby przypadki testowania błędów przerywały się. Chcę przejąć kontrolę nad błędem asercji i określić, czy chcę przerwać testowanie w przypadku potwierdzenia błędu, czy nie.

Chcę również dodać coś takiego, jeśli wystąpi błąd asercji, to przypadek testowy powinien zostać wstrzymany, a użytkownik może wznowić w dowolnym momencie.

Nie mam pojęcia, jak to zrobić

Przykład kodu, używamy tutaj zapytania

import pytest
def test_abc():
    a = 10
    assert a == 10, "some error message"

Below is my expectation

Gdy assert zgłasza błąd assertionError, powinienem mieć możliwość wstrzymania testu i mogę debugować, a następnie wznowić. Do pauzy i wznowienia użyję tkintermodułu. Zrobię funkcję aser, jak poniżej

import tkinter
import tkinter.messagebox

top = tkinter.Tk()

def _assertCustom(assert_statement, pause_on_fail = 0):
    #assert_statement will be something like: assert a == 10, "Some error"
    #pause_on_fail will be derived from global file where I can change it on runtime
    if pause_on_fail == 1:
        try:
            eval(assert_statement)
        except AssertionError as e:
            tkinter.messagebox.showinfo(e)
            eval (assert_statement)
            #Above is to raise the assertion error again to fail the testcase
    else:
        eval (assert_statement)

Idąc dalej, muszę zmienić każdą instrukcję aser z tą funkcją jako

import pytest
def test_abc():
    a = 10
    # Suppose some code and below is the assert statement 
    _assertCustom("assert a == 10, 'error message'")

To dla mnie za duży wysiłek, ponieważ muszę dokonać zmian w tysiącach miejsc, w których korzystałem. Czy jest na to jakiś prosty sposóbpytest

Summary:Potrzebuję czegoś, w którym mogę wstrzymać testowanie w przypadku niepowodzenia, a następnie wznowić po debugowaniu. wiem otkinter tym i dlatego go wykorzystałem. Wszelkie inne pomysły będą mile widziane

Note: Powyższy kod nie został jeszcze przetestowany. Mogą występować również małe błędy składniowe

Edycja: Dzięki za odpowiedzi. Rozszerzając to pytanie nieco teraz. Co jeśli chcę zmienić zachowanie aser. Obecnie, gdy występuje błąd asercji, testcase kończy pracę. Co jeśli chcę wybrać, czy potrzebuję wyjść z testu w przypadku konkretnego błędu, czy nie. Nie chcę pisać niestandardowej funkcji potwierdzenia, jak wspomniano powyżej, ponieważ w ten sposób muszę zmieniać liczbę miejsc

Nitesh
źródło
3
Czy możesz podać kodowy przykład tego, co chcesz zrobić?
mrblewog,
1
Nie używaj, assertale pisz własne funkcje sprawdzające, które robią, co chcesz.
molbdnilo,
Dlaczego nie wstawisz aser w try block i komunikat o błędzie w wyjątkiem ?
Prathik Kini,
1
Wygląda na to, że naprawdę chcesz używać pytestdo testowania przypadków. Obsługuje stosowanie testów aser i pomijanie, a także wiele innych funkcji ułatwiających pisanie zestawów testów.
blubberdiblub
1
Czy nie byłoby łatwo napisać proste narzędzie, które mechanicznie zastąpiłoby każdy assert cond, "msg"w twoim kodzie _assertCustom("assert cond, 'msg'")? Prawdopodobnie sedmoże to zrobić jeden liniowiec.
NPE

Odpowiedzi:

23

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.

Martijn Pieters
źródło
7

Możesz osiągnąć dokładnie to, co chcesz bez absolutnie żadnej modyfikacji kodu dzięki pytest --pdb .

Na przykład:

import pytest
def test_abc():
    a = 9
    assert a == 10, "some error message"

Uruchom z --pdb:

py.test --pdb
collected 1 item

test_abc.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    def test_abc():
        a = 9
>       assert a == 10, "some error message"
E       AssertionError: some error message
E       assert 9 == 10

test_abc.py:4: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /private/tmp/a/test_abc.py(4)test_abc()
-> assert a == 10, "some error message"
(Pdb) p a
9
(Pdb)

Gdy tylko test się nie powiedzie, można go debugować za pomocą wbudowanego debugera python. Po continuezakończeniu debugowania możesz wykonać resztę testów.

gnvk
źródło
Czy to się zatrzyma, gdy przypadek testowy się nie powiedzie lub krok testu się nie powiedzie.
Nitesh
Sprawdź powiązane dokumenty: doc.pytest.org/en/latest/…
gnvk
Świetny pomysł. Ale jeśli użyjesz opcji --pdb, testcase zatrzyma się przy każdej awarii. czy mogę zdecydować w czasie wykonywania, przy której awarii chcę wstrzymać przypadek testowy
Nitesh
5

Jeśli używasz PyCharm, możesz dodać punkt przerwania wyjątku, aby wstrzymać wykonanie, gdy nie powiedzie się potwierdzenie. Wybierz opcję Pokaż punkty przerwania (CTRL-SHIFT-F8) i dodaj moduł obsługi wyjątków przy podnoszeniu dla AssertionError. Zauważ, że może to spowolnić wykonywanie testów.

W przeciwnym razie, jeśli nie masz nic przeciwko wstrzymywaniu się na końcu każdego testu zakończonego niepowodzeniem (tuż przed błędem), a nie w momencie, gdy asercja nie powiedzie się, masz kilka opcji. Należy jednak pamiętać, że w tym momencie różne kody czyszczenia, takie jak zamykanie plików, które zostały otwarte w teście, mogły już zostać uruchomione. Możliwe opcje to:

  1. Możesz powiedzieć pytestowi, aby wrzucił cię do debuggera w przypadku błędów, używając opcji --pdb .

  2. Możesz zdefiniować następujący dekorator i ozdobić każdą odpowiednią funkcją testową. (Oprócz rejestrowania wiadomości możesz w tym momencie uruchomić pdb.post_mortem , a nawet interaktywny kod. Wchodzić w interakcję z lokalnymi ramkami, z których powstał wyjątek, jak opisano w tej odpowiedzi .)

from functools import wraps

def pause_on_assert(test_func):
    @wraps(test_func)
    def test_wrapper(*args, **kwargs):
        try:
            test_func(*args, **kwargs)
        except AssertionError as e:
            tkinter.messagebox.showinfo(e)
            # re-raise exception to make the test fail
            raise
    return test_wrapper

@pause_on_assert
def test_abc()
    a = 10
    assert a == 2, "some error message"
  1. Jeśli nie chcesz ręcznie dekorować każdej funkcji testowej, możesz zamiast tego zdefiniować urządzenie automatyczne, które sprawdza sys.last_value :
import sys

@pytest.fixture(scope="function", autouse=True)
def pause_on_assert():
    yield
    if hasattr(sys, 'last_value') and isinstance(sys.last_value, AssertionError):
        tkinter.messagebox.showinfo(sys.last_value)
Uri Granta
źródło
Podobało mi się to z dekoratorami, ale nie można tego zrobić dynamicznie. Chcę dynamicznie kontrolować, kiedy chcę pauzować_w_podstawie, czy nie. Czy jest na to jakieś rozwiązanie?
Nitesh
Dynamiczny w jaki sposób? Jak w jednym przełączniku, aby włączyć / wyłączyć go wszędzie? Lub jakiś sposób, aby to kontrolować dla każdego testu?
Uri Granta
Załóżmy, że uruchamiam kilka testów. W środku potrzebowałem przerwy na niepowodzenie. Włączę przełącznik. Później w dowolnym momencie czuję, że muszę wyłączyć przełącznik.
Nitesh
Wasz dekorator w odpowiedzi: 2 nie będzie dla mnie działać, ponieważ mój przypadek testowy będzie zawierał wiele stwierdzeń
Nitesh
Jeśli chodzi o „przełącznik”, możesz zaktualizować albo implementację pause_on_assertodczytu z pliku, aby zdecydować, czy wstrzymać, czy nie.
Uri Granta
4

Jednym prostym rozwiązaniem, jeśli chcesz używać programu Visual Studio Code, może być użycie warunkowych punktów przerwania .

Umożliwiłoby to skonfigurowanie swoich twierdzeń, na przykład:

import pytest
def test_abc():
    a = 10
    assert a == 10, "some error message"

Następnie dodaj warunkowy punkt przerwania do linii potwierdzenia, który złamie się tylko wtedy, gdy asercja się nie powiedzie:

wprowadź opis zdjęcia tutaj

Nick Martin
źródło
@Nitesh - Myślę, że to rozwiązanie rozwiązuje wszystkie twoje problemy, łamiesz się tylko wtedy, gdy asercja się nie powiedzie, możesz tam debugować kod, a potem możesz kontynuować pozostałe testy ... Chociaż konfiguracja jest nieco trudniejsza początkowo
Nick Martin