コマンドラインオプションのテストを書く

見てしまった、、、


昨日の今日なのでやりましょう、頼まれていませんがー(> <)

テスト対象のサンプルとして http://d.hatena.ne.jp/t2y-1979/20101124/1290534464 から optparse モジュール を利用している parse_option() 関数だけ抜き出してきます。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import optparse
import os
import sys

# Globals
VERSION = "0.30"

def parse_option():
    usage = "usage: %prog [option] eratos.log"
    ver = "%s %s" % ("%prog", VERSION)
    parser = optparse.OptionParser(usage, version=ver)
    parser.add_option("-f", "--from", dest="date_from",
                      metavar="YYYYMMDDHHMM", help="filter with from date")
    parser.add_option("-t", "--to", dest="date_to",
                      metavar="YYYYMMDDHHMM", help="filter with date to")
    parser.add_option("-q", "--query", dest="query",
                      metavar="STRING", help="filter with string")
    parser.add_option("-o", "--outputfile", dest="output",
                      metavar="OUTPUT_FILE", default="checked.log",
                      help = "output file name")
    parser.add_option("-e", "--error", dest="error",
                      action="store_true", default=False,
                      help="filter with error log message")
    parser.add_option("-v", "--verbose", dest="verbose",
                      action="store_true", default=False,
                      help="print log on standard output")
    
    opts, args = parser.parse_args()
    if args and os.access(args[0], os.R_OK):
        return opts, args
    else:
        print parser.print_help()
        sys.exit(0)

def main():
    opts, args = parse_option()

if __name__ == "__main__":
    main()

Pikzie (ピクジー) のデータ駆動テストを利用してテストを書きます*1。デコレータでデータを登録できるのですっきり書けます。

# -*- coding: utf-8 -*-

import sys
import tempfile
import pikzie
from StringIO import StringIO

# target modules for test
from log_analyzer import VERSION, parse_option

class TestCommandLineParser(pikzie.TestCase):
    """Test for command line parser"""

    def setup(self):
        self.log_file = tempfile.NamedTemporaryFile()
        self.log_name = self.log_file.name
        sys.stdout, sys.stderr = StringIO(), StringIO()

    def teardown(self):
        self.log_file.close()

    @pikzie.data("ver01", ["prog", "--version"])
    def test_version(self, argv):
        sys.argv = argv
        self.assert_raise_call(SystemExit, parse_option)
        self.assert_equal(sys.stdout.getvalue().split()[-1], VERSION)

    @pikzie.data("from03", ["prog", "--from", "20101124" ])
    @pikzie.data("from02", ["prog", "-f", "201011241952", "-e"])
    @pikzie.data("from01", ["prog", "-f", "201011241952"])
    def test_date_from(self, argv):
        argv.append(self.log_name)
        sys.argv = argv
        opts, args = parse_option()
        self.assert_equal(opts.date_from, argv[2])
        self.assert_equal(args, argv[-1:])

    @pikzie.data("err_true03", ["prog", "-e", "-v", "--error"])
    @pikzie.data("err_true02", ["prog", "-t", "201011242045", "-e"])
    @pikzie.data("err_true01", ["prog", "-e"])
    def test_error_true(self, argv):
        argv.append(self.log_name)
        sys.argv = argv
        opts, args = parse_option()
        self.assert_true(opts.error)
        self.assert_equal(args, argv[-1:])

    @pikzie.data("err_false03", ["prog", "--outputfile", "t.log"])
    @pikzie.data("err_false02", ["prog", "-q", "クエリ", "-v"])
    @pikzie.data("err_false01", ["prog", "-f", "201011242035"])
    def test_error_false(self, argv):
        argv.append(self.log_name)
        sys.argv = argv
        opts, args = parse_option()
        self.assert_false(opts.error)
        self.assert_equal(args, argv[-1:])

    @pikzie.data("fail05", ["prog", "-v", "--detarame", "-e"])
    @pikzie.data("fail04", ["prog", "-o", "-t", "201011242036"])
    @pikzie.data("fail03", ["prog", "-q", "a", "--outputfile"])
    @pikzie.data("fail02", ["prog", "-f", "--error", "20101124"])
    @pikzie.data("fail01", ["prog", "--from"])
    def test_parser_fail(self, argv):
        argv.append(self.log_name)
        sys.argv = argv
        self.assert_raise_call(SystemExit, parse_option)

    @pikzie.data("noargs01", ["prog", "-v"])
    def test_no_args(self, argv):
        sys.argv = argv
        self.assert_raise_call(SystemExit, parse_option)

テストを実行します。

$ python test_log_analyzer_optparse.py 
................
Finished in 0.043 seconds

16 test(s), 33 assertion(s), 0 failure(s), 0 error(s),
 0 pending(s), 0 omission(s), 0 notification(s)

全部、成功しました!テストはやりだすと成功するのが嬉しくて楽しくなってきます。

Pikzie の良いところの1つはテストが失敗したときの見易さもあります。ここではわざと test_date_from() のテストが失敗するように "from02" のデータを変更します。

(変更前)
    @pikzie.data("from02", ["prog", "-f", "201011241952", "-e"])

(変更後)
    @pikzie.data("from02", ["prog", "-v", "-f", "201011241952", "-e"])

テストを実行します。

$ python test_log_analyzer_optparse.py 
.F..............

1) Failure: TestCommandLineParser.test_date_from (from02):
            self.assert_equal(opts.date_from, argv[2])
  data: ['prog', '-v', '-f', '201011241952', '-e', '/tmp/tmpeX2M2B']
test_log_analyzer_optparse.py:33: self.assert_equal(opts.date_from, argv[2])
expected: <'201011241952'>
 but was: <'-f'>

Finished in 0.039 seconds

16 test(s), 31 assertion(s), 1 failure(s), 0 error(s),
 0 pending(s), 0 omission(s), 0 notification(s)

どのテストの失敗して、原因が何なのかが "expected" と "but was" の diff が付いていて分かり易いです。またデータ駆動テストなので他の "from01" や "from03" のテスト実行には影響がありません。"from02" のテストだけエラーだと分かるので、"from02" で登録したデータに問題があるんじゃ、、、とデバッグし易いですね。

次に parse_option() 関数内では print でヘルプを出力しています。

...
        print parser.print_help()
...

テスト結果に支障はありませんが、StringIO モジュール 等を使うと不要な出力をターミナルへ表示させなくて済みます。

...
        sys.stdout, sys.stderr = StringIO(), StringIO()
...

さらにバージョン表示のテストにも応用できたりします。

...
    def test_version(self, argv):
        sys.argv = argv
        self.assert_raise_call(SystemExit, parse_option)
        self.assert_equal(sys.stdout.getvalue().split()[-1], VERSION)
...