ちょっとしたコマンドラインツールをpythonで作る時に便利なargparseのテストで気をつけるべきこと。

ちょっとしたコマンドラインツールをpythonで作る時に便利なargparseなんですが、位置引数ありparserが出す例外をテストするコードを書いて初めて知ったことがありました。

そのメモ書きになります。

位置引数とは

argparseの公式ページにも載っている通り、コマンドライン実行時に必須となる引数のことです。以下引用。

位置引数は次のように作成します:

python

>

parser.add_argument('bar')

parse_args() が呼ばれたとき、オプション引数は接頭辞 – により識別され、それ以外の引数は位置引数として扱われます:

python

>

parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('-f', '--foo')
parser.add_argument('bar')
parser.parse_args(['BAR'])
Namespace(bar='BAR', foo=None)
parser.parse_args(['BAR', '--foo', 'FOO'])
Namespace(bar='BAR', foo='FOO')
parser.parse_args(['--foo', 'FOO'])
usage: PROG [-h] [-f FOO] bar
PROG: error: too few arguments

位置引数ありのparserをテストする際に気をつけるべきこと

あれっ と思いました。

位置引数ありのparserをテストするとき、エラーケースのテストを書くにはどうするんだ??と。

例えば以下のようなコードを書いたとして、

python
import sys
import argparse

def init(argv=sys.argv[1:]):
arg = argparse.ArgumentParser(
description=""main program to test TS-MPPT-60 monitor modules"")
arg.add_argument(
""host_name"",
type=str,
help=""TS-MPPT-60 host address""
)
arg.add_argument(
""-xa"", ""--xively-api-key"",
type=str,
nargs='?', default=None, const=None,
help=""Xively API key string""
)
return arg.parse_args(argv)

以下のようなテストを書くと、

python
class TestArgParser(unittest.TestCase):
def test_default_args(self):
parsed = argparser.init([])

以下のようなエラーになります。

bash
test_argparser.py: error: the following arguments are required: host_name
Eusage: test_argparser.py [-h] [-xa [XIVELY_API_KEY]] [-xf [XIVELY_FEED_KEY]]
[-kp [KEENIO_PROJECT_ID]] [-kw [KEENIO_WRITE_KEY]]
[-tck [TWITTER_CONSUMER_KEY]]
[-tcs [TWITTER_CONSUMER_SECRET]] [-tk [TWITTER_KEY]]
[-ts [TWITTER_SECRET]] [-be] [-bl [BATTERY_LIMIT]]
[-bs [BATTERY_LIMIT_HOOK_SCRIPT]]
[-ch [CHARGE_CURRENT_HIGH]]
[-bf [BATTERY_FULL_LIMIT]] [-i INTERVAL]
[-l LOG_FILE] [--just-get-status] [--status-all]
[--debug]
host_name
test_argparser.py: error: the following arguments are required: host_name

E

ERROR: test_battery_full_limit (main.TestArgParser)

Traceback (most recent call last):
File ""/Users/takashi/Development/solar_monitor/test/test_argparser.py"", line 81, in test_battery_full_limit
parsed = argparser.init([""-bf"", ])
File ""/Users/takashi/.anyenv/envs/pyenv/versions/test_py35/lib/python3.5/site-packages/solar_monitor/argparser.py"", line 147, in init
return arg.parse_args(argv)
File ""/Users/takashi/.anyenv/envs/pyenv/versions/3.5.1/lib/python3.5/argparse.py"", line 1726, in parse_args
args, argv = self.parse_known_args(args, namespace)
File ""/Users/takashi/.anyenv/envs/pyenv/versions/3.5.1/lib/python3.5/argparse.py"", line 1758, in parse_known_args
namespace, args = self._parse_known_args(args, namespace)
File ""/Users/takashi/.anyenv/envs/pyenv/versions/3.5.1/lib/python3.5/argparse.py"", line 1993, in _parse_known_args
', '.join(required_actions))
File ""/Users/takashi/.anyenv/envs/pyenv/versions/3.5.1/lib/python3.5/argparse.py"", line 2385, in error
self.exit(2, _('%(prog)s: error: %(message)s\n') % args)
File ""/Users/takashi/.anyenv/envs/pyenv/versions/3.5.1/lib/python3.5/argparse.py"", line 2372, in exit
_sys.exit(status)
SystemExit: 2

最後の行に回答があるんですけどね。

SystemExit例外がraiseされることを想定してテストを書けば良いのです。

python
class TestArgParser(unittest.TestCase):
def test_default_args(self):
self.assertRaises(SystemExit, argparser.init, [])

SystemExit例外とは

これはsys.exit()が送出する例外で、Exceptionを継承した普通の例外とはちょっと扱いが異なります。

詳しくは公式のヘルプに載っていますが、重要な部分だけ引用したのが以下です。

Exception をキャッチするコードに誤ってキャッチされないように、Exception ではなく BaseException を継承しています。

つまり、以下のようなコードではキャッチできないんですね。

python
try:
argparser.init([])
except Exception as e:
print(""hoge: "" + type(e).name)

SystemExitをキャッチするにはこうする必要があります。

python
try:
argparser.init([])
except BaseException as e:
print(""hoge: "" + type(e).name)

例外階層をみると、SystemExit以外にもKeyboardInterruptGeneratorExitなんかもBaseExceptionを継承しているようです。なるほど。

まとめ

  • 位置引数ありのparserに位置引数を与えずに実行すると、SystemExit例外を出す(要するにsys.exit()する)
  • SystemExitExceptionではなくBaseExceptionを継承している
  • 位置引数ありのparserのSystemExitを出すケースをテストする際には、普通にunittestassertRaisesが使える
スポンサーリンク

シェアする

  • このエントリーをはてなブックマークに追加

フォローする