argparseライブラリを使用するPythonモジュールがあります。コードベースのそのセクションのテストを作成するにはどうすればよいですか?
foo()
と、関数が呼び出されます」。sys.argv
そうだとすれば、のモックが答えです。cli-test-helpers Pythonパッケージをご覧ください。また、stackoverflow.com
argparseライブラリを使用するPythonモジュールがあります。コードベースのそのセクションのテストを作成するにはどうすればよいですか?
foo()
と、関数が呼び出されます」。sys.argv
そうだとすれば、のモックが答えです。cli-test-helpers Pythonパッケージをご覧ください。また、stackoverflow.com
回答:
コードをリファクタリングし、解析を関数に移動する必要があります。
def parse_args(args):
parser = argparse.ArgumentParser(...)
parser.add_argument...
# ...Create your parser as you like...
return parser.parse_args(args)
次に、main
関数でそれを呼び出すだけです:
parser = parse_args(sys.argv[1:])
(の最初の要素 sys.argv
スクリプト名を表す、CLI操作中に追加のスイッチとして送信されないように削除されています。)
テストでは、次に、テストする引数のリストを使用してパーサー関数を呼び出すことができます。
def test_parser(self):
parser = parse_args(['-l', '-m'])
self.assertTrue(parser.long)
# ...and so on.
この方法では、パーサーをテストするためだけにアプリケーションのコードを実行する必要はありません。
アプリケーションの後でパーサーのオプションを変更または追加する必要がある場合は、ファクトリメソッドを作成します。
def create_parser():
parser = argparse.ArgumentParser(...)
parser.add_argument...
# ...Create your parser as you like...
return parser
後で必要に応じて操作でき、テストは次のようになります。
class ParserTest(unittest.TestCase):
def setUp(self):
self.parser = create_parser()
def test_something(self):
parsed = self.parser.parse_args(['--something', 'test'])
self.assertEqual(parsed.something, 'test')
2
... ...にargparse
直接出力されるので、テストフレンドリーではありませんsys.stderr
...
「argparse部分」は少し曖昧なので、この回答は1つの部分に焦点を当てています。 parse_args
メソッドにます。これは、コマンドラインとやり取りして、渡されたすべての値を取得するメソッドです。基本的に、parse_args
コマンドラインから実際に値を取得する必要がないように、戻り値をモックすることができます。mock
パッケージには、 Pythonのバージョン2.6から3.2のためのピップを介してインストールすることができます。unittest.mock
バージョン3.3以降の標準ライブラリの一部です。
import argparse
try:
from unittest import mock # python 3.3+
except ImportError:
import mock # python 2.6-3.2
@mock.patch('argparse.ArgumentParser.parse_args',
return_value=argparse.Namespace(kwarg1=value, kwarg2=value))
def test_command(mock_args):
pass
Namespace
それらが渡されない場合でも、すべてのコマンドメソッドの引数を含める必要があります。これらの引数にの値を指定しNone
ます。(ドキュメントを参照)このスタイルは、メソッドの引数ごとに異なる値が渡される場合のテストをすばやく行うのに役立ちます。Namespace
テストで完全なargparseの非依存性を模倣することを選択する場合は、実際のNamespace
クラスと同様に動作することを確認してください。
以下は、argparseライブラリの最初のスニペットを使用した例です。
# test_mock_argparse.py
import argparse
try:
from unittest import mock # python 3.3+
except ImportError:
import mock # python 2.6-3.2
def main():
parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
help='an integer for the accumulator')
parser.add_argument('--sum', dest='accumulate', action='store_const',
const=sum, default=max,
help='sum the integers (default: find the max)')
args = parser.parse_args()
print(args) # NOTE: this is how you would check what the kwargs are if you're unsure
return args.accumulate(args.integers)
@mock.patch('argparse.ArgumentParser.parse_args',
return_value=argparse.Namespace(accumulate=sum, integers=[1,2,3]))
def test_command(mock_args):
res = main()
assert res == 6, "1 + 2 + 3 = 6"
if __name__ == "__main__":
print(main())
argparse
そのNamespace
クラスに依存しています。あなたはあざける必要がありNamespace
ます。
from unittest import mock
正しいインポート方法になりました-少なくともpython3の場合はそうです
Namespace
ここでのクラスの使用は、まさに私が探していたものです。テストは依然としてに依存しargparse
ていargparse
ますが、テスト中のコードによるの特定の実装には依存していません。これは、私の単体テストにとって重要です。さらに、pytest
のparametrize()
メソッドを使用して、を含むテンプレート化されたモックでさまざまな引数の組み合わせをすばやくテストできますreturn_value=argparse.Namespace(accumulate=accumulate, integers=integers)
。
あなたの作るmain()
機能テイクをargv
、むしろそれはさせるよりも、引数としてから読み取るsys.argv
ことが、デフォルトでなりますよう:
# mymodule.py
import argparse
import sys
def main(args):
parser = argparse.ArgumentParser()
parser.add_argument('-a')
process(**vars(parser.parse_args(args)))
return 0
def process(a=None):
pass
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
その後、正常にテストできます。
import mock
from mymodule import main
@mock.patch('mymodule.process')
def test_main(process):
main([])
process.assert_call_once_with(a=None)
@mock.patch('foo.process')
def test_main_a(process):
main(['-a', '1'])
process.assert_call_once_with(a='1')
sys.argv.append()
から、を呼び出し
parse()
、結果を確認して繰り返します。if __name__ == "__main__":
呼び出しの解析に置き、結果をダンプ/評価してから、バッチ/ bashファイルからこれをテストします。parse_args
a SystemExit
をスローしてstderrに出力します。これらの両方をキャッチできます。
import contextlib
import io
import sys
@contextlib.contextmanager
def captured_output():
new_out, new_err = io.StringIO(), io.StringIO()
old_out, old_err = sys.stdout, sys.stderr
try:
sys.stdout, sys.stderr = new_out, new_err
yield sys.stdout, sys.stderr
finally:
sys.stdout, sys.stderr = old_out, old_err
def validate_args(args):
with captured_output() as (out, err):
try:
parser.parse_args(args)
return True
except SystemExit as e:
return False
stderrを検査します(err.seek(0); err.read()
ただし、通常はその細分性は必要ありません。
今、あなたはassertTrue
好きなテストを使うことができます:
assertTrue(validate_args(["-l", "-m"]))
あるいは、(の代わりにSystemExit
)別のエラーをキャッチして再スローすることもできます。
def validate_args(args):
with captured_output() as (out, err):
try:
return parser.parse_args(args)
except SystemExit as e:
err.seek(0)
raise argparse.ArgumentError(err.read())
argparse.ArgumentParser.parse_args
関数の結果を渡すときにnamedtuple
、テストのために引数をモックするためにa を使用することがあります。
import unittest
from collections import namedtuple
from my_module import main
class TestMyModule(TestCase):
args_tuple = namedtuple('args', 'arg1 arg2 arg3 arg4')
def test_arg1(self):
args = TestMyModule.args_tuple("age > 85", None, None, None)
res = main(args)
assert res == ["55289-0524", "00591-3496"], 'arg1 failed'
def test_arg2(self):
args = TestMyModule.args_tuple(None, [42, 69], None, None)
res = main(args)
assert res == [], 'arg2 failed'
if __name__ == '__main__':
unittest.main()
コマンド出力ではなく CLI(コマンドラインインターフェイス)をテストするために、私はこのようなことをしました
import pytest
from argparse import ArgumentParser, _StoreAction
ap = ArgumentParser(prog="cli")
ap.add_argument("cmd", choices=("spam", "ham"))
ap.add_argument("-a", "--arg", type=str, nargs="?", default=None, const=None)
...
def test_parser():
assert isinstance(ap, ArgumentParser)
assert isinstance(ap, list)
args = {_.dest: _ for _ in ap._actions if isinstance(_, _StoreAction)}
assert args.keys() == {"cmd", "arg"}
assert args["cmd"] == ("spam", "ham")
assert args["arg"].type == str
assert args["arg"].nargs == "?"
...