Подтвердить что ты не робот

Получение Unitest Python приводит к методу tearDown()

Можно ли получить результаты теста (т.е. прошли ли все утверждения) в методе tearDown()? Я запускаю сценарии Selenium, и я хотел бы сделать некоторые сообщения изнутри tearDown(), однако я не знаю, возможно ли это.

4b9b3361

Ответ 1

CAVEAT: У меня нет возможности дважды проверить следующую теорию на данный момент, находясь в стороне от dev-бокса. Так что это может быть выстрел в темноте.

Возможно, вы можете проверить возвращаемое значение sys.exc_info() внутри вашего метода tearDown(), если он возвращает (None, None, None), вы знаете, что тестовый пример преуспел. В противном случае вы можете использовать возвращенный кортеж для опроса объекта исключения.

Смотрите документацию sys.exc_info.

Еще один более явный подход - написать декоратор метода, который вы могли бы наброситься на все ваши тестовые методы, требующие этой специальной обработки. Этот декоратор может перехватывать исключения утверждения и на основании этого изменять какое-либо состояние в self, позволяя вашему методу tearDown узнать, что произошло.

@assertion_tracker
def test_foo(self):
    # some test logic

Ответ 2

Если вы посмотрите на реализацию unittest.TestCase.run, вы увидите, что все результаты теста собираются в объекте результата (обычно в экземпляре unittest.TestResult), переданном как аргумент. В объекте unittest.TestCase статус результата не сохраняется.

Таким образом, вы не можете много сделать в методе 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 ниже).

Ответ 3

Это решение предназначено для Python версий с 2.7 по 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 могут содержать только один или ноль элементов вместе до tearDown. Строки после "демо" комментария сообщают о коротком результате.

Демонстрационный вывод: (не важно)

$ 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 и может работать со многими пакетами расширения, но не с тестом на нос (не удивительно, потому что тест на носитель не совместим, например, с unittest.expectedFailure).

  • Решения с декораторами на пользовательских методах тестирования или с индивидуальными failException (mgilson, Павел Репин 2-й путь, Кенорб) устойчивы к будущий питон версии, но если все должно работать полностью, они будут расти как снежный ком с большим количеством поддерживаемых исключений и более повторяющимися внутренностями юнит-тест. Декорированные функции имеют менее читаемые следы (еще больше уровней добавлено одним декоратором), они более сложны для отладка, и это неприятно, если другой, более важный декоратор есть проблема. (Благодаря mgilson основной функционал готов и известен проблемы могут быть исправлены.)

  • Решение с модифицированным методом run и уловленным result параметр

    • (scoffey) должен работать также для Python 2.6. Интерпретация результатов может быть улучшена до требования вопроса, но ничто не может работать в Python 3. 4+, потому что result обновляется после вызова tearDown, никогда ранее.
    • Знак G.: (проверено на Python 2.7, 3.2, 3.3, 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)

Примечание (читая сообщения фиксации Python). Причиной того, что результаты тестов так сильно отделены от тестов, является предотвращение утечек памяти. Любая информация об исключении может получить доступ к кадрам состояния сбоя процесса, включая все локальные переменные. Если фрейм назначен локальной переменной в блоке кода, который также может завершиться с ошибкой, тогда можно легко создать перекрестную ссылку на память. Это не страшно, благодаря сборщику мусора, но свободная память может фрагментироваться быстрее, чем если бы память была освобождена правильно. По этой причине информация об исключении и обратная трассировка очень скоро преобразуются в строки, и поэтому временные объекты, такие как self._outcome, инкапсулированы и имеют значение None в блоке finally, чтобы предотвратить утечки памяти.

Ответ 4

Если вы используете Python2, вы можете использовать метод _resultForDoCleanups. Этот метод возвращает объект TextTestResult:

<unittest.runner.TextTestResult run=1 errors=0 failures=0>

Вы можете использовать этот объект для проверки результатов своих тестов:

def tearDown(self):
    if self._resultForDoCleanups.failures:
        ...
    elif self._resultForDoCleanups.errors:
        ...
    else:
        #Success

Если вы используете Python3, вы можете использовать _outcomeForDoCleanups:

def tearDown(self):
    if not self._outcomeForDoCleanups.success:
        ...

Ответ 5

Следуя ответу от amatellanes, если вы на Python3.4, вы не можете использовать _outcomeForDoCleanups. Вот что мне удалось взломать:

def _test_has_failed(self):
    for method, error in self._outcome.errors:
        if error:
            return True
    return False

yucky, но, похоже, он работает.

Ответ 6

Это зависит от того, какую отчетность вы хотите создать.

Если вы хотите сделать некоторые действия при сбое (например, создание скриншотов), вместо использования 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

Ответ 7

