Pythonモジュールのargparse部分のテストをどのように記述しますか?[閉まっている]


162

argparseライブラリを使用するPythonモジュールがあります。コードベースのそのセクションのテストを作成するにはどうすればよいですか?


argparseはコマンドラインインターフェイスです。コマンドラインを介してアプリケーションを呼び出すテストを記述します。
Homer6 2013

あなたの質問はあなたがテストしたいものを理解することを難しくします。それは最終的には疑わしいでしょう。たとえば、「コマンドライン引数X、Y、Zを使用するfoo()と、関数が呼び出されます」。sys.argvそうだとすれば、のモックが答えです。cli-test-helpers Pythonパッケージをご覧ください。また、stackoverflow.com
a / 58594599/202834

回答:


214

コードをリファクタリングし、解析を関数に移動する必要があります。

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

4
ご回答有難うございます。特定の引数が渡されない場合にエラーをテストするにはどうすればよいですか?
Pratik Khadloya 2015

3
@PratikKhadloya引数が必須で渡されない場合、argparseは例外を発生させます。
Viktor Kerkez、2015

2
@PratikKhadloyaはい、メッセージは残念ながら本当に役に立ちません:(それはただ2... ...にargparse直接出力されるので、テストフレンドリーではありませんsys.stderr...
ヴィクトルKerkez

1
@ViktorKerkez sys.stderrをモックして特定のメッセージを確認できる場合があります。mock.assert_called_withまたはmock_callsを調べることにより、詳細についてはdocs.python.org/3/library/unittest.mock.htmlを参照してください。stdinのモックの例については、stackoverflow.com / questions / 6271947 /…も参照してください。(stderrは類似している必要があります)
BryCoBat

1
@PratikKhadloyaエラーの処理/テストに関する私の答えをご覧くださいstackoverflow.com/a/55234595/1240268
アンディヘイデン

25

「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ます。
imrek

1
@DrunkenMaster卑劣な口調でお詫びします。回答を説明と可能な用途で更新しました。私もここで学んでいるので、もしそうなら、あなた(または他の誰か)が戻り値をモックすることが有益であるケースを提供できますか?(または、少なくとも戻り値をモックしないことが有害な場合)
munsu

1
from unittest import mock正しいインポート方法になりました-少なくともpython3の場合はそうです
マイケルホール

1
@MichaelHallありがとう。スニペットを更新し、コンテキスト情報を追加しました。
munsu 2018

1
Namespaceここでのクラスの使用は、まさに私が探していたものです。テストは依然としてに依存しargparseていargparseますが、テスト中のコードによるの特定の実装には依存していません。これは、私の単体テストにとって重要です。さらに、pytestparametrize()メソッドを使用して、を含むテンプレート化されたモックでさまざまな引数の組み合わせをすばやくテストできますreturn_value=argparse.Namespace(accumulate=accumulate, integers=integers)
アセトン

17

あなたの作る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')

9
  1. を使用して引数リストにデータを入力してsys.argv.append()から、を呼び出し parse()、結果を確認して繰り返します。
  2. フラグとダンプ引数フラグを含むバッチ/ bashファイルから呼び出します。
  3. すべての引数の解析を別のファイルとif __name__ == "__main__":呼び出しの解析に置き、結果をダンプ/評価してから、バッチ/ bashファイルからこれをテストします。

9

元の配信スクリプトを変更したくなかったのでsys.argv、argparse の部分をモックアウトしました。

from unittest.mock import patch

with patch('argparse._sys.argv', ['python', 'serve.py']):
    ...  # your test code here

これは、argparseの実装が変更された場合に機能しなくなりますが、簡単なテストスクリプトには十分です。とにかく、感度はテストスクリプトの具体性よりもはるかに重要です。


6

パーサーをテストする簡単な方法は次のとおりです。

parser = ...
parser.add_argument('-a',type=int)
...
argv = '-a 1 foo'.split()  # or ['-a','1','foo']
args = parser.parse_args(argv)
assert(args.a == 1)
...

別の方法は、を変更sys.argvして呼び出すことですargs = parser.parse_args()

テストの例がたくさんありますargparseでは、lib/test/test_argparse.py


5

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

2

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

0

コマンド出力ではなく 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 == "?"
    ...
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.