質問

編集:より良い例に切り替えて、なぜこれが本当の問題であるのかを明らかにしました。

単一のテストで複数の障害を見ることができるように、アサーションが失敗したときに実行を継続する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!

ここで、テストの目的は、車のことを確実にすることです __init__ フィールドを正しく設定します。私はそれを4つの方法に分割することができました(そしてそれはしばしば素晴らしいアイデアです)が、この場合、単一の概念をテストする単一の方法として保持する方が読みやすいと思います(「オブジェクトは正しく初期化されます」)。

ここでメソッドを解散しないことが最善であると仮定した場合、新しい問題があります。すべてのエラーが一度に表示されることはありません。修正するとき model エラーと再実行、次に wheel_count エラーが表示されます。最初にテストを実行したときに、両方のエラーが表示される時間を節約できます。

比較のために、GoogleのC ++ユニットテストフレームワーク 間を区別します 非脂肪の間 EXPECT_* アサーションと致命的 ASSERT_* アサーション:

アサーションは、同じことをテストするが、現在の関数に異なる影響を与えるペアで提供されます。 ASSERT_*バージョンは、失敗したときに致命的な障害を生成し、現在の関数を中止します。 expect_*バージョンは、現在の関数を中止しない非致命的な障害を生成します。通常、試験で複数の障害を報告することを許可するため、通常は期待されています。ただし、ASSERT_*は、問題のアサーションが失敗した場合に継続することが意味をなさない場合は使用する必要があります。

得る方法はありますか EXPECT_*- Pythonの動作のように unittest?入っていない場合 unittest, 、次に、この動作をサポートする別のPythonユニットテストフレームワークはありますか?


ちなみに、私は何回の実際のテストが非致命的な主張から利益を得るかについて興味がありましたので、私はいくつかを見ました コードの例 (Googleコード検索の代わりにSearchCodeを使用するために2014-08-19を編集しました、RIP)。最初のページからランダムに選択された10の結果のうち、すべてが同じテスト方法で複数の独立したアサーションを作成するテストが含まれていました。すべてが致命的ではない主張の恩恵を受けるでしょう。

役に立ちましたか?

解決

あなたがおそらくやりたいことは派生することです unittest.TestCase それは、アサーションが失敗したときにスローするクラスだからです。あなたはあなたを再び編成する必要があります TestCase 投げないでください(代わりに失敗のリストを保持します)。ものを再編成すると、解決しなければならない他の問題を引き起こす可能性があります。たとえば、導き出す必要がある場合があります TestSuite あなたに加えられた変更をサポートするために変更を加えるために TestCase.

他のヒント

非致命的な主張をする別の方法は、アサーション例外をキャプチャし、例外をリストに保存することです。次に、そのリストは断取り候の一部として空であると主張します。

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

1つのオプションは、すべての値をタプルとして一度にアサートすることです。

例えば:

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

これは、モデルとホイール数の両方が正しくないことを示しています。

単一のユニットテストで複数のアサートを持つためのアンチパターンと見なされます。単一のユニットテストでは、1つのことだけをテストすることが期待されています。おそらくあなたはテストをしすぎているでしょう。このテストを複数のテストに分割することを検討してください。これにより、各テストに適切に名前を付けることができます。

ただし、複数のものを同時に確認してもかまいません。たとえば、同じオブジェクトのプロパティを主張する場合。その場合、実際には、そのオブジェクトが正しいかどうかを主張しています。これを行う方法は、そのオブジェクトを主張する方法を知っているカスタムヘルパーメソッドを作成することです。その方法は、すべての故障したプロパティを表示するように、またはたとえば、アサートが失敗したときに予想されるオブジェクトの完全な状態と実際のオブジェクトの完全な状態を示すように書くことができます。

それぞれが別の方法でアサートします。

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で非常に役立つことを期待してください。これはPythonの方法です 要旨, 、およびコード:

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がこのように拡張されるのを見たくないでしょう。

テスト関数ごとに1つのアサーションに固執することを好みます(より具体的には、テストごとに1つの概念を主張します)そして書き直します 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)

このアプローチがあなたのためではないと判断した場合、あなたは見つけるかもしれません この答え 役に立った。

アップデート

更新された質問で2つの概念をテストしているようで、これらを2つのユニットテストに分割します。 1つ目は、パラメーターが新しいオブジェクトの作成時に保存されていることです。これには2つのアサーションがあります make そして、1つ model. 。最初の障害が失敗した場合、2番目のパスまたは故障がこの時点では無関係であるかどうかにかかわらず、明らかに修正する必要があります。

2番目の概念はより疑わしいものです...いくつかのデフォルト値が初期化されているかどうかをテストしています。 なぜ?これらの値が実際に使用されている時点でこれらの値をテストする方が便利です(そして、それらが使用されていない場合、なぜそこにいるのですか?)。

これらのテストは両方とも失敗し、両方とも必要です。私がユニットテストをしているとき、私は成功しているよりも失敗にはるかに興味があります。それは私が集中する必要がある場所です。

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
 : 

正しいStacktraceキャプチャのためのより多くの代替ソリューションを投稿することができます testcase.assertequal()を正しく上書きする方法、適切なスタックトレースを生成するにはどうすればよいですか?

この質問は文字通り数年前に尋ねられたことに気付きましたが、これを行うことができる2つのPythonパッケージがあります。

1つは最も柔らかいです: https://pypi.org/project/softest/

もう1つはPython-Delayed-Assertです。 https://github.com/pr4bh4sh/python-delayed-assert

私も使用していませんが、彼らは私にかなり似ています。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top