Python unittest - przeciwieństwo assertRaises?


374

Chcę napisać test, aby ustalić, że wyjątek nie jest zgłaszany w danych okolicznościach.

Łatwo jest sprawdzić, czy zgłoszony został wyjątek ...

sInvalidPath=AlwaysSuppliesAnInvalidPath()
self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath) 

... ale jak możesz zrobić odwrotnie .

Coś w tym stylu, o co mi chodzi ...

sValidPath=AlwaysSuppliesAValidPath()
self.assertNotRaises(PathIsNotAValidOne, MyObject, sValidPath) 

6
Zawsze możesz po prostu zrobić wszystko, co powinno zadziałać w teście. Jeśli zgłosi błąd, pojawi się (liczony jako błąd, a nie błąd). Oczywiście zakłada się, że nie powoduje to żadnego błędu, a nie tylko określony rodzaj błędu. Poza tym myślę, że musiałbyś napisać własny.
— Thomas K


Okazuje się, że w rzeczywistości można zaimplementować assertNotRaisesmetodę, która dzieli 90% jego kodu / zachowania assertRaisesw około 30-liniowych wierszach kodu. Zobacz moją odpowiedź poniżej, aby uzyskać szczegółowe informacje.
— tel.

Chcę tego, abym mógł porównać dwie funkcje, hypothesisaby upewnić się, że generują takie same dane wyjściowe dla wszystkich rodzajów danych wejściowych, ignorując przypadki, w których oryginał zgłasza wyjątek. assume(func(a))nie działa, ponieważ dane wyjściowe mogą być tablicą o niejednoznacznej wartości prawdy. Chcę tylko wywołać funkcję i uzyskać, Truejeśli się nie powiedzie. assume(func(a) is not None)prace chyba
— endolith

Odpowiedzi:


394
def run_test(self):
    try:
        myFunc()
    except ExceptionType:
        self.fail("myFunc() raised ExceptionType unexpectedly!")

32
@hiwaylon - Nie, w rzeczywistości jest to właściwe rozwiązanie. Rozwiązanie zaproponowane przez user9876 jest wadliwe koncepcyjnie: jeśli testujesz pod kątem braku podniesienia powiedzmy ValueError, ale ValueErrorzamiast tego jest podniesiony, twój test musi zakończyć się warunkiem błędu, a nie błędu. Z drugiej strony, jeśli uruchomisz ten sam kod, zgłosiłbyś KeyErrorbłąd, a nie błąd. W Pythonie - inaczej niż w niektórych innych językach - rutynowo stosuje się wyjątki dla przepływu sterowania, dlatego rzeczywiście mamy except <ExceptionName>składnię. W tym względzie rozwiązanie user9876 jest po prostu złe.
— Mac

@mac - Czy to również prawidłowe rozwiązanie? stackoverflow.com/a/4711722/6648326
— MasterJoe2

Ten ma niefortunny efekt wykazania <100% pokrycia (z wyjątkiem nigdy się nie zdarzy) dla testów.
— Shay

3
@Shay, IMO, zawsze powinieneś wykluczyć same pliki testowe z raportów zasięgu (ponieważ prawie zawsze uruchamiają się one w 100% z definicji, sztucznie zawyżasz raporty)
— Original BBQ Sauce

@ original-bbq-sauce, czy nie zostawiłbym mnie otwartym na przypadkowo pominięte testy. Np. Literówka w nazwie testu (funkcja ttst), niepoprawna konfiguracja uruchomienia w pycharmie itp.?
— Shay,

67

Cześć - Chcę napisać test, aby ustalić, że wyjątek nie jest zgłaszany w danych okolicznościach.

Takie jest domyślne założenie - wyjątki nie są zgłaszane.

Jeśli nie powiesz nic więcej, jest to zakładane w każdym teście.

Nie musisz tak naprawdę pisać żadnych twierdzeń.


7
@IndradhanushGupta Dobrze przyjęta odpowiedź sprawia, że ​​test jest bardziej pythonowy niż ten. Jawne jest lepsze niż niejawne.
— 0xc0de,

