Получение unittest Python приводит к методу tearDown()
можно ли получить результаты теста (т. е. прошли ли все утверждения) в методе tearDown ()? Я запускаю сценарии Selenium, и я хотел бы сделать некоторые отчеты изнутри tearDown (), однако я не знаю, возможно ли это.
10 ответов
предостережение: у меня нет возможности дважды проверить следующую теорию на данный момент, находясь вдали от dev box. Так что это может быть выстрел в темноте.
возможно, вы могли бы проверить возвращаемое значение sys.exc_info()
внутри вашего метода tearDown (), если он возвращает (None, None, None)
, вы знаете, что тестовый случай удался. В противном случае можно использовать возвращаемый Кортеж для опроса объекта исключения.
посмотреть sys.exc_info документация.
другой более явный подход написать декоратор метода, который вы могли бы шлепнуть на все ваши методы тестового случая, которые требуют этой специальной обработки. Этот декоратор может перехватывать исключения утверждений и на основе этого изменять некоторое состояние в self
позволяет вашему методу демонтажа узнать, что случилось.
@assertion_tracker
def test_foo(self):
# some test logic
если вы посмотрите на реализацию unittest.TestCase.run
, вы можете видеть, что все результаты теста собираются в объекте результата (обычно unittest.TestResult
instance) передается как аргумент. В "объект".
так что вы мало что можете сделать в unittest.TestCase.tearDown
метод, если вы не беспощадно нарушаете элегантную развязку тестовых случаев и результатов тестов с чем-то вроде этого:
import unittest
class MyTest(unittest.TestCase):
currentResult = None # holds last result object passed to run method
def setUp(self):
pass
def tearDown(self):
ok = self.currentResult.wasSuccessful()
errors = self.currentResult.errors
failures = self.currentResult.failures
print ' All tests passed so far!' if ok else \
' %d errors and %d failures so far' % \
(len(errors), len(failures))
def run(self, result=None):
self.currentResult = result # remember result for use in tearDown
unittest.TestCase.run(self, result) # call superclass run method
def test_onePlusOneEqualsTwo(self):
self.assertTrue(1 + 1 == 2) # succeeds
def test_onePlusOneEqualsThree(self):
self.assertTrue(1 + 1 == 3) # fails
def test_onePlusNoneIsNone(self):
self.assertTrue(1 + None is None) # raises TypeError
if __name__ == '__main__':
unittest.main()
EDIT: это работает для Python 2.6-3.3, (изменено для новых Питон Беллоу).
это решение для Python версии 2,7 до 3,6 (самая высокая текущая версия и разработка незадолго до 3.7-Альфа), без каких-либо декораторов или других изменений в любом коде до tearDown
. Все работает в соответствии со встроенной классификацией результатов. Также пропустил тесты или expectedFailure
распознаны верно. Он оценивает результат текущего теста, а не сводку всех тестов, пройденных до сих пор. Совместимый также с pytest.
import unittest
class MyTest(unittest.TestCase):
def tearDown(self):
if hasattr(self, '_outcome'): # Python 3.4+
result = self.defaultTestResult() # these 2 methods have no side effects
self._feedErrorsToResult(result, self._outcome.errors)
else: # Python 3.2 - 3.3 or 3.0 - 3.1 and 2.7
result = getattr(self, '_outcomeForDoCleanups', self._resultForDoCleanups)
error = self.list2reason(result.errors)
failure = self.list2reason(result.failures)
ok = not error and not failure
# demo: report short info immediately (not important)
if not ok:
typ, text = ('ERROR', error) if error else ('FAIL', failure)
msg = [x for x in text.split('\n')[1:] if not x.startswith(' ')][0]
print("\n%s: %s\n %s" % (typ, self.id(), msg))
def list2reason(self, exc_list):
if exc_list and exc_list[-1][0] is self:
return exc_list[-1][1]
# DEMO tests
def test_success(self):
self.assertEqual(1, 1)
def test_fail(self):
self.assertEqual(2, 1)
def test_error(self):
self.assertEqual(1 / 0, 1)
комментарии: необходимо сообщить только об одном или нулевом исключении (ошибке или сбое), потому что до tearDown
. Пакет unittest
ожидает, что второе исключение может быть вызвано tearDown. Поэтому списки errors
и failures
может содержать только один или ноль элементов вместе до демонтажа. Строки после комментария "demo" сообщают короткий результат.
демо-выход: (не важно)
$ python3.5 -m unittest test
EF.
ERROR: test.MyTest.test_error
ZeroDivisionError: division by zero
FAIL: test.MyTest.test_fail
AssertionError: 2 != 1
==========================================================
... skipped usual output from unittest with tracebacks ...
...
Ran 3 tests in 0.002s
FAILED (failures=1, errors=1)
сравнение с другими решениями - (относительно истории фиксации исходного репозитория Python):
данное решение использует частная атрибут экземпляра TestCase, как многие иное решение, но я тщательно проверил все соответствующие коммиты в исходном репозитории Python эти три альтернативных имени охватывают историю кода начиная с Python 2.7-3.6.2 без любые пробелы. Это может быть проблемой после некоторого нового майора Выпуск Python, но он может быть четко распознан, пропущен и легко исправлен позже для нового Python. Преимущество в том, что ничего не изменяется до запуск tearDown, он никогда не должен нарушать тест и всю функциональность unittest поддерживается, работает с pytest, может работать со многими расширяющимися пакетами, но не с nosetest (не удивительно, потому что nosetest не совместим, например, с unittest.expectedFailure).
в решения с декораторы на методах теста потребителя или с подгонянным failureException ( mgilson, Павел Репин 2-й путь, kenorb) являются надежными против будущий Python версий, но если все должно работать полностью, они будут расти как snow ball с более поддерживаемыми исключениями и более реплицированными внутренними из unittest. Украшенные функции имеют менее читаемые tracebacks (еще больше уровней, добавленных одним декоратором), они сложнее для отладка и это unpleassant, если другой более важный декоратор иметь проблему. (Благодаря mgilson основная функциональность готова и известна проблемы могут быть исправлены.)
-
решение с modifired
run
способ и поймалresult
параметр- (scoffey) должно работать
также для Python 2.6. Интерпретация результатов может быть улучшена до
требования вопроса, но ничего не может работать в Python 3.4+,
потому что
result
обновляется после вызова tearDown, никогда раньше. - Марк Григорьевич: (проверено с помощью Python 2.7, 3.2, 3.3, 3.4 и с nosetest)
- (scoffey) должно работать
также для Python 2.6. Интерпретация результатов может быть улучшена до
требования вопроса, но ничего не может работать в Python 3.4+,
потому что
решение
exc_info()
(Павел Репин 2-й путь) работает только с Python 2.другие решения в основном похожи, но менее полные или с большим количеством ущерб.
объясняется исходным репозиторием Python
= Lib/unittest/case.py =
Python v 2.7-3.3
class TestCase(object):
...
def run(self, result=None):
...
self._outcomeForDoCleanups = result # Python 3.2, 3.3
# self._resultForDoCleanups = result # Python 2.7
# # Python 2.6 - no result saved
...
try:
testMethod()
except... # many times for different exception classes
result.add...(self, sys.exc_info()) # _addSkip, addError, addFailure
...
try:
self.tearDown()
...
Python V. 3.4-3.6
def run(self, result=None):
...
# outocome is a context manager to catch and collect different exceptions
self._outcome = outcome
...
with outcome...(self):
testMethod()
...
with outcome...(self):
self.tearDown()
...
self._feedErrorsToResult(result, outcome.errors)
Примечание (при чтении сообщения): одна из причин, почему результаты тестов так сильно отделены от испытаний утечки памяти профилактика. Каждая информация об исключении может получить доступ к кадрам состояния неудачного процесса, включая все локальные переменные. Если фрейм назначен локальной переменной в блоке кода, который также может завершиться ошибкой, то перекрестная память refence можно легко создать. Это не страшно, благодаря сборщику мусора, но свободная память может стать фрагментированной быстрее, чем если бы память была выпущена правильно. Вот почему информация об исключениях и трассировка очень скоро преобразуются в строки и почему временные объекты, такие как self._outcome
инкапсулируются и никто в finally
блок для предотвращения утечек памяти.
если вы используете Python2, вы можете использовать метод _resultForDoCleanups
. Этот метод возвращает TextTestResult
следуя из ответа amatellanes, если вы находитесь на Python3.4, Вы не можете использовать _outcomeForDoCleanups
. Вот что мне удалось взломать вместе:
def _test_has_failed(self):
for method, error in self._outcome.errors:
if error:
return True
return False
yucky, но, похоже, это работает.
Это зависит от того, какую отчетность вы хотели бы произвести.
в случае, если вы хотите сделать некоторые действия при сбое (например, создание скриншотов), вместо tearDown()
, вы можете достичь этого, переопределив failureException
.
например:
@property
def failureException(self):
class MyFailureException(AssertionError):
def __init__(self_, *args, **kwargs):
screenshot_dir = 'reports/screenshots'
if not os.path.exists(screenshot_dir):
os.makedirs(screenshot_dir)
self.driver.save_screenshot('{0}/{1}.png'.format(screenshot_dir, self.id()))
return super(MyFailureException, self_).__init__(*args, **kwargs)
MyFailureException.__name__ = AssertionError.__name__
return MyFailureException
вот решение для тех из нас, кто неудобно использовать решения, которые полагаются на unittest
внутренние:
во-первых, мы создаем декоратор, который будет установлен флаг TestCase
экземпляр, чтобы определить, был ли тестовый случай неудачным или прошел:
import unittest
import functools
def _tag_error(func):
"""Decorates a unittest test function to add failure information to the TestCase."""
@functools.wraps(func)
def decorator(self, *args, **kwargs):
"""Add failure information to `self` when `func` raises an exception."""
self.test_failed = False
try:
func(self, *args, **kwargs)
except unittest.SkipTest:
raise
except Exception: # pylint: disable=broad-except
self.test_failed = True
raise # re-raise the error with the original traceback.
return decorator
этот декоратор на самом деле довольно прост. Он полагается на то, что unittest
обнаруживает неудачные тесты через исключения. Насколько мне известно, единственное специальные исключением, которое необходимо обработать, является unittest.SkipTest
(что не указывает на провал теста). Все остальные исключения указывают на провалы тестов, поэтому мы отмечаем их как таковые, когда они всплывают к нам.
теперь мы можем использовать этот декоратор напрямую:
class MyTest(unittest.TestCase):
test_failed = False
def tearDown(self):
super(MyTest, self).tearDown()
print(self.test_failed)
@_tag_error
def test_something(self):
self.fail('Bummer')
это будет очень раздражать писать этот декоратор все время. Есть ли способ упростить? Да есть!* мы можем написать метакласс для обработки применения декоратора для мы:
class _TestFailedMeta(type):
"""Metaclass to decorate test methods to append error information to the TestCase instance."""
def __new__(cls, name, bases, dct):
for name, prop in dct.items():
# assume that TestLoader.testMethodPrefix hasn't been messed with -- otherwise, we're hosed.
if name.startswith('test') and callable(prop):
dct[name] = _tag_error(prop)
return super(_TestFailedMeta, cls).__new__(cls, name, bases, dct)
теперь мы применяем это к нашей базе TestCase
подкласс, и мы все готовы:
import six # For python2.x/3.x compatibility
class BaseTestCase(six.with_metaclass(_TestFailedMeta, unittest.TestCase)):
"""Base class for all our other tests.
We don't really need this, but it demonstrates that the
metaclass gets applied to all subclasses too.
"""
class MyTest(BaseTestCase):
def tearDown(self):
super(MyTest, self).tearDown()
print(self.test_failed)
def test_something(self):
self.fail('Bummer')
вероятно, есть несколько случаев, которые это не обрабатывает должным образом. Например, он неправильно обнаруживает сбой субтестам или ожидаемые ошибки. Я был бы заинтересован в других режимах отказа этого, поэтому, если вы найдете случай, который я не обрабатываю должным образом, Дайте мне знать в комментариях, и я посмотрю он.
*если бы не было более простого способа, я бы не сделал _tag_error
собственная функция ;-)
Python 2.7.
вы также можете получить результат после unittest.main():
t = unittest.main(exit=False)
print t.result
или используйте suite:
suite.addTests(tests)
result = unittest.result.TestResult()
suite.run(result)
print result
имя текущего теста можно получить с помощью unittest.Тест-кейс.id () метод. Так что в tearDown вы можете проверить себя.id().
пример показывает, как:
- найти, если текущий тест имеет ошибку или сбой в списке ошибок или сбоев
- печать тестового идентификатора с пропуском или сбоем или исключением
проверенный пример здесь работает с хорошим примером @scoffey.
def tearDown(self):
result = "PASS"
#### find and show result for current test
# I did not find any nicer/neater way of comparing self.id() with test id stored in errors or failures lists :-7
id = str(self.id()).split('.')[-1]
# id() e.g. tup[0]:<__main__.MyTest testMethod=test_onePlusNoneIsNone>
# str(tup[0]):"test_onePlusOneEqualsThree (__main__.MyTest)"
# str(self.id()) = __main__.MyTest.test_onePlusNoneIsNone
for tup in self.currentResult.failures:
if str(tup[0]).startswith(id):
print ' test %s failure:%s' % (self.id(), tup[1])
## DO TEST FAIL ACTION HERE
result = "FAIL"
for tup in self.currentResult.errors:
if str(tup[0]).startswith(id):
print ' test %s error:%s' % (self.id(), tup[1])
## DO TEST EXCEPTION ACTION HERE
result = "EXCEPTION"
print "Test:%s Result:%s" % (self.id(), result)
пример:
python run_scripts/tut2.py 2>&1
E test __main__.MyTest.test_onePlusNoneIsNone error:Traceback (most recent call last):
File "run_scripts/tut2.py", line 80, in test_onePlusNoneIsNone
self.assertTrue(1 + None is None) # raises TypeError
TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'
Test:__main__.MyTest.test_onePlusNoneIsNone Result:EXCEPTION
F test __main__.MyTest.test_onePlusOneEqualsThree failure:Traceback (most recent call last):
File "run_scripts/tut2.py", line 77, in test_onePlusOneEqualsThree
self.assertTrue(1 + 1 == 3) # fails
AssertionError: False is not true
Test:__main__.MyTest.test_onePlusOneEqualsThree Result:FAIL
Test:__main__.MyTest.test_onePlusOneEqualsTwo Result:PASS
.
======================================================================
ERROR: test_onePlusNoneIsNone (__main__.MyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "run_scripts/tut2.py", line 80, in test_onePlusNoneIsNone
self.assertTrue(1 + None is None) # raises TypeError
TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'
======================================================================
FAIL: test_onePlusOneEqualsThree (__main__.MyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "run_scripts/tut2.py", line 77, in test_onePlusOneEqualsThree
self.assertTrue(1 + 1 == 3) # fails
AssertionError: False is not true
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1, errors=1)
вдохновленный scoffey это, я решил взять mercilessnes на следующий уровень, и придумали следующее.
он работает как в vanilla unittest, так и при запуске через nosetests, а также работает в версиях Python 2.7, 3.2, 3.3 и 3.4 (я специально не тестировал 3.0, 3.1 или 3.5, так как у меня их нет на данный момент, но если я прочитаю исходный код правильно, он должен работать в 3.5 а ну):
#! /usr/bin/env python
from __future__ import unicode_literals
import logging
import os
import sys
import unittest
# Log file to see squawks during testing
formatter = logging.Formatter(fmt='%(levelname)-8s %(name)s: %(message)s')
log_file = os.path.splitext(os.path.abspath(__file__))[0] + '.log'
handler = logging.FileHandler(log_file)
handler.setFormatter(formatter)
logging.root.addHandler(handler)
logging.root.setLevel(logging.DEBUG)
log = logging.getLogger(__name__)
PY = tuple(sys.version_info)[:3]
class SmartTestCase(unittest.TestCase):
"""Knows its state (pass/fail/error) by the time its tearDown is called."""
def run(self, result):
# Store the result on the class so tearDown can behave appropriately
self.result = result.result if hasattr(result, 'result') else result
if PY >= (3, 4, 0):
self._feedErrorsToResultEarly = self._feedErrorsToResult
self._feedErrorsToResult = lambda *args, **kwargs: None # no-op
super(SmartTestCase, self).run(result)
@property
def errored(self):
if (3, 0, 0) <= PY < (3, 4, 0):
return bool(self._outcomeForDoCleanups.errors)
return self.id() in [case.id() for case, _ in self.result.errors]
@property
def failed(self):
if (3, 0, 0) <= PY < (3, 4, 0):
return bool(self._outcomeForDoCleanups.failures)
return self.id() in [case.id() for case, _ in self.result.failures]
@property
def passed(self):
return not (self.errored or self.failed)
def tearDown(self):
if PY >= (3, 4, 0):
self._feedErrorsToResultEarly(self.result, self._outcome.errors)
class TestClass(SmartTestCase):
def test_1(self):
self.assertTrue(True)
def test_2(self):
self.assertFalse(True)
def test_3(self):
self.assertFalse(False)
def test_4(self):
self.assertTrue(False)
def test_5(self):
self.assertHerp('Derp')
def tearDown(self):
super(TestClass, self).tearDown()
log.critical('---- RUNNING {} ... -----'.format(self.id()))
if self.errored:
log.critical('----- ERRORED -----')
elif self.failed:
log.critical('----- FAILED -----')
else:
log.critical('----- PASSED -----')
if __name__ == '__main__':
unittest.main()
при запуске с unittest
:
$ ./test.py -v
test_1 (__main__.TestClass) ... ok
test_2 (__main__.TestClass) ... FAIL
test_3 (__main__.TestClass) ... ok
test_4 (__main__.TestClass) ... FAIL
test_5 (__main__.TestClass) ... ERROR
[…]
$ cat ./test.log
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_1 ... -----
CRITICAL __main__: ----- PASSED -----
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_2 ... -----
CRITICAL __main__: ----- FAILED -----
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_3 ... -----
CRITICAL __main__: ----- PASSED -----
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_4 ... -----
CRITICAL __main__: ----- FAILED -----
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_5 ... -----
CRITICAL __main__: ----- ERRORED -----
при запуске с nosetests
:
$ nosetests ./test.py -v
test_1 (test.TestClass) ... ok
test_2 (test.TestClass) ... FAIL
test_3 (test.TestClass) ... ok
test_4 (test.TestClass) ... FAIL
test_5 (test.TestClass) ... ERROR
$ cat ./test.log
CRITICAL test: ---- RUNNING test.TestClass.test_1 ... -----
CRITICAL test: ----- PASSED -----
CRITICAL test: ---- RUNNING test.TestClass.test_2 ... -----
CRITICAL test: ----- FAILED -----
CRITICAL test: ---- RUNNING test.TestClass.test_3 ... -----
CRITICAL test: ----- PASSED -----
CRITICAL test: ---- RUNNING test.TestClass.test_4 ... -----
CRITICAL test: ----- FAILED -----
CRITICAL test: ---- RUNNING test.TestClass.test_5 ... -----
CRITICAL test: ----- ERRORED -----
фон
я started С этого:
class SmartTestCase(unittest.TestCase):
"""Knows its state (pass/fail/error) by the time its tearDown is called."""
def run(self, result):
# Store the result on the class so tearDown can behave appropriately
self.result = result.result if hasattr(result, 'result') else result
super(SmartTestCase, self).run(result)
@property
def errored(self):
return self.id() in [case.id() for case, _ in self.result.errors]
@property
def failed(self):
return self.id() in [case.id() for case, _ in self.result.failures]
@property
def passed(self):
return not (self.errored or self.failed)
однако это работает только в Python 2. В Python 3, вплоть до 3.3, поток управления, похоже, немного изменился: unittest packageпроцессы результаты после вызов каждого теста tearDown()
этот способ... поведение может быть подтверждено, если мы просто добавим дополнительную строку (или шесть) в наш тестовый класс:
@@ -63,6 +63,12 @@
log.critical('----- FAILED -----')
else:
log.critical('----- PASSED -----')
+ log.warning(
+ 'ERRORS THUS FAR:\n'
+ + '\n'.join(tc.id() for tc, _ in self.result.errors))
+ log.warning(
+ 'FAILURES THUS FAR:\n'
+ + '\n'.join(tc.id() for tc, _ in self.result.failures))
if __name__ == '__main__':
затем просто повторно запустите тесты:
$ python3.3 ./test.py -v
test_1 (__main__.TestClass) ... ok
test_2 (__main__.TestClass) ... FAIL
test_3 (__main__.TestClass) ... ok
test_4 (__main__.TestClass) ... FAIL
test_5 (__main__.TestClass) ... ERROR
[…]
...и вы увидите, что вы получите как результат:
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_1 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_2 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_3 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_4 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_5 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
__main__.TestClass.test_4
теперь сравните вышеизложенное с выводом Python 2:
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_1 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_2 ... -----
CRITICAL __main__: ----- FAILED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_3 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_4 ... -----
CRITICAL __main__: ----- FAILED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
__main__.TestClass.test_4
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_5 ... -----
CRITICAL __main__: ----- ERRORED -----
WARNING __main__: ERRORS THUS FAR:
__main__.TestClass.test_5
WARNING __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
__main__.TestClass.test_4
поскольку Python 3 обрабатывает ошибки / сбои после тест снесен, мы не можем легко вывести результат теста с помощью result.errors
или result.failures
в каждом случае. (Я думаю, что, вероятно, имеет смысл архитектурно обрабатывать результаты теста после срывая его, однако, это тут сделайте совершенно допустимый вариант использования следующей другой процедуры завершения теста в зависимости от состояния pass/fail теста немного сложнее встретить...)
поэтому, вместо того, чтобы полагаться на общую