Продолжение на Python unittest, когда утверждение не выполняется
РЕДАКТИРОВАТЬ: переключился на лучший пример, и пояснил, почему это реальная проблема.
Я хотел бы написать модульные тесты на 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
. Это сэкономило бы мне время, чтобы увидеть обе ошибки при первом запуске теста.
Для сравнения, в модуле модульного тестирования Google C++ проводится различие между EXPECT_*
утверждениями EXPECT_*
и фатальными утверждениями ASSERT_*
:
Утверждения приходят парами, которые проверяют одно и то же, но по-разному влияют на текущую функцию. Версии ASSERT_ * генерируют фатальные сбои при сбое и отменяют текущую функцию. Версии EXPECT_ * генерируют нефатальные сбои, которые не отменяют текущую функцию. Обычно EXPECT_ * предпочтительнее, так как они позволяют сообщать о нескольких сбоях в тесте. Тем не менее, вы должны использовать ASSERT_ *, если не имеет смысла продолжать, когда утверждение не выполняется.
Есть ли способ получить EXPECT_*
-like в unittest
Python? Если нет в unittest
, то есть ли еще одна среда модульного тестирования Python, которая поддерживает это поведение?
Кстати, мне было любопытно, сколько реальных тестов может извлечь пользу из нефатальных утверждений, поэтому я посмотрел несколько примеров кода (отредактировано 2014-08-19, чтобы использовать поисковый код вместо Google Code Search, RIP). Из 10 случайно выбранных результатов с первой страницы все содержали тесты, которые сделали несколько независимых утверждений в одном и том же методе тестирования. Все извлекут выгоду из несмертельных утверждений.
Ответы
Ответ 1
Что вы, вероятно, захотите сделать, это вывести unittest.TestCase
, так как класс, который бросает, когда утверждение терпит неудачу. Вам нужно будет перепроектировать ваш TestCase
, чтобы не выбрасывать (возможно, вместо этого должен содержать список сбоев). Повторная архивирование может вызвать другие проблемы, которые вам придется решать. Например, вы можете получить TestSuite
, чтобы внести изменения в поддержку изменений, внесенных в ваш TestCase
.
Ответ 2
Другим способом иметь нефатальные утверждения является захват исключения утверждения и сохранение исключений в списке. Затем утвердите, что этот список пуст как часть 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()
Ответ 3
Один параметр - это утверждение для всех значений сразу как кортеж.
Например:
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)
? ^ ++++ ^
Это показывает, что и модель, и кол-во колес неверны.
Ответ 4
Считается, что анти-шаблон имеет несколько утверждений в одном unit test. Ожидается, что один unit test проверит только одно. Возможно, вы слишком много тестируете. Рассмотрите разделение этого теста на несколько тестов. Таким образом, вы можете правильно назвать каждый тест.
Иногда, однако, можно проверить несколько вещей одновременно. Например, когда вы утверждаете свойства одного и того же объекта. В этом случае вы фактически утверждаете, является ли этот объект правильным. Способ сделать это - написать собственный вспомогательный метод, который знает, как утверждать на этом объекте. Вы можете написать этот метод таким образом, чтобы он показывал все отказоустойчивые свойства или, например, показывал полное состояние ожидаемого объекта и полное состояние фактического объекта при неудачном утверждении.
Ответ 5
Выполняет ли каждый утверждение отдельным методом.
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)
Ответ 6
Мне понравился подход @Anthony-Batchelor, чтобы захватить исключение AssertionError. Но небольшая вариация этого подхода с использованием декораторов, а также способ сообщить о случаях тестов с ошибкой pass/fail.
#!/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
Ответ 7
ожидание очень полезно в gtest.
Это путь python в gist и код:
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
Ответ 8
В PyPI есть пакет мягких утверждений, называемый softest
который будет отвечать вашим требованиям. Он работает, собирая сбои, комбинируя данные об исключениях и трассировке стека, и сообщая обо всех этих unittest
как часть обычного вывода 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
.
Ответ 9
Я не думаю, что есть способ сделать это с помощью 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)
Если вы решите, что этот подход не для вас, вы можете найти этот ответ полезным.
Update
Похоже, вы тестируете две концепции с обновленным вопросом, и я бы разделил их на два модульных теста. Первое заключается в том, что параметры сохраняются при создании нового объекта. У этого были бы два утверждения: один для 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)
Ответ 10
У меня проблема с ответом @Anthony Batchelor, потому что это вынуждает меня использовать try...catch
внутри моих юнит-тестов. Затем я инкапсулировал логику try...catch
в переопределение метода TestCase.assertEqual
. Следующий хак удаляет блоки try...catch
из кода модульных тестов:
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()
# /questions/17023307/how-to-correctly-override-testcaseassertequal-producing-the-right-stacktracehow-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(), производя правильную трассировку стека?
Ответ 11
Я понимаю, что этот вопрос был задан буквально несколько лет назад, но сейчас (по крайней мере) есть два пакета Python, которые позволяют вам сделать это.
Один из них самый мягкий: https://pypi.org/project/softest/
Другой - Python-Delayed-Assert: https://github.com/pr4bh4sh/python-delayed-assert
Я тоже не использовал, но они выглядят очень похоже на меня.