IT TIP

어설 션이 실패 할 때 Python의 unittest에서 계속

itqueen 2020. 10. 24. 12:07
반응형

어설 션이 실패 할 때 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_*어설 션과 치명적 ASSERT_*어설 션을 구분합니다.

단언은 동일한 것을 테스트하지만 현재 기능에 다른 영향을 미치는 쌍으로 제공됩니다. ASSERT_ * 버전은 실패 할 때 치명적인 실패를 생성하고 현재 기능을 중단합니다. EXPECT_ * 버전은 현재 함수를 중단하지 않는 치명적이지 않은 오류를 생성합니다. 일반적으로 EXPECT_ *는 테스트에서 둘 이상의 실패를보고 할 수 있으므로 선호됩니다. 그러나 문제의 주장이 실패 할 때 계속하는 것이 타당하지 않으면 ASSERT_ *를 사용해야합니다.

EXPECT_*파이썬에서 같은 동작 을 얻는 방법 unittest있습니까? 에 없으면 unittest이 동작을 지원하는 다른 Python 단위 테스트 프레임 워크가 있습니까?


덧붙여서, 치명적이지 않은 주장으로 얼마나 많은 실제 테스트가 도움이 될지 궁금해서 몇 가지 코드 예제를 살펴 보았습니다 (Google 코드 검색, RIP 대신 검색 코드를 사용하도록 2014-08-19 편집 됨). 첫 페이지에서 무작위로 선택된 10 개의 결과 중 모두 동일한 테스트 방법에서 여러 개의 독립적 인 주장을하는 테스트를 포함했습니다. 모든 사람은 치명적이지 않은 주장으로 이익을 얻을 수 있습니다.


unittest.TestCase어설 션이 실패 할 때 던지는 클래스이기 때문에 아마도 당신이하고 싶은 것은 파생하는 것입니다 . TestCase던지지 않도록 다시 설계해야 합니다 (대신 실패 목록을 유지하는 것이 좋습니다). 항목을 재구성하면 해결해야 할 다른 문제가 발생할 수 있습니다. 예를 들어 .NET Framework의 TestSuite변경 사항을 지원하기 위해 변경을 수행하기 위해 파생해야 할 수 있습니다 TestCase.


치명적이지 않은 어설 션을 갖는 또 다른 방법은 어설 션 예외를 캡처하고 예외를 목록에 저장하는 것입니다. 그런 다음 해당 목록이 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()

하나의 옵션은 모든 값에 대해 튜플로 한 번에 주장하는 것입니다.

예를 들면 :

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)
?           ^  ++++         ^

이것은 모델과 바퀴 수가 모두 정확하지 않다는 것을 보여줍니다.


단일 단위 테스트에서 여러 개의 어설 션을 갖는 것은 안티 패턴으로 간주됩니다. 단일 단위 테스트는 한 가지만 테스트 할 것으로 예상됩니다. 너무 많이 테스트하고있을 수 있습니다. 이 테스트를 여러 테스트로 분할하는 것을 고려하십시오. 이렇게하면 각 테스트의 이름을 올바르게 지정할 수 있습니다.

그러나 때로는 여러 항목을 동시에 확인하는 것이 좋습니다. 예를 들어 동일한 객체의 속성을 주장 할 때. 이 경우 실제로 그 객체가 올바른지 여부를 주장합니다. 이를 수행하는 방법은 해당 객체에 대해 어설 션하는 방법을 알고있는 사용자 지정 도우미 메서드를 작성하는 것입니다. 실패한 모든 속성을 표시하거나 예를 들어 어설 션이 실패 할 때 예상되는 개체의 전체 상태와 실제 개체의 전체 상태를 표시하는 방식으로 해당 메서드를 작성할 수 있습니다.


별도의 방법으로 각 assert를 수행하십시오.

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)

AssertionError 예외를 캡처하기 위해 @ Anthony-Batchelor의 접근 방식이 마음에 들었습니다. 그러나 데코레이터를 사용하는이 접근 방식에 약간의 변형이 있으며 통과 / 실패로 테스트 사례를보고하는 방법도 있습니다.

#!/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

gtest에서 매우 유용합니다. 이것은 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

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.


나는 PyUnit으로 이것을 할 방법이 없다고 생각하며 PyUnit이 이런 식으로 확장되는 것을보고 싶지 않을 것입니다.

나는 테스트 함수 당 하나의 주장을 고수하는 것을 선호하며 ( 또는 더 구체적으로 테스트 당 하나의 개념을 주장 ) test_addition()4 개의 개별 테스트 함수로 다시 작성 합니다. 이것은 실패에 대한 유용한 정보를 줄 것 즉를 :

.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)

@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()

        # https://stackoverflow.com/questions/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
 : 

More alternative solutions for the correct stacktrace capture could be posted on How to correctly override TestCase.assertEqual(), producing the right stacktrace?


I realize this question was asked literally years ago, but there are now (at least) two Python packages that allow you to do this.

One is softest: https://pypi.org/project/softest/

The other is Python-Delayed-Assert: https://github.com/pr4bh4sh/python-delayed-assert

I haven't used either, but they look pretty similar to me.

참고URL : https://stackoverflow.com/questions/4732827/continuing-in-pythons-unittest-when-an-assertion-fails

반응형