EDYCJA: przełączono na lepszy przykład i wyjaśniono, dlaczego jest to prawdziwy problem.
Chciałbym napisać testy jednostkowe w Pythonie, które będą kontynuowane, gdy asercja nie powiedzie się, tak żebym mógł zobaczyć wiele niepowodzeń w jednym teście. Na przykład:
class Car(object):
def __init__(self, make, model):
self.make = make
self.model = make # Copy and paste error: should be model.
self.has_seats = True
self.wheel_count = 3 # Typo: should be 4.
class CarTest(unittest.TestCase):
def test_init(self):
make = "Ford"
model = "Model T"
car = Car(make=make, model=model)
self.assertEqual(car.make, make)
self.assertEqual(car.model, model) # Failure!
self.assertTrue(car.has_seats)
self.assertEqual(car.wheel_count, 4) # Failure!
Tutaj celem testu jest upewnienie się, że Car's __init__
poprawnie ustawia pola. Mógłbym podzielić to na cztery metody (i często jest to świetny pomysł), ale w tym przypadku bardziej czytelne jest zachowanie go jako pojedynczej metody, która testuje pojedynczą koncepcję („obiekt jest poprawnie zainicjowany”).
Jeśli założymy, że najlepiej tutaj nie przerywać metody, to mam nowy problem: nie widzę wszystkich błędów naraz. Kiedy naprawię model
błąd i ponownie uruchomię test, wheel_count
pojawia się błąd. Zaoszczędziłoby mi to czas, aby zobaczyć oba błędy podczas pierwszego uruchomienia testu.
Dla porównania, Google w C ++ framework testów jednostkowych rozróżnia między niezakończonych zgonem EXPECT_*
twierdzeń i śmiertelnych ASSERT_*
twierdzeń:
Twierdzenia występują w parach, które testują to samo, ale mają różny wpływ na bieżącą funkcję. Wersje ASSERT_ * generują krytyczne błędy w przypadku niepowodzenia i przerywają bieżącą funkcję. Wersje EXPECT_ * generują niepowodzenia niekrytyczne, które nie przerywają bieżącej funkcji. Zwykle preferowane są EXPECT_ *, ponieważ pozwalają na zgłoszenie więcej niż jednego błędu w teście. Należy jednak użyć ASSERT_ *, jeśli nie ma sensu kontynuować, gdy dane stwierdzenie zawodzi.
Czy istnieje sposób, aby uzyskać EXPECT_*
podobne zachowanie w Pythonie unittest
? Jeśli nie unittest
, to czy istnieje inna platforma testów jednostkowych Pythona, która obsługuje to zachowanie?
Nawiasem mówiąc, byłem ciekawy, ile rzeczywistych testów może skorzystać na niekrytycznych asercjach, więc przyjrzałem się kilku przykładom kodu (edytowanym 2014-08-19, aby używać kodu wyszukiwania zamiast Google Code Search, RIP). Spośród 10 losowo wybranych wyników z pierwszej strony wszystkie zawierały testy, które zawierały wiele niezależnych stwierdzeń w tej samej metodzie testowej. Wszyscy skorzystaliby na twierdzeniach nie prowadzących do śmierci.
źródło
Odpowiedzi:
Prawdopodobnie będziesz chciał wyprowadzić,
unittest.TestCase
ponieważ jest to klasa, która generuje, gdy asercja nie powiedzie się. Będziesz musiał zmienić architekturę,TestCase
aby nie rzucać (może zamiast tego zachować listę awarii). Zmiana architektury może spowodować inne problemy, które musiałbyś rozwiązać. Na przykład może się okazać, że będziesz musiał wyprowadzić,TestSuite
aby wprowadzić zmiany wspierające zmiany wprowadzone w plikuTestCase
.źródło
TestCase
ze względu na implementację miękkich asercji - są one szczególnie łatwe do wykonania w Pythonie: po prostu złap wszystkie swojeAssertionError
(może w prostej pętli) i zapisz je na liście lub w zestawie , a następnie zawieść je wszystkie na raz. Sprawdź odpowiedź @Anthony Batchelor, aby uzyskać szczegółowe informacje.Innym sposobem na uzyskanie potwierdzeń niekrytycznych jest przechwycenie wyjątku potwierdzenia i zapisanie wyjątków na liście. Następnie potwierdź, że ta lista jest pusta jako część tearDown.
import unittest class Car(object): def __init__(self, make, model): self.make = make self.model = make # Copy and paste error: should be model. self.has_seats = True self.wheel_count = 3 # Typo: should be 4. class CarTest(unittest.TestCase): def setUp(self): self.verificationErrors = [] def tearDown(self): self.assertEqual([], self.verificationErrors) def test_init(self): make = "Ford" model = "Model T" car = Car(make=make, model=model) try: self.assertEqual(car.make, make) except AssertionError, e: self.verificationErrors.append(str(e)) try: self.assertEqual(car.model, model) # Failure! except AssertionError, e: self.verificationErrors.append(str(e)) try: self.assertTrue(car.has_seats) except AssertionError, e: self.verificationErrors.append(str(e)) try: self.assertEqual(car.wheel_count, 4) # Failure! except AssertionError, e: self.verificationErrors.append(str(e)) if __name__ == "__main__": unittest.main()
źródło
unittest.TestCase
pomocą bloków try / except.Jedną z opcji jest potwierdzenie wszystkich wartości naraz jako krotki.
Na przykład:
class CarTest(unittest.TestCase): def test_init(self): make = "Ford" model = "Model T" car = Car(make=make, model=model) self.assertEqual( (car.make, car.model, car.has_seats, car.wheel_count), (make, model, True, 4))
Wynik tych testów byłby następujący:
====================================================================== FAIL: test_init (test.CarTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\temp\py_mult_assert\test.py", line 17, in test_init (make, model, True, 4)) AssertionError: Tuples differ: ('Ford', 'Ford', True, 3) != ('Ford', 'Model T', True, 4) First differing element 1: Ford Model T - ('Ford', 'Ford', True, 3) ? ^ - ^ + ('Ford', 'Model T', True, 4) ? ^ ++++ ^
To pokazuje, że zarówno model, jak i liczba kół są nieprawidłowe.
źródło
Od Pythona 3.4 możesz także używać podtestów :
def test_init(self): make = "Ford" model = "Model T" car = Car(make=make, model=model) with self.subTest(msg='Car.make check'): self.assertEqual(car.make, make) with self.subTest(msg='Car.model check'): self.assertEqual(car.model, model) with self.subTest(msg='Car.has_seats check'): self.assertTrue(car.has_seats) with self.subTest(msg='Car.wheel_count check'): self.assertEqual(car.wheel_count, 4)
(
msg
parametr służy do łatwiejszego określenia, który test się nie powiódł).Wynik:
====================================================================== FAIL: test_init (__main__.CarTest) [Car.model check] ---------------------------------------------------------------------- Traceback (most recent call last): File "test.py", line 23, in test_init self.assertEqual(car.model, model) AssertionError: 'Ford' != 'Model T' - Ford + Model T ====================================================================== FAIL: test_init (__main__.CarTest) [Car.wheel_count check] ---------------------------------------------------------------------- Traceback (most recent call last): File "test.py", line 27, in test_init self.assertEqual(car.wheel_count, 4) AssertionError: 3 != 4 ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=2)
źródło
Za anty-wzorzec uważa się posiadanie wielu potwierdzeń w jednym teście jednostkowym. Oczekuje się, że pojedynczy test jednostkowy przetestuje tylko jedną rzecz. Być może za dużo testujesz. Rozważ podzielenie tego testu na wiele testów. W ten sposób możesz poprawnie nazwać każdy test.
Czasami jednak dobrze jest sprawdzić kilka rzeczy jednocześnie. Na przykład, gdy potwierdzasz właściwości tego samego obiektu. W takim przypadku faktycznie zapewniasz, czy ten obiekt jest poprawny. Sposobem na to jest napisanie niestandardowej metody pomocniczej, która wie, jak potwierdzić ten obiekt. Możesz napisać tę metodę w taki sposób, aby pokazywała wszystkie niepoprawne właściwości lub na przykład pokazywała kompletny stan oczekiwanego obiektu i kompletny stan rzeczywistego obiektu, gdy asert nie powiedzie się.
źródło
Wykonaj każdą asercję w oddzielnej metodzie.
class MathTest(unittest.TestCase): def test_addition1(self): self.assertEqual(1 + 0, 1) def test_addition2(self): self.assertEqual(1 + 1, 3) def test_addition3(self): self.assertEqual(1 + (-1), 0) def test_addition4(self): self.assertEqaul(-1 + (-1), -1)
źródło
setup()
, ponieważ to jeden z testów. Ale jeśli umieszczę każde stwierdzenie w jego własnej funkcji, muszę załadować dane 3 razy, a to ogromna strata zasobów. Jaki jest najlepszy sposób radzenia sobie w takiej sytuacji?W PyPI jest pakiet asercji miękkiej o nazwie
softest
, który poradzi sobie z Twoimi wymaganiami. Działa poprzez zbieranie informacji o błędach, łączenie danych o wyjątkach i danych śledzenia stosu oraz raportowanie tego wszystkiego w ramach zwykłychunittest
danych wyjściowych.Na przykład ten kod:
import softest class ExampleTest(softest.TestCase): def test_example(self): # be sure to pass the assert method object, not a call to it self.soft_assert(self.assertEqual, 'Worf', 'wharf', 'Klingon is not ship receptacle') # self.soft_assert(self.assertEqual('Worf', 'wharf', 'Klingon is not ship receptacle')) # will not work as desired self.soft_assert(self.assertTrue, True) self.soft_assert(self.assertTrue, False) self.assert_all() if __name__ == '__main__': softest.main()
... generuje takie wyjście konsoli:
====================================================================== FAIL: "test_example" (ExampleTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\...\softest_test.py", line 14, in test_example self.assert_all() File "C:\...\softest\case.py", line 138, in assert_all self.fail(''.join(failure_output)) AssertionError: ++++ soft assert failure details follow below ++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The following 2 failures were found in "test_example" (ExampleTest): ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Failure 1 ("test_example" method) +--------------------------------------------------------------------+ Traceback (most recent call last): File "C:\...\softest_test.py", line 10, in test_example self.soft_assert(self.assertEqual, 'Worf', 'wharf', 'Klingon is not ship receptacle') File "C:\...\softest\case.py", line 84, in soft_assert assert_method(*arguments, **keywords) File "C:\...\Python\Python36-32\lib\unittest\case.py", line 829, in assertEqual assertion_func(first, second, msg=msg) File "C:\...\Python\Python36-32\lib\unittest\case.py", line 1203, in assertMultiLineEqual self.fail(self._formatMessage(msg, standardMsg)) File "C:\...\Python\Python36-32\lib\unittest\case.py", line 670, in fail raise self.failureException(msg) AssertionError: 'Worf' != 'wharf' - Worf + wharf : Klingon is not ship receptacle +--------------------------------------------------------------------+ Failure 2 ("test_example" method) +--------------------------------------------------------------------+ Traceback (most recent call last): File "C:\...\softest_test.py", line 12, in test_example self.soft_assert(self.assertTrue, False) File "C:\...\softest\case.py", line 84, in soft_assert assert_method(*arguments, **keywords) File "C:\...\Python\Python36-32\lib\unittest\case.py", line 682, in assertTrue raise self.failureException(msg) AssertionError: False is not true ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (failures=1)
UWAGA : stworzyłem i utrzymuję
softest
.źródło
oczekiwanie jest bardzo przydatne w gtest. Tak wygląda Python w skrócie i kod:
import sys import unittest class TestCase(unittest.TestCase): def run(self, result=None): if result is None: self.result = self.defaultTestResult() else: self.result = result return unittest.TestCase.run(self, result) def expect(self, val, msg=None): ''' Like TestCase.assert_, but doesn't halt the test. ''' try: self.assert_(val, msg) except: self.result.addFailure(self, sys.exc_info()) def expectEqual(self, first, second, msg=None): try: self.failUnlessEqual(first, second, msg) except: self.result.addFailure(self, sys.exc_info()) expect_equal = expectEqual assert_equal = unittest.TestCase.assertEqual assert_raises = unittest.TestCase.assertRaises test_main = unittest.main
źródło
Podobało mi się podejście @ Anthony-Batchelor, aby uchwycić wyjątek AssertionError. Ale niewielka zmiana w tym podejściu przy użyciu dekoratorów, a także sposób zgłaszania przypadków testów z wynikiem pozytywnym / negatywnym.
#!/usr/bin/env python # -*- coding: utf-8 -*- import unittest class UTReporter(object): ''' The UT Report class keeps track of tests cases that have been executed. ''' def __init__(self): self.testcases = [] print "init called" def add_testcase(self, testcase): self.testcases.append(testcase) def display_report(self): for tc in self.testcases: msg = "=============================" + "\n" + \ "Name: " + tc['name'] + "\n" + \ "Description: " + str(tc['description']) + "\n" + \ "Status: " + tc['status'] + "\n" print msg reporter = UTReporter() def assert_capture(*args, **kwargs): ''' The Decorator defines the override behavior. unit test functions decorated with this decorator, will ignore the Unittest AssertionError. Instead they will log the test case to the UTReporter. ''' def assert_decorator(func): def inner(*args, **kwargs): tc = {} tc['name'] = func.__name__ tc['description'] = func.__doc__ try: func(*args, **kwargs) tc['status'] = 'pass' except AssertionError: tc['status'] = 'fail' reporter.add_testcase(tc) return inner return assert_decorator class DecorateUt(unittest.TestCase): @assert_capture() def test_basic(self): x = 5 self.assertEqual(x, 4) @assert_capture() def test_basic_2(self): x = 4 self.assertEqual(x, 4) def main(): #unittest.main() suite = unittest.TestLoader().loadTestsFromTestCase(DecorateUt) unittest.TextTestRunner(verbosity=2).run(suite) reporter.display_report() if __name__ == '__main__': main()
Dane wyjściowe z konsoli:
(awsenv)$ ./decorators.py init called test_basic (__main__.DecorateUt) ... ok test_basic_2 (__main__.DecorateUt) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK ============================= Name: test_basic Description: None Status: fail ============================= Name: test_basic_2 Description: None Status: pass
źródło
Miałem problem z odpowiedzią od @Anthony Batchelor, ponieważ zmusiłaby mnie to do użycia
try...catch
wewnątrz moich testów jednostkowych. Zamiast tego zawarłemtry...catch
logikę w zastąpieniuTestCase.assertEqual
metody. Oto kod:import unittest import traceback class AssertionErrorData(object): def __init__(self, stacktrace, message): super(AssertionErrorData, self).__init__() self.stacktrace = stacktrace self.message = message class MultipleAssertionFailures(unittest.TestCase): def __init__(self, *args, **kwargs): self.verificationErrors = [] super(MultipleAssertionFailures, self).__init__( *args, **kwargs ) def tearDown(self): super(MultipleAssertionFailures, self).tearDown() if self.verificationErrors: index = 0 errors = [] for error in self.verificationErrors: index += 1 errors.append( "%s\nAssertionError %s: %s" % ( error.stacktrace, index, error.message ) ) self.fail( '\n\n' + "\n".join( errors ) ) self.verificationErrors.clear() def assertEqual(self, goal, results, msg=None): try: super( MultipleAssertionFailures, self ).assertEqual( goal, results, msg ) except unittest.TestCase.failureException as error: goodtraces = self._goodStackTraces() self.verificationErrors.append( AssertionErrorData( "\n".join( goodtraces[:-2] ), error ) ) def _goodStackTraces(self): """ Get only the relevant part of stacktrace. """ stop = False found = False goodtraces = [] # stacktrace = traceback.format_exc() # stacktrace = traceback.format_stack() stacktrace = traceback.extract_stack() # /programming/54499367/how-to-correctly-override-testcase for stack in stacktrace: filename = stack.filename if found and not stop and \ not filename.find( 'lib' ) < filename.find( 'unittest' ): stop = True if not found and filename.find( 'lib' ) < filename.find( 'unittest' ): found = True if stop and found: stackline = ' File "%s", line %s, in %s\n %s' % ( stack.filename, stack.lineno, stack.name, stack.line ) goodtraces.append( stackline ) return goodtraces # class DummyTestCase(unittest.TestCase): class DummyTestCase(MultipleAssertionFailures): def setUp(self): self.maxDiff = None super(DummyTestCase, self).setUp() def tearDown(self): super(DummyTestCase, self).tearDown() def test_function_name(self): self.assertEqual( "var", "bar" ) self.assertEqual( "1937", "511" ) if __name__ == '__main__': unittest.main()
Wynik końcowy:
F ====================================================================== FAIL: test_function_name (__main__.DummyTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "D:\User\Downloads\test.py", line 77, in tearDown super(DummyTestCase, self).tearDown() File "D:\User\Downloads\test.py", line 29, in tearDown self.fail( '\n\n' + "\n\n".join( errors ) ) AssertionError: File "D:\User\Downloads\test.py", line 80, in test_function_name self.assertEqual( "var", "bar" ) AssertionError 1: 'var' != 'bar' - var ? ^ + bar ? ^ : File "D:\User\Downloads\test.py", line 81, in test_function_name self.assertEqual( "1937", "511" ) AssertionError 2: '1937' != '511' - 1937 + 511 :
Więcej alternatywnych rozwiązań dla prawidłowego przechwytywania śledzenia stosu można opublikować na stronie Jak poprawnie zastąpić TestCase.assertEqual (), tworząc właściwy ślad stosu?
źródło
Myślę, że nie ma sposobu, aby to zrobić z PyUnit i nie chciałbym, aby PyUnit był rozszerzany w ten sposób.
Wolę trzymać się jednego potwierdzenia na funkcję testową ( lub dokładniej mówiąc, że jedna koncepcja na test ) i przepisałbym
test_addition()
jako cztery oddzielne funkcje testowe. Dałoby to bardziej przydatne informacje na temat awarii, a mianowicie :.FF. ====================================================================== FAIL: test_addition_with_two_negatives (__main__.MathTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_addition.py", line 10, in test_addition_with_two_negatives self.assertEqual(-1 + (-1), -1) AssertionError: -2 != -1 ====================================================================== FAIL: test_addition_with_two_positives (__main__.MathTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_addition.py", line 6, in test_addition_with_two_positives self.assertEqual(1 + 1, 3) # Failure! AssertionError: 2 != 3 ---------------------------------------------------------------------- Ran 4 tests in 0.000s FAILED (failures=2)
Jeśli zdecydujesz, że to podejście nie jest dla Ciebie, możesz znaleźć tę odpowiedź pomocna.
Aktualizacja
Wygląda na to, że testujesz dwie koncepcje za pomocą zaktualizowanego pytania i podzieliłbym je na dwa testy jednostkowe. Po pierwsze, parametry są zapisywane podczas tworzenia nowego obiektu. To miałoby dwa twierdzenia, jedno za
make
i jedno zamodel
. Jeśli pierwsza zawiedzie, to wyraźnie trzeba to naprawić, to czy druga się powiedzie, czy nie, nie ma znaczenia w tym momencie.Druga koncepcja jest bardziej wątpliwa… Testujesz, czy niektóre wartości domyślne są inicjalizowane. Dlaczego ? Bardziej przydatne byłoby przetestowanie tych wartości w momencie, w którym są faktycznie używane (a jeśli nie są używane, to dlaczego tam są?).
Oba te testy kończą się niepowodzeniem i oba powinny. Kiedy testuję jednostki, bardziej interesuje mnie porażka niż sukces, ponieważ na tym muszę się skoncentrować.
FF ====================================================================== FAIL: test_creation_defaults (__main__.CarTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_car.py", line 25, in test_creation_defaults self.assertEqual(self.car.wheel_count, 4) # Failure! AssertionError: 3 != 4 ====================================================================== FAIL: test_creation_parameters (__main__.CarTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_car.py", line 20, in test_creation_parameters self.assertEqual(self.car.model, self.model) # Failure! AssertionError: 'Ford' != 'Model T' ---------------------------------------------------------------------- Ran 2 tests in 0.000s FAILED (failures=2)
źródło
Zdaję sobie sprawę, że to pytanie zostało zadane dosłownie lata temu, ale są teraz (co najmniej) dwa pakiety Pythona, które pozwalają to zrobić.
Jeden jest najdelikatniejszy: https://pypi.org/project/softest/
Drugi to Python-Delayed-Assert: https://github.com/pr4bh4sh/python-delayed-assert
Ja też nie używałem, ale wyglądają bardzo podobnie do mnie.
źródło