17
Żaden inny komentator nie wskazał, dlaczego ta odpowiedź jest błędna, chociaż jest to ten sam powód, dla którego odpowiedź user9876 jest nieprawidłowa: błędy i błędy to różne rzeczy w kodzie testowym. Jeśli funkcja byli rzucić wyjątek podczas testu, dokłada nie assert ramy testu będzie traktować tego jako błędu , zamiast awarii nie dochodzić.
— coredumperror

@CoreDumpError Rozumiem różnicę między awarią a błędem, ale czy nie zmusiłoby cię to do otoczenia każdego testu konstrukcją try / wyjątek? A może poleciłbyś to zrobić tylko w przypadku testów, które jawnie zgłaszają wyjątek pod pewnymi warunkami (co w zasadzie oznacza, że ​​wyjątek jest oczekiwany ).
— federicojasson

4
@federicojasson W tym drugim zdaniu dość dobrze odpowiedziałeś na własne pytanie. Błędy vs. niepowodzenia w testach można zwięźle opisać odpowiednio jako „nieoczekiwane awarie” i „niezamierzone zachowanie”. Chcesz, aby twoje testy pokazywały błąd, gdy twoja funkcja ulega awarii, ale nie wtedy, gdy wyjątek, o którym wiesz, że zostanie zgłoszony, pod warunkiem, że niektóre dane wejściowe zostaną wyrzucone, gdy podane zostaną inne dane wejściowe.
— coredumperror

52

Wystarczy wywołać funkcję. Jeśli zgłosi wyjątek, środowisko testów jednostkowych oznaczy to jako błąd. Możesz dodać komentarz, np .:

sValidPath=AlwaysSuppliesAValidPath()
# Check PathIsNotAValidOne not thrown
MyObject(sValidPath)

35
Awarie i błędy są koncepcyjnie różne. Ponadto, ponieważ w Pythonie rutynowo stosuje się wyjątki do sterowania przepływem, bardzo utrudni to zrozumienie na pierwszy rzut oka (= bez eksplorowania kodu testowego), jeśli złamałeś logikę lub kod ...
— Mac

1
Albo twój test się powiedzie, albo nie. Jeśli nie przejdzie, będziesz musiał to naprawić. To, czy jest zgłaszane jako „awaria” czy „błąd”, jest w większości nieistotne. Jest jedna różnica: z moją odpowiedzią zobaczysz ślad stosu, abyś mógł zobaczyć, gdzie została rzucona ścieżka PathIsNotAValidOne; przy przyjętej odpowiedzi nie będziesz mieć tych informacji, więc debugowanie będzie trudniejsze. (Zakładając, że Py2; nie jestem pewien, czy Py3 jest w tym lepszy).
— user9876,

19
@ user9876 - Nie. Testowe warunki wyjścia to 3 (pass / nopass / error), a nie 2, jak się wydaje błędnie. Różnica między błędami a awariami jest znacząca, a traktowanie ich tak, jakby były identyczne, jest po prostu słabym programowaniem. Jeśli mi nie wierzysz, po prostu rozejrzyj się, jak działają testerzy i jakie drzewa decyzyjne wdrażają w przypadku awarii i błędów. Dobry punkt wyjścia dla Pythona jest dekorator w pytest. xfail
— Mac

4
Myślę, że to zależy od tego, jak korzystasz z testów jednostkowych. Sposób, w jaki mój zespół stosuje testy jednostkowe, muszą przejść wszystkie. (Programowanie zwinne, z maszyną do ciągłej integracji, która uruchamia wszystkie testy jednostkowe). Wiem, że przypadek testowy może zgłaszać „pozytywny”, „nieudany” lub „błąd”. Ale na wysokim poziomie tak naprawdę liczy się mój zespół: „czy wszystkie testy jednostkowe zdały?” (tzn. „czy Jenkins jest zielony?”). Więc dla mojego zespołu nie ma praktycznej różnicy między „niepowodzeniem” a „błędem”. Możesz mieć inne wymagania, jeśli używasz testów jednostkowych w inny sposób.
— user9876

