python(unittestモジュールを使用))で単体テストを作成している場合、失敗したテストからデータを出力することは可能ですか?一部の情報を伝えることができるカスタマイズされたメッセージを作成する機能を知っていますが、文字列として簡単に表現できない、より複雑なデータを処理する場合があります。
たとえば、クラスFooがあり、testdataというリストのデータを使用してメソッドバーをテストしているとします。
class TestBar(unittest.TestCase):
def runTest(self):
for t1, t2 in testdata:
f = Foo(t1)
self.assertEqual(f.bar(t2), 2)
テストが失敗した場合、t1、t2、および/またはfを出力して、この特定のデータが失敗に終わった理由を確認できます。出力とは、テストが実行された後、他の変数と同様に変数にアクセスできることを意味します。
私のように、簡単で素早い答えを求めてここに来る人にとっては非常に遅い答えです。
Python 2.7では、追加パラメーターmsg
を使用して、次のようなエラーメッセージに情報を追加できます。
self.assertEqual(f.bar(t2), 2, msg='{0}, {1}'.format(t1, t2))
公式ドキュメント ここ
このためにロギングモジュールを使用します。
例えば:
import logging
class SomeTest( unittest.TestCase ):
def testSomething( self ):
log= logging.getLogger( "SomeTest.testSomething" )
log.debug( "this= %r", self.this )
log.debug( "that= %r", self.that )
# etc.
self.assertEquals( 3.14, pi )
if __== "__main__":
logging.basicConfig( stream=sys.stderr )
logging.getLogger( "SomeTest.testSomething" ).setLevel( logging.DEBUG )
unittest.main()
これにより、失敗していることがわかっており、追加のデバッグ情報が必要な特定のテストのデバッグをオンにできます。
ただし、私の好みの方法は、デバッグに多くの時間を費やすのではなく、よりきめ細かいテストを記述して問題を明らかにすることです。
単純なprintステートメント、またはstdoutへのその他の書き込み方法を使用できます。また、テストのどこでもPythonデバッガーを呼び出すことができます。
nose を使用してテストを実行すると(推奨)、各テストの標準出力が収集され、テストが失敗した場合にのみ表示されるため、テストに合格すると出力が乱雑になります。
また、noseには、アサートで言及された変数を自動的に表示するスイッチ、または失敗したテストでデバッガーを起動するスイッチがあります。例えば -s
(--nocapture
)stdoutのキャプチャを防ぎます。
私はこれがあなたの探しているものとはまったく思えません、失敗しない変数値を表示する方法はありませんが、これはあなたが望むように結果を出力することに近づくのに役立つかもしれません。
結果の分析と処理には、TestRunner.run()によって返されるTestResultオブジェクトを使用できます。 。特に、TestResult.errorsおよびTestResult.failures
TestResultsオブジェクトについて:
http://docs.python.org/library/unittest.html#id
そして、あなたを正しい方向に向けるコード:
>>> import random
>>> import unittest
>>>
>>> class TestSequenceFunctions(unittest.TestCase):
... def setUp(self):
... self.seq = range(5)
... def testshuffle(self):
... # make sure the shuffled sequence does not lose any elements
... random.shuffle(self.seq)
... self.seq.sort()
... self.assertEqual(self.seq, range(10))
... def testchoice(self):
... element = random.choice(self.seq)
... error_test = 1/0
... self.assert_(element in self.seq)
... def testsample(self):
... self.assertRaises(ValueError, random.sample, self.seq, 20)
... for element in random.sample(self.seq, 5):
... self.assert_(element in self.seq)
...
>>> suite = unittest.TestLoader().loadTestsFromTestCase(TestSequenceFunctions)
>>> testResult = unittest.TextTestRunner(verbosity=2).run(suite)
testchoice (__main__.TestSequenceFunctions) ... ERROR
testsample (__main__.TestSequenceFunctions) ... ok
testshuffle (__main__.TestSequenceFunctions) ... FAIL
======================================================================
ERROR: testchoice (__main__.TestSequenceFunctions)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<stdin>", line 11, in testchoice
ZeroDivisionError: integer division or modulo by zero
======================================================================
FAIL: testshuffle (__main__.TestSequenceFunctions)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<stdin>", line 8, in testshuffle
AssertionError: [0, 1, 2, 3, 4] != [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
----------------------------------------------------------------------
Ran 3 tests in 0.031s
FAILED (failures=1, errors=1)
>>>
>>> testResult.errors
[(<__main__.TestSequenceFunctions testMethod=testchoice>, 'Traceback (most recent call last):\n File "<stdin>"
, line 11, in testchoice\nZeroDivisionError: integer division or modulo by zero\n')]
>>>
>>> testResult.failures
[(<__main__.TestSequenceFunctions testMethod=testshuffle>, 'Traceback (most recent call last):\n File "<stdin>
", line 8, in testshuffle\nAssertionError: [0, 1, 2, 3, 4] != [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n')]
>>>
私はこれを考えすぎていたのではないかと思います。仕事をする私が思いついた一つの方法は、単に診断データを蓄積するグローバル変数を持つことです。
このようなもの:
log1 = dict()
class TestBar(unittest.TestCase):
def runTest(self):
for t1, t2 in testdata:
f = Foo(t1)
if f.bar(t2) != 2:
log1("TestBar.runTest") = (f, t1, t2)
self.fail("f.bar(t2) != 2")
返信いただきありがとうございます。単体テストから情報を記録する方法について、いくつかの代替アイデアを提供してくれました。
別のオプション-テストが失敗した場所でデバッガーを起動します。
Testoobでテストを実行してみて(変更なしでunittestスイートを実行します)、「-debug」コマンドラインスイッチを使用して、テストが失敗したときにデバッガーを開くことができます。
Windowsのターミナルセッションを次に示します。
C:\work> testoob tests.py --debug
F
Debugging for failure in test: test_foo (tests.MyTests.test_foo)
> c:\python25\lib\unittest.py(334)failUnlessEqual()
-> (msg or '%r != %r' % (first, second))
(Pdb) up
> c:\work\tests.py(6)test_foo()
-> self.assertEqual(x, y)
(Pdb) l
1 from unittest import TestCase
2 class MyTests(TestCase):
3 def test_foo(self):
4 x = 1
5 y = 2
6 -> self.assertEqual(x, y)
[EOF]
(Pdb)
私が使用する方法は本当に簡単です。実際に表示されるように、警告として記録するだけです。
import logging
class TestBar(unittest.TestCase):
def runTest(self):
#this line is important
logging.basicConfig()
log = logging.getLogger("LOG")
for t1, t2 in testdata:
f = Foo(t1)
self.assertEqual(f.bar(t2), 2)
log.warning(t1)
そのためにlogging
モジュールを使用できます。
そのため、単体テストコードでは、次を使用します。
import logging as log
def test_foo(self):
log.debug("Some debug message.")
log.info("Some info message.")
log.warning("Some warning message.")
log.error("Some error message.")
デフォルトでは、警告とエラーは/dev/stderr
に出力されるため、コンソールに表示されるはずです。
ログ(カスタマイズなど)をカスタマイズするには、次のサンプルを試してください。
# Set-up logger
if args.verbose or args.debug:
logging.basicConfig( stream=sys.stdout )
root = logging.getLogger()
root.setLevel(logging.INFO if args.verbose else logging.DEBUG)
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO if args.verbose else logging.DEBUG)
ch.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(name)s: %(message)s'))
root.addHandler(ch)
else:
logging.basicConfig(stream=sys.stderr)
ロギングを使用:
import unittest
import logging
import inspect
import os
logging_level = logging.INFO
try:
log_file = os.environ["LOG_FILE"]
except KeyError:
log_file = None
def logger(stack=None):
if not hasattr(logger, "initialized"):
logging.basicConfig(filename=log_file, level=logging_level)
logger.initialized = True
if not stack:
stack = inspect.stack()
name = stack[1][3]
try:
name = stack[1][0].f_locals["self"].__class__.__+ "." + name
except KeyError:
pass
return logging.getLogger(name)
def todo(msg):
logger(inspect.stack()).warning("TODO: {}".format(msg))
def get_pi():
logger().info("sorry, I know only three digits")
return 3.14
class Test(unittest.TestCase):
def testName(self):
todo("use a better get_pi")
pi = get_pi()
logger().info("pi = {}".format(pi))
todo("check more digits in pi")
self.assertAlmostEqual(pi, 3.14)
logger().debug("end of this test")
pass
使用法:
# LOG_FILE=/tmp/log python3 -m unittest LoggerDemo
.
----------------------------------------------------------------------
Ran 1 test in 0.047s
OK
# cat /tmp/log
WARNING:Test.testName:TODO: use a better get_pi
INFO:get_pi:sorry, I know only three digits
INFO:Test.testName:pi = 3.14
WARNING:Test.testName:TODO: check more digits in pi
LOG_FILE
を設定しない場合、ロギングはstderr
になります。
これらの場合に私がやることは、アプリケーションにいくつかのメッセージを含むlog.debug()
を含めることです。デフォルトのログレベルはWARNING
であるため、このようなメッセージは通常の実行では表示されません。
次に、unittestでログレベルをDEBUG
に変更し、実行中にそのようなメッセージが表示されるようにします。
_import logging
log.debug("Some messages to be shown just when debugging or unittesting")
_
ユニットテストで:
_# Set log level
loglevel = logging.DEBUG
logging.basicConfig(level=loglevel)
_
完全な例を参照してください。
これは_daikiri.py
_で、名前と価格でDaikiriを実装する基本クラスです。特定の割引を適用した後、特定のダイキリの価格を返すメソッドmake_discount()
があります。
_import logging
log = logging.getLogger(__name__)
class Daikiri(object):
def __init__(self, name, price):
self.name = name
self.price = price
def make_discount(self, percentage):
log.debug("Deducting discount...") # I want to see this message
return self.price * percentage
_
次に、使用状況をチェックするunittest _test_daikiri.py
_を作成します。
_import unittest
import logging
from .daikiri import Daikiri
class TestDaikiri(unittest.TestCase):
def setUp(self):
# Changing log level to DEBUG
loglevel = logging.DEBUG
logging.basicConfig(level=loglevel)
self.mydaikiri = Daikiri("cuban", 25)
def test_drop_price(self):
new_price = self.mydaikiri.make_discount(0)
self.assertEqual(new_price, 0)
if __== "__main__":
unittest.main()
_
したがって、実行すると_log.debug
_メッセージが表示されます。
_$ python -m test_daikiri
DEBUG:daikiri:Deducting discount...
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
_
inspect.traceを使用すると、例外がスローされた後にローカル変数を取得できます。その後、次のようなデコレータで単体テストをラップし、事後分析中にそれらのローカル変数を検査のために保存できます。
import random
import unittest
import inspect
def store_result(f):
"""
Store the results of a test
On success, store the return value.
On failure, store the local variables where the exception was thrown.
"""
def wrapped(self):
if 'results' not in self.__dict__:
self.results = {}
# If a test throws an exception, store local variables in results:
try:
result = f(self)
except Exception as e:
self.results[f.__name__] = {'success':False, 'locals':inspect.trace()[-1][0].f_locals}
raise e
self.results[f.__name__] = {'success':True, 'result':result}
return result
return wrapped
def suite_results(suite):
"""
Get all the results from a test suite
"""
ans = {}
for test in suite:
if 'results' in test.__dict__:
ans.update(test.results)
return ans
# Example:
class TestSequenceFunctions(unittest.TestCase):
def setUp(self):
self.seq = range(10)
@store_result
def test_shuffle(self):
# make sure the shuffled sequence does not lose any elements
random.shuffle(self.seq)
self.seq.sort()
self.assertEqual(self.seq, range(10))
# should raise an exception for an immutable sequence
self.assertRaises(TypeError, random.shuffle, (1,2,3))
return {1:2}
@store_result
def test_choice(self):
element = random.choice(self.seq)
self.assertTrue(element in self.seq)
return {7:2}
@store_result
def test_sample(self):
x = 799
with self.assertRaises(ValueError):
random.sample(self.seq, 20)
for element in random.sample(self.seq, 5):
self.assertTrue(element in self.seq)
return {1:99999}
suite = unittest.TestLoader().loadTestsFromTestCase(TestSequenceFunctions)
unittest.TextTestRunner(verbosity=2).run(suite)
from pprint import pprint
pprint(suite_results(suite))
最後の行は、テストが成功した場合の戻り値と、失敗した場合のローカル変数(この場合はx)を出力します。
{'test_choice': {'result': {7: 2}, 'success': True},
'test_sample': {'locals': {'self': <__main__.TestSequenceFunctions testMethod=test_sample>,
'x': 799},
'success': False},
'test_shuffle': {'result': {1: 2}, 'success': True}}
Har detgøy:-)
アサーションの失敗から生成される例外をキャッチしてみませんか? catchブロックでは、どこにでも好きなようにデータを出力できます。その後、完了したら、例外を再スローできます。テストランナーはおそらく違いを知らないでしょう。
免責事項:私はPythonの単体テストフレームワークでこれを試していませんが、他の単体テストフレームワークで試しました。
私はそれを試していないと認めて、 testfixtures 'logging feature は非常に便利に見えます...