РЕДАКТИРОВАТЬ: переключился на лучший пример и пояснил, почему это настоящая проблема.
Я хотел бы написать модульные тесты на Python, которые продолжат выполнение при сбое утверждения, чтобы я мог видеть несколько сбоев в одном тесте. Например:
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!
Здесь цель теста - убедиться, что Car __init__
правильно устанавливает свои поля. Я мог бы разбить его на четыре метода (и это часто отличная идея), но в данном случае я думаю, что более читабельным будет оставить его как единственный метод, который проверяет одну концепцию («объект инициализирован правильно»).
Если мы предположим, что здесь лучше не разбивать метод, тогда у меня возникнет новая проблема: я не могу видеть сразу все ошибки. Когда я исправляю model
ошибку и повторно запускаю тест, wheel_count
появляется ошибка. Я бы сэкономил время, увидев обе ошибки при первом запуске теста.
Для сравнения, фреймворк модульного тестирования C ++ от Google различает нефатальные EXPECT_*
утверждения и фатальные ASSERT_*
утверждения:
Утверждения приходят парами, которые проверяют одно и то же, но по-разному влияют на текущую функцию. Версии ASSERT_ * генерируют фатальные отказы, когда они терпят неудачу, и прерывают текущую функцию. Версии EXPECT_ * генерируют нефатальные сбои, которые не прерывают текущую функцию. Обычно предпочтительнее EXPECT_ *, поскольку они позволяют сообщать о более чем одном сбое в тесте. Однако вы должны использовать ASSERT_ *, если нет смысла продолжать, когда рассматриваемое утверждение не выполняется.
Есть ли способ добиться EXPECT_*
подобного поведения в Python unittest
? Если нет unittest
, то существует ли другая среда модульного тестирования Python, которая поддерживает такое поведение?
Кстати, мне было любопытно, сколько реальных тестов могут выиграть от нефатальных утверждений, поэтому я просмотрел несколько примеров кода (отредактировано 2014-08-19 для использования кода поиска вместо Google Code Search, RIP). Из 10 случайно выбранных результатов с первой страницы все содержали тесты, которые делали несколько независимых утверждений в одном методе тестирования. Все выиграют от несмертельных утверждений.
источник
Ответы:
Что вы, вероятно, захотите сделать, так это унаследовать,
unittest.TestCase
поскольку это класс, который выдает, когда утверждение не выполняется. Вам придется переделать свою архитектуру,TestCase
чтобы не бросать (возможно, вместо этого сохраните список отказов). Изменение архитектуры может вызвать другие проблемы, которые вам придется решать. Например, вам может потребоватьсяTestSuite
внести изменения в поддержку изменений, внесенных в вашTestCase
.источник
TestCase
ради реализации мягких утверждений - излишество - их особенно легко сделать на python: просто поймайте все вашиAssertionError
s (возможно, в простом цикле) и сохраните их в списке или наборе , а затем провалить их все сразу. Ознакомьтесь с ответом @Anthony Batchelor для подробностей.Другой способ иметь нефатальные утверждения - это захватить исключение утверждения и сохранить исключения в списке. Затем подтвердите, что этот список пуст, как часть 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()
источник
unittest.TestCase
с помощью блоков try / except.Один из вариантов - это утверждение сразу для всех значений как кортежа.
Например:
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))
Результатом этих тестов будет:
====================================================================== 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) ? ^ ++++ ^
Это показывает, что и модель, и количество колес неверны.
источник
Начиная с Python 3.4 вы также можете использовать подтесты :
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
параметр используется для более простого определения того, какой тест не прошел.)Вывод:
====================================================================== 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)
источник
Наличие нескольких утверждений в одном модульном тесте считается анти-шаблоном. Ожидается, что единичный модульный тест будет проверять только одно. Возможно, вы слишком много тестируете. Рассмотрите возможность разделения этого теста на несколько тестов. Таким образом, вы можете правильно назвать каждый тест.
Однако иногда можно проверить несколько вещей одновременно. Например, когда вы утверждаете свойства одного и того же объекта. В этом случае вы фактически утверждаете, верен ли этот объект. Один из способов сделать это - написать собственный вспомогательный метод, который знает, как утверждать этот объект. Вы можете написать этот метод таким образом, чтобы он отображал все свойства с ошибкой или, например, показывал полное состояние ожидаемого объекта и полное состояние фактического объекта, когда утверждение не выполняется.
источник
Выполняйте каждое утверждение в отдельном методе.
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)
источник
setup()
, потому что это один из тестов. Но если я помещу каждое утверждение в отдельную функцию, тогда мне придется загружать данные 3 раза, а это огромная трата ресурсов. Как лучше всего справиться с такой ситуацией?В PyPI есть пакет мягких утверждений,
softest
который будет обрабатывать ваши требования. Он работает, собирая сбои, комбинируя данные об исключениях и трассировке стека и сообщая обо всем этом как часть обычногоunittest
вывода.Например, этот код:
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()
... производит этот вывод консоли:
====================================================================== 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)
ПРИМЕЧАНИЕ : я создал и поддерживаю
softest
.источник
expect очень полезен в gtest. Это питон путь в сущности , и код:
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
источник
Мне понравился подход @ Anthony-Batchelor для захвата исключения AssertionError. Но небольшая вариация этого подхода с использованием декораторов, а также способ сообщения тестовых случаев с прохождением / неудачей.
#!/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()
Вывод с консоли:
(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
источник
У меня возникла проблема с ответом от @Anthony Batchelor, потому что он заставил бы меня использовать
try...catch
внутри моих модульных тестов. Вместо этого я инкапсулировалtry...catch
логику в переопределенииTestCase.assertEqual
метода. Вот код: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()
Результат вывода:
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 :
Дополнительные альтернативные решения для правильного захвата трассировки стека могут быть опубликованы на странице Как правильно переопределить TestCase.assertEqual (), создав правильную трассировку стека?
источник
Я не думаю, что есть способ сделать это с помощью PyUnit, и не хотел бы, чтобы PyUnit расширялся таким образом.
Я предпочитаю придерживаться одного утверждения для каждой тестовой функции ( или, более конкретно, утверждать одну концепцию для каждого теста ) и переписывать
test_addition()
как четыре отдельные тестовые функции. Это даст более полезную информацию о сбое, а именно :.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)
Если вы решите, что этот подход не для вас, вы можете найти этот ответ полезным.
Обновить
Похоже, вы тестируете две концепции с помощью обновленного вопроса, и я бы разделил их на два модульных теста. Во-первых, параметры сохраняются при создании нового объекта. Это будет иметь два утверждения, одно для
make
и одно дляmodel
. Если первое не удается, то это явно необходимо исправить, независимо от того, проходит ли второе или нет, на данном этапе не имеет значения.Вторая концепция более сомнительна ... Вы проверяете, инициализированы ли некоторые значения по умолчанию. Почему ? Было бы более полезно проверить эти значения в момент, когда они действительно используются (а если они не используются, то почему они там?).
Оба этих теста не пройдут, и оба должны. Когда я занимаюсь модульным тестированием, меня гораздо больше интересуют неудачи, чем успех, поскольку именно на этом мне нужно сконцентрироваться.
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)
источник
Я понимаю, что этот вопрос задавали буквально много лет назад, но сейчас есть (по крайней мере) два пакета Python, которые позволяют это делать.
Один из них самый мягкий: https://pypi.org/project/softest/
Другой - Python-Delayed-Assert: https://github.com/pr4bh4sh/python-delayed-assert
Я тоже не пользовался, но они очень похожи на меня.
источник