1
@ user9876 różnica między „niepowodzeniem” a „błędem” jest różnicą między „moim asersem nieudanym” a „moim testem nawet nie dochodzi do asercji”. Jest to dla mnie użyteczne rozróżnienie podczas testów naprawczych, ale myślę, że, jak mówisz, nie dla wszystkich.
— CS

14

Jestem oryginalnym plakatem i przyjąłem powyższą odpowiedź przez DGH bez wcześniejszego użycia jej w kodzie.

Kiedy już skorzystałem, zdałem sobie sprawę, że trzeba było trochę ulepszyć, aby faktycznie zrobić to, czego potrzebowałem (aby być uczciwym wobec DGH, on / ona powiedział „lub coś podobnego”!).

Pomyślałem, że warto zamieścić tutaj ulepszenie z korzyścią dla innych:

    try:
        a = Application("abcdef", "")
    except pySourceAidExceptions.PathIsNotAValidOne:
        pass
    except:
        self.assertTrue(False)

To, co próbowałem tutaj zrobić, to upewnić się, że jeśli podjęta zostanie próba utworzenia wystąpienia obiektu aplikacji z drugim argumentem spacji, pySourceAidExceptions.PathIsNotAValidOne zostanie podniesiony.

Wierzę, że użycie powyższego kodu (opartego w dużej mierze na odpowiedzi DGH) to zrobi.


2
Ponieważ wyjaśniacie swoje pytanie i nie udzielacie odpowiedzi, powinniście go edytować (nie odpowiedzieli na nie). Proszę zobaczyć moją odpowiedź poniżej.
— hiwaylon

13
Wydaje się być zupełnie odwrotnym do pierwotnego problemu. self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath)powinien wykonać pracę w tym przypadku.
— Antony Hatchkins

8

Możesz zdefiniować assertNotRaises, ponownie wykorzystując około 90% oryginalnej implementacji assertRaisesw unittestmodule. Dzięki takiemu podejściu uzyskuje się assertNotRaisesmetodę, która oprócz odwróconego stanu awarii zachowuje się identycznie assertRaises.

TLDR i demo na żywo

Okazuje się, że dodanie assertNotRaisesmetody jest zaskakująco łatwe unittest.TestCase(napisanie tej odpowiedzi zajęło mi około 4 razy więcej czasu niż kodu). Oto demo na żywo assertNotRaisesmetody w akcji . Tylko jakassertRaises , można przejść wywoływalnym i args się assertNotRaises, czy można go używać w withoświadczeniu. Demo na żywo zawiera przypadki testowe, które pokazują, że assertNotRaisesdziała zgodnie z przeznaczeniem.

Detale

Realizacja assertRaiseswunittest jest dość skomplikowana, ale przy odrobinie sprytnej podklasy można zastąpić i odwrócić jej stan awarii.

assertRaisesjest krótką metodą, która w zasadzie tworzy instancję unittest.case._AssertRaisesContextklasy i zwraca ją (patrz jej definicja w unittest.casemodule). Możesz zdefiniować własną _AssertNotRaisesContextklasę, podklasując _AssertRaisesContexti zastępując jej __exit__metodę:

import traceback
from unittest.case import _AssertRaisesContext

class _AssertNotRaisesContext(_AssertRaisesContext):
    def __exit__(self, exc_type, exc_value, tb):
        if exc_type is not None:
            self.exception = exc_value.with_traceback(None)

            try:
                exc_name = self.expected.__name__
            except AttributeError:
                exc_name = str(self.expected)

            if self.obj_name:
                self._raiseFailure("{} raised by {}".format(exc_name,
                    self.obj_name))
            else:
                self._raiseFailure("{} raised".format(exc_name))

        else:
            traceback.clear_frames(tb)

        return True

Zwykle klasy przypadków testowych są definiowane przez dziedziczenie TestCase. Jeśli zamiast tego dziedziczysz z podklasy MyTestCase:

