これまでPythonスクリプトを組んできたものの、いずれもユニットテストは未実施。自分仕様ですから「そこまでしなくても」となりがち。でも、実際の現場ではテストは必須。潜在的なバグに気づけますからね。
実は昨今のテストって仕組みがややこしそう・・・と敬遠してましたが、それではいつまで経ってもレベルアップできませんのでユニットテストにチャレンジしました。
環境
- Python 3.8.2
- テストフレームワーク:unittest
-
テスト対象:md-generator
テストの準備
テストファイルの作成
まずはテストファイルを作ります。「test_ + テスト対象モジュール名.py」とするのが習わしなので、「test_gen_md_generator.py」となります。
コードはこんな感じになりました。
# test_gen_md_generator.py
""" md-generator のユニットテストケース """
import os
import unittest
import gen_md_file as md
TEST_ANS_DICT = {
"new_dir_name": "006",
"new_dir_path": "/Users/nnamm/Develop/MyProject/_test/006",
"created_date_long": "2020-04-29 15:00:00",
"created_date_short": "200429",
"eye_path": "/ec/blog/ec_blog_006.jpg",
"slug_str": "006-200429-",
"post_type": "blog",
}
TEST_ANS_LIST = ["006_200429.md", "img"]
class GeneratorMdTest(unittest.TestCase):
""" テストクラス """
def setUp(self) -> None:
pass
def tearDown(self) -> None:
pass
def test_create_front_matter_info(self):
""" フロントマター情報の確認 """
self.assertDictEqual(
md.create_front_matter_info("/Users/nnamm/Develop/MyProject/_test/"),
TEST_ANS_DICT,
)
def test_generate_blog_file(self):
""" ディレクトリとファイル生成の確認 """
# まずディレクトリとファイルを作成
md.generate_blog_file(TEST_ANS_DICT)
# ディレクトリとファイルが正しく作成されているか確認(詳細な中身は目視確認とする)
path = "/Users/nnamm/Develop/MyProject/_test/006/"
files_list = os.listdir(path)
files_list.sort()
self.assertListEqual(files_list, TEST_ANS_LIST)
テストコードの説明
import
「unittest」と「テスト対象とモジュール」をインポートして、クラスの中に「def test_XXXXXX」でテストケースを作っていきます。
TEST_ANS_DICT / TEST_ANS_LIST
assertで評価される値です。テストケースに定数を書いて良いものか標準的なルールを知らないため、そこらへんはご容赦を。
setUp() / tearDown()
各テストの実施前後にさせたい処理を書きます。たとえばprint("Test Start")、print("Test End")としてコンソールログを見やすくしたり、オブジェクトを破棄したりのお掃除系。
def test_create_front_matter_info(self)
Front matterを生成する処理でdictが返ります。テスト用のディレクトリを準備し、想定される結果(TEST_ANS_DICT)とassertDictEqualしています。
def test_generate_blog_file(self)
Front matterを含めたmdファイルを生成する処理です。とくにreturnされるものはなく、ディレクトリとファイルが生成されます。
さて、ここでは何を確認するべきか?
想定されたディレクトリ・ファイル構造になったかどうかを評価することにしました。そのためにos.listdir()したものをassertListEqualしています。
実際のファイルは開いて中身を目視確認すればいいでしょう。
テストの実行
テストコードをPyCharmで走らせてみます。
Edit Configurations
Edit Configurationsから設定します。
Add New Configurationからunittestsを選択したら、適当なNameをつけて対象スクリプトを選択してOKします。
Run
うまくいけば「Tests passed」が表示されます。
assertで想定した結果にならなかった場合、どこが間違っているか表示されるので修整もやりやすかったです。
やって良かったこと
潜在的なバグを見つけたことですね。
具体的には「create_front_matter_info」で「Python list index out of range」が起こる可能性があります。
この関数にはリストから最後の要素を取り出す処理があります。でも、そもそもリストがなければエラーとなります。僕の環境では起こらないので対処しなくてもいいけれど、せっかくなので該当箇所にtry 〜 catchをいれました。
「改修した」感ゲット(笑)
# gen_md_generator.py(対応前)
p = pathlib.Path(work_dir)
dir_list = [p.name for p in p.iterdir() if p.is_dir()]
dir_list.sort()
latest_dir = int(dir_list[-1]) ← ★ここ
fm_dict["new_dir_name"] = str(latest_dir + 1).zfill(3)
fm_dict["new_dir_path"] = work_dir + fm_dict["new_dir_name"]
# gen_md_generator.py(対応後)
p = pathlib.Path(work_dir)
dir_list = [p.name for p in p.iterdir() if p.is_dir()]
dir_list.sort()
try:
latest_dir = int(dir_list[-1])
fm_dict["new_dir_name"] = str(latest_dir + 1).zfill(3)
fm_dict["new_dir_path"] = work_dir + fm_dict["new_dir_name"]
except IndexError as err:
print(f"作成できません。対象ディレクトリを確認してください。:{err}")
不明点
datetimeでnow()を使って現在日時を取得している値の正確性を得るためには、テストケースとしてどうすれば良いかわかりませんでした。普通であれば絶対にイコールになることはないですから。
しゃーないので、テストのときだけテスト対象のモジュールに任意の日時を指定するロジックを加えて回避しました。
# gen_md_generator.py
# 新記事の作成日時(long: YYYY-MM-DD HH:MM:SS / short: YYMMDD)
dt = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# dt = datetime.datetime(2020, 4, 29, 15).strftime("%Y-%m-%d %H:%M:%S") # unittest用に日時指定
↑★ここ、テスト時だけコメントアウトする
※おそらくunittest.mockを使うのかも。今回は調査していませんので次の課題です。
おわりに
初歩の初歩とはいえユニットテストをやり切りました!
今回得たことは「テストしやすいコードはどうすれば組めるか?」という視点ですね。複雑な処理を重ねてやることもあるでしょうけど、ややこしい処理は分解してシンプルに組めばテストしやすくなるはず。
テストしやすいということは、コード品質を上げやすいということ。
だからといって、やたらと分解していいものでもないと思いますので、その塩梅は開発を経験し、センスが磨かれていくことで掴めていくものでしょう。センスはやればやるほど自然と磨かれているものだからね。
ユニットテスト、いい経験と気づきになりました。
※写真は大阪城公園をRICOH GRで撮影