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.