class MyTestCase(unittest.TestCase):
    def assertNotRaises(self, expected_exception, *args, **kwargs):
        context = _AssertNotRaisesContext(expected_exception, self)
        try:
            return context.handle('assertNotRaises', args, kwargs)
        finally:
            context = None

wszystkie twoje przypadki testowe będą teraz miały assertNotRaisesdostępną dla nich metodę.


Gdzie jest twój tracebackw swoim elseoświadczeniu pochodzących z?
— NOhs

1
@NOhs Brakowało import. Naprawiono
— tel.

2
def _assertNotRaises(self, exception, obj, attr):                                                                                                                              
     try:                                                                                                                                                                       
         result = getattr(obj, attr)                                                                                                                                            
         if hasattr(result, '__call__'):                                                                                                                                        
             result()                                                                                                                                                           
     except Exception as e:                                                                                                                                                     
         if isinstance(e, exception):                                                                                                                                           
            raise AssertionError('{}.{} raises {}.'.format(obj, attr, exception)) 

można zmodyfikować, jeśli musisz zaakceptować parametry.

zadzwoń jak

self._assertNotRaises(IndexError, array, 'sort')

1

Uważam, że przydatne jest łatanie małp unittestw następujący sposób:

def assertMayRaise(self, exception, expr):
  if exception is None:
    try:
      expr()
    except:
      info = sys.exc_info()
      self.fail('%s raised' % repr(info[0]))
  else:
    self.assertRaises(exception, expr)

unittest.TestCase.assertMayRaise = assertMayRaise

To wyjaśnia cel podczas testowania pod kątem braku wyjątku:

self.assertMayRaise(None, does_not_raise)

Upraszcza to również testowanie w pętli, co często robię:

# ValueError is raised only for op(x,x), op(y,y) and op(z,z).
for i,(a,b) in enumerate(itertools.product([x,y,z], [x,y,z])):
  self.assertMayRaise(None if i%4 else ValueError, lambda: op(a, b))

Co to jest łatka na małpy?
— ScottMcC

1
Zobacz en.wikipedia.org/wiki/Monkey_patch . Po dodaniu assertMayRaisedo unittest.TestSuitewas może udawać, że to część unittestbiblioteki.
— AndyJost

0

Jeśli przekażesz klasę wyjątków, otrzymasz assertRaises()menedżera kontekstu. Może to poprawić czytelność twoich testów:

# raise exception if Application created with bad data
with self.assertRaises(pySourceAidExceptions.PathIsNotAValidOne):
    application = Application("abcdef", "")

Umożliwia to testowanie przypadków błędów w kodzie.

W takim przypadku testowane PathIsNotAValidOnejest podniesienie podczas przekazywania niepoprawnych parametrów do konstruktora aplikacji.


1
Nie, nie powiedzie się to tylko wtedy, gdy wyjątek nie zostanie zgłoszony w bloku menedżera kontekstu. Może być łatwo przetestowany przez 'with self.assertRaises (TypeError): raise TypeError', który przechodzi.
— Matthew Trevor

@MatthewTrevor Dobre połączenie. O ile pamiętam, zamiast testowania kodu działa poprawnie, tzn. Nie podnosi się, sugerowałem testowanie błędów. Odpowiednio zredagowałem odpowiedź. Mam nadzieję, że mogę zarobić +1, aby wyjść z czerwonego. :)
— hiwaylon

Uwaga: jest to również Python 2.7 i nowsze wersje
— qneill,

0

możesz spróbować w ten sposób. try: self.assertRaises (None, function, arg1, arg2) oprócz: pass, jeśli nie umieścisz kodu w środku, spróbuj zablokować to poprzez wyjątek „AssertionError: None not failed” i przypadek testowy zakończy się niepowodzeniem. Przypadek testowy zostanie zaliczony jeśli jest w środku spróbuj bloku, którego zachowanie jest oczekiwane.


0

Jednym prostym sposobem na zapewnienie inicjalizacji obiektu bez żadnego błędu jest przetestowanie instancji typu obiektu.

Oto przykład :

p = SomeClass(param1=_param1_value)
self.assertTrue(isinstance(p, SomeClass))
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.