対処したい課題
こんなパーサがあったとする。
__version__ = '0.0.1'
def prepare_parser():
parser = ArgumentParser(description=(__doc__),
formatter_class=RawDescriptionHelpFormatter)
parser.add_argument('-d', '--debug', action='store_true',
help='Show debug log')
parser.add_argument('-v', '--version', action='version',
version=__version__,
help='Show version and exit')
return parser
このverison部分がちゃんと使えることをテストしたい、とする。
厄介なことに、 parser.parse_args(['-v'])
とかするとその時点でSystemExitが飛ぶ。
>>> from main import prepare_parser
>>> parser = prepare_parser()
>>> args = parser.parse_args(['-v'])
0.0.1
$ <-- SystemExitが飛ぶのでインタープリタが終了した
これはargparseのversionアクションの仕様だ。
'version' - This expects a version= keyword argument in the add_argument() call, and prints version information and exits when invoked:
ユニットテストの途中で sys.exit()
が無造作に飛ぶのは避けたい。またバージョンがちゃんと出力されていることは確認しときたい。
patch ( https://docs.python.jp/3/library/unittest.mock-examples.html#patch-decorators )もあり得るが、出来ればargparseの中身はブラックボックスとして扱いたい。
やったこと
次のようにしてみる。
from contextlib import contextmanager
from io import StringIO
import unittest
import sys
from main import prepare_parser, __version__
@contextmanager
def capture_stdout(command, *args, **kwargs):
out, sys.stdout = sys.stdout, StringIO()
try:
command(*args, **kwargs)
finally:
sys.stdout.seek(0)
yield sys.stdout.read()
sys.stdout = out
class MyTest(unittest.TestCase):
def test_version(self):
parser = prepare_parser()
with self.assertRaises(SystemExit):
with capture_stdout(parser.parse_args, ['-v']) as output:
self.assertIsNotNone(output)
self.assertEqual(__version__, output.rstrip())
with self.assertRaises(SystemExit):
with capture_stdout(parser.parse_args,
['--version']) as output:
self.assertIsNotNone(output)
self.assertEqual(__version__, output.rstrip())
if __name__ == '__main__':
unittest.main()
SystemExit
が飛ばされることを期待しつつ、かつ例外が飛んだ際にstdoutに送られていた文字列をコンテクストマネージャにyieldしてもらうことで
- versionを標準出力に表示すること
- sys.exit()等で終了すること
をテスト出来るに至った。
-v
等をパースするオプション部分をコメントアウトした場合
$ python tests.py
usage: tests.py [-h] [-d]
tests.py: error: unrecognized arguments: -v
F
======================================================================
FAIL: test_version (__main__.MyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "tests.py", line 26, in test_version
self.assertEqual(__version__, output.rstrip())
AssertionError: '0.0.1' != ''
- 0.0.1
+
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
コメントインした場合
$ python tests.py
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
感想
あんまりやりたい類のテストではない気はする。
このテストは脆弱か……?よくわからない。
もっと良い方法ないだろか
確認に使った環境
- macOS Sierra 10.12.5
- Python 3.5.2, Python 3.6.1 (pyenv)