Python(3.6.3)でunittestを利用したとき,どのようにスクリプト内のunittest.TestCaseを継承したクラスを認識しているのか気になりました。この記事は,処理が行われている場所を特定するまでの流れを記録したものです。
動機
公式ドキュメントのとおり,unittestでは,テストケースを作成する際にunittest.TestCaseを継承したクラスを作成します。そのクラスの中にテスト用のメソッドを記述することで,そのメソッドは,unittest.mainを実行したときにテストの対象とみなされます。このとき下記の2つが直感的に理解できませんでした。
- ファイル内から継承したクラスを取り出す方法
- 継承したクラスからテスト用メソッドを取り出す方法
まさかファイルを1行ずつ文字列検索して解析するとは思えず…この処理を理解するため,unittestパッケージを読むことにしました。
ソースコードを読む前に
unittestパッケージのソースコードを読み始める前に,処理の概要を再確認します。この時点で細かいオプションを気にする必要ありません。ドキュメントにあるサンプルコードを実行しておくと処理のイメージがつかめます。
まずは全体の流れをつかむ
ある程度の規模のソースコードを読む場合は,細部を見る前に全体の流れをつかむことが重要です。全体の流れをつかむために以下の読み方をお勧めします。
コマンドオプションなどの処理は読まない
unittestのような汎用的なライブラリには多くのオプションが備わっており,引数やコマンドラインオプションによって挙動を変化させることが可能です。これらの処理を細かく追っていくと全体の流れを把握する前に息切れしてしまいます。まずはサンプルコードのように,オプションが全くない状態で実行した場合の処理を前提に読み進めると良いです。
メソッドの最終行に注目する
最初にエントリポイントとなるメソッドから読み始めます。そのメソッドを見つけたら,そのメソッドの最終行に注目します。オブジェクト指向のプログラミング言語では,メソッドの前半に実行に必要な変数などの下処理を行い,後半に実際の実行処理を行うケースが多いためです。
実際のソースコードを確認していきます。unittest.main()は,main.pyファイルに定義されており,main.py内で"main = TestProgram"との記述があるため,TestProgramクラスをインスタンス化している処理であることがわかります。そして,TestProgramの"__init__"メソッドの最終行を確認すると,self.runTests()が呼ばれていることがわかります。そのrunTestメソッドの最終行(の1つ前)には,self.result = testRunner.run(self.test)があります(実際の最終行は,sys.exitの処理ですが,明らかに終了のための後処理なのでこちらは無視します)。次に,testRunnerの実体であるrunner.TextTestRunnerクラスのrunメソッドを見ると,最終行がreturn文になっていることがわかります。このことから,メソッド呼び出しによる処理の流れの終端は,runner.TextTestRunnerクラスのrunメソッドであることがわかります。まとめると下記の順番で処理が実行されることがわかりました。
- unittest.main ※実体は"TestProgram.__init__"
- TestProgram.runTests
- runner.TextTestRunner.run
処理の中心となるオブジェクトを探す
全体の流れを把握できたら,各メソッドを下から順に見ていきます。下から見ていく理由は,メソッドと同様に処理の前半はオプションなどのデータ整理,後半に整理したデータを利用して処理が行われていく傾向があるためです。
画面上に表示されるメッセージに着目
上記の3番目にあるrunner.TextTestRunner.runを見ていきます。ここで注目すべきなのは,self.stream.writelnメソッドの引数に"Ran %d test%..."という記述があることです。サンプルコードを実行すると,この文言が画面上に表示されることから,ここより上でテストの処理が実行されていることがわかります。そして,そのすぐ上にstartTestRunとstopTestRunという処理があり,その間にtestというオブジェクトがコールされています。ここまでの調査からtestオブジェクトが処理の中心となるオブジェクトである可能性が高いです。
中心となるオブジェクトの生成元を探す
testオブジェクトがどこで生成されているかrunner.TextTestRunner.runを起点として探していきます。testオブジェクトは,呼び出し元であるrunTestsから引数で渡されていることがわかります。main.pyに戻り,testRunner.run(self.test)メソッドを確認すると,メソッド内でself.testを代入している処理が無いことがわかります。このため探索範囲をmain.pyファイル全体に広げて,代入している箇所を探します。
pdb(Pythonデバッガ)の利用
"self.test ="でmain.pyファイル内を検索すると3つ見つかります。それぞれの場所から呼び出し元をたどるよりも,サンプルコードをデバッガで実行してみて,3つの内どこの処理を通過するかを確認するほうが効率が良いです。pdbを利用して,3つの場所にブレークポイントをセットした結果,main.pyファイルの145行目を通ることがわかりました。さらにスタックトレースをみると,self.argParseメソッドからコールされていることがわかります。
中心となるオブジェクトの生成
145行目は,"self.test = self.testLoader.loadTestFromModule(self.module)"となっています。self.testLoaderには,loader.defaultTestloaderが代入されるため,loader.pyファイル内にあるtestLoaderクラスのloadTestFromModuleメソッドを確認します。
loadTestFromModuleメソッドの最終行を見ると,戻り値がtestsになっており,その上のコードで"tests = self.suiteClass(tests)",さらにその上では,tests = [] および tests.append(...)というコードがあります。
生成処理の詳細
ここで行われている処理をpdbを利用して詳しく見ていきます。初期値と推測されるtests = []の下には以下のコードが存在します。
tests = []
for name in dir(module):
obj = getattr(module, name)
if isinstance(obj, type) and issubclass(obj, case.TestCase):
tests.append(self.loadTestsFromTestCase(obj))
まず,dir(module)から要素を取り出しています。サンプルコードの場合,moduleにはサンプルコードのファイルがモジュールとして格納されるため,moduleの属性名がnameにセットされます。この後,getattrメソッドを利用してnameの実体がobjにセットされます。次のif文でobjの型チェックが行われ,パスしたオブジェクトはtests.appendメソッドによってtestsに追加されていきます。
実はここに最初に示した疑問の答えの1つがあります。nameにはmodule内の属性名がセットされます。モジュールの属性にはモジュール内で定義したクラスが含まれるため,テストケースとして定義したクラスの名前がnameにセットされ,その名前を持つモジュール内のオブジェクトの型が,クラスであり(isinstance関数でチェック),TestCaseクラスを継承している(issubclass関数でチェック)場合は,testsに追加されます。(2つ目の疑問の答えとなる処理は,self.loadTestsFromTestCaseメソッド内にありますが,長くなってしまったため割愛します)
さいごに
この記事では,ライブラリの理解できない処理をソースコードから解析する1つの例を紹介しました。解析のコツは始めに理全体の流れを把握することです。次に中心的な働きをするオブジェクトに注目して,そのオブジェクトが生成される処理を丁寧に確認していくと,ソースコードの全貌が見え始め,気になる処理を探す下地ができます。今回のケースでは,あるオブジェクトに注目する過程で答えを見つけましたが,そうでないケースではこの下地が活かされると思います。