Здесь решение для тех из нас, кто неудобно, используя решения, полагающиеся на внутренние элементы 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 обнаруживает неудачные тесты через Exceptions. Насколько мне известно, единственным исключением, которое необходимо обработать, является 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')

Вероятно, в ряде случаев это неправильно обрабатывается. Например, он не обнаруживает неудачно subtests или ожидаемых сбоев. Я был бы заинтересован в других режимах сбоев, поэтому, если вы найдете случай, который я неправильно обрабатываю, дайте мне знать в комментариях, и я рассмотрю его.


* Если бы не было более простого способа, я бы не сделал _tag_error частной функцией; -)

Ответ 8

Python 2.7.

Вы также можете получить результат после unittest.main():

t = unittest.main(exit=False)
print t.result

или использовать пакет:

suite.addTests(tests)
result = unittest.result.TestResult()
suite.run(result)
print result

Ответ 9

Имя текущего теста можно получить с помощью метода unittest.TestCase.id(). Итак, в tearDown вы можете проверить self.id().

Пример показывает, как:

  • найти, если текущий тест имеет ошибку или ошибку в списке ошибок или сбоев
  • распечатать идентификатор теста с помощью PASS или FAIL или EXCEPTION

Проведенный пример здесь работает с красивым примером @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)

Ответ 10

Вдохновленный scoffeys answer, я решил принять 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 -----

Фон

Я начал с этого:

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 включительно поток управления немного изменился: пакет Python 3s unittest обрабатывает результаты после вызова каждого метода тестов 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 2s:

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 в каждом случае. (Я думаю, что, возможно, имеет смысл архитектурно обрабатывать результаты тестов после его разрыва, однако он делает совершенно корректный вариант использования следующей процедуры конца теста в зависимости от состояния проверки/отказа теста труднее встретить...)

Поэтому вместо того, чтобы полагаться на общий объект result, вместо этого мы можем ссылаться на _outcomeForDoCleanups как другие уже, который содержит объект результата для текущего теста и имеет необходимые атрибуты errors и failrues, которые мы можем использовать для вывода состояния тестов к моменту tearDown():

@@ -3,6 +3,7 @@
 from __future__ import unicode_literals
 import logging
 import os
+import sys
 import unittest


@@ -16,6 +17,9 @@
 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."""
@@ -27,10 +31,14 @@

     @property
     def errored(self):
+        if PY >= (3, 0, 0):
+            return bool(self._outcomeForDoCleanups.errors)
         return self.id() in [case.id() for case, _ in self.result.errors]

     @property
     def failed(self):
+        if PY >= (3, 0, 0):
+            return bool(self._outcomeForDoCleanups.failures)
         return self.id() in [case.id() for case, _ in self.result.failures]

     @property

Это добавляет поддержку ранних версий Python 3.

В отличие от Python 3.4, эта переменная частного члена больше не существует, и вместо этого был добавлен новый (хотя и закрытый) метод: _feedErrorsToResult.

Это означает, что для версий 3.4 (и более поздних версий), если потребность достаточно велика, можно - очень хакерски - заставьте их работать, как в версии 2...

@@ -27,17 +27,20 @@
     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 PY >= (3, 0, 0):
+        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 PY >= (3, 0, 0):
+        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]

@@ -45,6 +48,10 @@
     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):

@@ -64,6 +71,7 @@
         self.assertHerp('Derp')

     def tearDown(self):
+        super(TestClass, self).tearDown()
         log.critical('---- RUNNING {} ... -----'.format(self.id()))
         if self.errored:
             log.critical('----- ERRORED -----')

... при условии, конечно, что все потребители этого класса помнят super(…, self).tearDown() в своих соответствующих методах tearDown...

Отказ от ответственности: Чисто просветительская, не пытайтесь это дома и т.д. и т.д. и т.д. Я не особенно горжусь этим решением, но, похоже, пока работает достаточно хорошо, и это лучше всего я мог взломать после возиться в течение часа или двух в субботу днем ​​...

Ответ 11

Протестировано на python 3.7 - пример кода для получения информации о неудачном утверждении, но может дать представление о том, как бороться с ошибками:

def tearDown(self):
    if self._outcome.errors[1][1] and hasattr(self._outcome.errors[1][1][1], 'actual'):
        print(self._testMethodName)
        print(self._outcome.errors[1][1][1].actual)
        print(self._outcome.errors[1][1][1].expected)

Ответ 12

В двух словах, это дает True, если все тестовые запуски завершены без ошибок или сбоев:

class WatheverTestCase(TestCase):

    def tear_down(self):
        return not self._outcome.result.errors and not self._outcome.result.failures

Изучите свойства _outcome, чтобы получить доступ к более подробным возможностям.