Pythonのテストで悩むあなたへ送るnoseフレームワーク
前回はデコレータを使ってコードのインデントを浅くし、ボイラープレートとなっていた繰り返し処理をひとつにまとめました。しかしその際、「挙動が変わってしまったらどうしよう」と怯えながら書き換えていたのも事実です。
今回はユニットテストという命綱を作ることで実装に専念できる環境を整え、また中途半端なリファクタリングもどきによってバラバラになった感のある仕様をまとめてみます。
教科書はいつものスマパイです。
スマートPythonプログラミング: Pythonのより良い書き方を学ぶ
- 作者: もみじあめ
- 発売日: 2016/03/12
- メディア: Kindle版
- この商品を含むブログ (1件) を見る
テスト方法
シンプルかつ多機能なテストフレームワークnose
を使います。コードカバレッジを計測するcoverage
パッケージとの連携も可能なので、こちらも使います。doctest
にも対応しているので、やはり使います。
pip install nose coverage
nose
Pythonには標準でユニットテストモジュールが付属していて、assert
文を使ったテストを書くことができます。慣れていればわかりやすいのですが、私は慣れていないので書きにくいです。もともとRubyのrspec
とguard
を組み合わせてテストを書いていたので、「DSLとまではいかなくても簡潔に書けるものがいいな」と思ったら、なんだかnose
が良さそうだったのです。
assertEqual(result, True)
のようなテストを
from nose import ok_
ok_(result)
と書くことができます。rspec
はちょっと煩雑だったかなと思う自分にとって、現在のベストを求めるとしたらnose
になるかも、というくらい好きです。(rspec
が煩雑である、という言い方は語弊があるので補足します。「ちゃんと勉強し続けないとrspec
が書けなくなる」です)
コードカバレッジ
コードカバレッジとは、テストコードが実装の何割をチェックしているか、というひとつの指標です。循環的複雑度と同じで、100%にしたからといってバグがなくなるわけではありません。とはいえ、「テストされているか・されていないか」では大きな違いが出てくるので、90%以上を目安にすればいいかな、と思います。
カバレッジを100%に保つのは非常に骨が折れる作業で、「テストばっかり書いて実装を書いてない」という状況に陥りがちです。もしくは、「実装を変更するとテストがエラーだらけになる」というのも、実装とテストが密結合になりすぎていてメンテナンス性が低下しています。
要はバランスが肝心なわけです。
doctest
バランスやメンテナンス性といえば、少々強引な流れですがdocstring
があります。Pythonのコード内でhelp(func)
と呼び出すとfunc
のdocstring
が表示されるというもので、これを利用して別途ドキュメントを用意しなくても、docstring
のみでドキュメントを生成することもできます。
doctest
は、ずばりdocstring
の中にテストを埋め込むものです。
def fizzbuzz(n): """ FizzBuzz 実装した関数。 引数の整数 n に応じて次のような値を返す。 - 3 で割り切れる: 'Fizz' - 5 で割り切れる: 'Buzz' - 3 と 5 の両方で割り切れる: 'FizzBuzz' - いずれの値でも割り切れない: nの文字列表現 >>> from fizzbuzz import fizzbuzz >>> fizzbuzz(2) '2' >>> fizzbuzz(3) 'Fizz' >>> fizzbuzz(5) 'Buzz' >>> fizzbuzz(15) 'FizzBuzz' """
>>>
がPythonのプロンプト、続くコードがテスト対象、次の行が想定される値です。doctest
は最近のPythonなら組み込みモジュールとしてインストールされているので、お手軽にTDD開発したい方におすすめです。
.nosercを用意する
nose
をインストールするとnosetests
というコマンドが使えるようになります。coverage
やdoctest
と連携させたいときは--with-coverage
などの引数を渡す必要があるのですが、実行するたびに渡さなければならないのはちょっと面倒です。そこで.noserc
という設定ファイルを用意して、そこに記述することにしましょう。
[nosetests]
with-doctest=1
with-coverage=1
cover-package=dictionary
cover-html=1
verbosity=2
引数のプレフィクス--
を取り除いた形で、[nosetests]
セクションに記述します。一時的に無効にしたい場合は、行頭に#
を付けることでコメントと見なされます。
doctest
との連携を有効coverage
との連携を有効coverage
でカバレッジを計測するのはdictionary
モジュールのみcoverage
の出力形式はHTML- テスト実行メッセージのレベルは2
を指定しています。verbosity
のデフォルト値が2なのでわざわざ指定する必要もないんですが、将来的に3にしたくなったときのために記述するだけしておきます。
また、他のモジュールのテストが追加されたときは、cover-package=unmo, dictionary
とカンマ区切りで増やしていけます。
呼び出すときはnosetests -c .noserc
というように、設定ファイルとして.noserc
を参照するように指定します。
dictionary.py
をテストする
すでにUnmo
モジュールは充分巨大になっています。戻り値にランダム性があったり、ファイルへの書き込みがあったり。とりあえずの対象として、Dictionary
のスタティックメソッド2つに的を絞りましょう。pattern_to_line
とmake_pattern
です。
@staticmethod def pattern_to_line(pattern): """ パターンのハッシュを文字列に変換する。 >>> pattern = {'pattern': 'Pattern', 'phrases': ['phrases', 'list']} >>> Dictionary.pattern_to_line(pattern) 'Pattern\\tphrases|list' """ return '{}\t{}'.format(pattern['pattern'], '|'.join(pattern['phrases'])) @staticmethod def make_pattern(line): """ 文字列lineを\tで分割し、{'pattern': [0], 'phrases': [1]}の形式で返す。 [1]はさらに`|`で分割し、文字列のリストとする。 >>> line = 'Pattern\\tphrases|list' >>> Dictionary.make_pattern(line) {'pattern': 'Pattern', 'phrases': ['phrases', 'list']} """ pattern, phrases = line.split('\t') if pattern and phrases: return {'pattern': pattern, 'phrases': phrases.split('|')}
メソッド名からしてすでに不適切な気がするのはひとまず置いておいて、doctest
を追加してみました。docstring
内ではエスケープシーケンスに気を配る必要があります。\t
と記述してしまうとdocstring
が解釈された時点で空白文字列へ変換されてしまうので、戻り値のチェックをするには\\t
とエスケープしてやらなければなりません。
エスケープされないようにダブルクォートではなくバッククォートで括る方法もありますが、現在は非推奨になっているみたいです。この辺はdoctest
の情報を調べてみる必要がありそうです。
次は肝心のtest_dictionary.py
です。
from nose.tools import eq_ from dictionary import Dictionary class TestDictionary: TEST_PATTERN = {'pattern': {'pattern': 'Test', 'phrases': ['This', 'is', 'test', 'phrases']}, 'line': 'Test\tThis|is|test|phrases', } def test_pattern_to_line(self): test_dict = TestDictionary.TEST_PATTERN['pattern'] test_result = TestDictionary.TEST_PATTERN['line'] eq_(Dictionary.pattern_to_line(test_dict), test_result) def test_make_pattern(self): test_line = TestDictionary.TEST_PATTERN['line'] test_result = TestDictionary.TEST_PATTERN['pattern'] eq_(Dictionary.make_pattern(test_line), test_result)
勉強不足による「テストケースそのものが読みづらい」という構造的欠陥を抱えていますが、こちらもまた情報を調べていくうちに改善していきます。unittest
ではなくnose
を使用しているので、TestDictionary
クラスはunittest.TestCase
クラスを継承する必要はありません。多分。
テストの実行
では実行してみましょう。
D:\Users\sandmark\repos\unmo>nosetests -c .noserc
Doctest: dictionary.Dictionary.make_pattern ... ok
Doctest: dictionary.Dictionary.pattern_to_line ... ok
test_dictionary.TestDictionary.test_make_pattern ... ok
test_dictionary.TestDictionary.test_pattern_to_line ... ok
Name Stmts Miss Cover
-----------------------------------
dictionary.py 114 74 35%
----------------------------------------------------------------------
Ran 4 tests in 1.284s
OK
テストは全部通りました。小規模なテストなので当然ですね。指定したモジュールdictionary.py
のカバレッジは35%になっています。多いように見えますが、def
やclass
などの宣言はPython処理系によってテストされていると見なされるため、底上げされています。ここからカバレッジを増やすのが難しいのです。
実際にどのコードがテストされたかを見るには、nosetests
(と連携しているcoverage
)が自動的に生成したcover
ディレクトリのindex.html
を開きます。繰り返しコードを見ることでリファクタリングのヒントが掴めるかもしれません。
まとめ
unittest
, coverage
, doctest
……nose
が連携できるプラグインは他にもたくさんあります。また、プラグインを自作してよりスマートにテストを行うこともできます。
日本語のドキュメントが少ないのが非常に残念ですが、引き続きnose
によるテストは追加していくので、何らかのリソースになれば幸いです。