ie_test
2018年7月22日日曜日
python unittest について
python unittest について # 目的 メンテしやすいモジュール作成のためにテストを考えてみた。 特にエラー処理のテストなど、どう書くかが結構手間なものについて実装しておきたいと思った。 # ディレクトリ構成 . ├── data_make_tools │ └── train_data_make.py └── tests ├ ── test_train_data_make.py └── data └── weather └── test1.csv # コード 以下のようなファイルからデータを読み込んで辞書を返すようなコードを考える。 data_make_tools/train_data_make.py ``` def get_ym_to_city_to_weather_data( fpath ): ym_to_city_to_weather_data = OrderedDict() with open( fpath, "r") as f: f.readline() # ヘッダ読み捨て. for i,line in enumerate(f): try: year, month, city, \ cloudness, daylight_hours, precipitation_sum, \ temperature_max, temperature_min, temperature \ = line.strip().split(",") ym = YearMonth( int(year),int(month) ) if ym not in ym_to_city_to_weather_data: ym_to_city_to_weather_data[ym] = OrderedDict() ym_to_city_to_weather_data[ym][city] \ = WeatherData( cloudness, daylight_hours, precipitation_sum, temperature_max, temperature_min, temperature ) except : print( traceback.format_exc() ) print( "data file: " + fpath + ", line: %d"%( i+1 ) ) print( "data error line: " + line ) exit(1) return ym_to_city_to_weather_data ``` エラーが生じたときにちゃんとメッセージを出力しているのかのテストを書きたいのだが、 このままでは難しい。 # コード変更 logging モジュールを使って以下のようにコードを変更 data_make_tools/train_data_make.py ``` import logging THIS_MODULE_LOGGER = logging.getLogger(__name__) def get_ym_to_city_to_weather_data( fpath ): ym_to_city_to_weather_data = OrderedDict() with open( fpath, "r") as f: f.readline() # ヘッダ読み捨て. for i,line in enumerate(f): try: year, month, city, \ cloudness, daylight_hours, precipitation_sum, \ temperature_max, temperature_min, temperature \ = line.strip().split(",") ym = YearMonth( int(year),int(month) ) if ym not in ym_to_city_to_weather_data: ym_to_city_to_weather_data[ym] = OrderedDict() ym_to_city_to_weather_data[ym][city] \ = WeatherData( cloudness, daylight_hours, precipitation_sum, temperature_max, temperature_min, temperature ) except : THIS_MODULE_LOGGER.error( traceback.format_exc() ) THIS_MODULE_LOGGER.error( "data file: " + fpath + ", line: %d"%( i+1 ) ) THIS_MODULE_LOGGER.error( "data error line: " + line ) exit(1) return ym_to_city_weather_data ``` このようにモジュール毎にログ管理をするように変更する。 # テストコード tests/test_train_data_make.py ``` import os from unittest import TestCase import logging from io import StringIO from data_make_tools.train_data_make import get_ym_to_city_to_weather_data LOGGER = logging.getLogger("data_make_tools.train_data_make") class Test_get_ym_to_city_to_weather_data(TestCase): data_dir = os.path.join( DATA_DIR, "weather") def setUp(self): self.stream = StringIO() self.handler = logging.StreamHandler( self.stream ) LOGGER.addHandler( self.handler ) def tearDown(self): LOGGER.removeHandler( self.handler ) self.handler.close() def test_weather_1(self): fpath = os.path.join( self.data_dir, "test1.csv" ) self.assertRaises( SystemExit, lambda: get_ym_to_city_to_weather_data( fpath ) ) self.assertIn( "not enough values to unpack (expected 9, got 8)", self.stream.getvalue() ) ``` # テストコード説明 以下でログを流し込むオブジェクトを確保し、 data_make_tools.train_data_make の logger に登録. これはテスト実行前に必要なので setUp で行う。 ``` self.stream = StringIO() self.handler = logging.StreamHandler( self.stream ) LOGGER.addHandler( self.handler ) ``` 以下でテストする関数の実行と想定している例外が上がっているかをチェック ``` self.assertRaises( SystemExit, lambda: get_ym_to_city_to_weather_data( fpath ) ) ``` 以下で想定している文字列が出力されているかチェック. ``` self.assertIn( "not enough values to unpack (expected 9, got 8)", self.stream.getvalue() ) ``` 実行が終わったら tearDown によりオブジェクトを取り除き、元の状態にする。 ``` LOGGER.removeHandler( self.handler ) self.handler.close() ``` # テスト実行 ディレクトリのトップで以下のコマンドを実行. ``` python -m unittest discover tests ``` 上記は tests ディレクトリにある unittest のテストを全て見つけて、実行するというコマンドになっている。 python の unittest のカバレッジをとる. # カバレッジ カバレッジを見ることでテスト書くモチベーションになるんではないかと思って やってみた。 以下のテストについてカバレッジを計測したい。ディレクトリ構造なども以下と同様。 ## ツール準備 以下で coverage モジュールをインストール. ``` pip install coverage ``` ## coverage を使うために coverage を使うために以下のコードを用意. test.py ``` import unittest def exec_test_all(): all_tests = unittest.TestLoader().discover( "tests", "test_*.py") runner = unittest.TextTestRunner() runner.run( all_tests ) if __name__ == "__main__": exec_test_all() ``` 上記のコードは tests ディレクトリ以下の test_ で始まる python コードをテストコードとして 取って来るというコード. ## 実行方法 以下のコマンドでテスト実行. ``` coverage run test.py ``` 実行するとカレントディレクトリに .coverage というファイルが作られる。 (このファイルに実行結果が書き込まれる。) 以下のコマンドでカバレッジを可視化した html ファイルを生成. ``` coverage html --include=data_make_tools/* ``` --include=data_make_tools/* は data_make_tools 以下のコードに対してのみ カバレッジ結果を生成するという意味。 (こうしないとテストスクリプトの方のカバレッジも含んでしまう。) 実行すると htmlcov というディレクトリに html の表示のためのファイルが生成される。 htmlcov/index.html をブラウザで開いて結果を見る。
0 件のコメント:
コメントを投稿
次の投稿
前の投稿
ホーム
登録:
コメントの投稿 (Atom)
0 件のコメント:
コメントを投稿