python3
ソースコードリーディング

Pythonのunittestパッケージをコードリーディング(その2)

前回の記事で扱ったとおり,unittestがどのようにファイル内から継承したクラスを取り出すのかを確認しました。今回は,前回割愛した"継承したクラスからテスト用メソッドを取り出す方法"を確認したいと思います。

前回のおさらい

unittest/loader.pyの119行目以降にあるコードによりunittestはファイル(モジュール)に定義されたcase.TestCaseを継承するクラスを見つけ,testsリストに追加(append)されます。その際,抽出されたクラスにself.loadTestsFromTestCaseメソッドが適用され,その結果がtestsに追加されます。

unittest/loader.py
tests = []
for name in dir(module):
    obj = getattr(module, name)
    if isinstance(obj, type) and issubclass(obj, case.TestCase):
        tests.append(self.loadTestsFromTestCase(obj))

loadTestsFromTestCaseメソッド

上記のように,loadTestsFromTestCaseメソッドが抽出されたクラスをもとに戻り値をappendメソッドにに渡しているため,こちらのメソッド(unittest/loader.pyの83行目以降)の中身を見ていきます。

unittest/loader.py
    def loadTestsFromTestCase(self, testCaseClass):
        """Return a suite of all test cases contained in testCaseClass"""
        if issubclass(testCaseClass, suite.TestSuite):
            raise TypeError("Test cases should not be derived from "
                            "TestSuite. Maybe you meant to derive from "
                            "TestCase?")
        testCaseNames = self.getTestCaseNames(testCaseClass)
        if not testCaseNames and hasattr(testCaseClass, 'runTest'):
            testCaseNames = ['runTest']
        loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames))
        return loaded_suite

メソッド内のコメント,メソッド名,変数名などから推測できるように,中段にあるgetTestCaseNamestestCaseClassクラス内に定義されたテスト用メソッドを抽出しているようです。

getTestCaseNamesメソッド

getTestCaseNamesメソッドは,unittest/loader.pyの222行目以降にあります。

unittest/loader.py
    def getTestCaseNames(self, testCaseClass):
        """Return a sorted sequence of method names found within testCaseClass
        """
        def isTestMethod(attrname, testCaseClass=testCaseClass,
                         prefix=self.testMethodPrefix):
            return attrname.startswith(prefix) and \
                callable(getattr(testCaseClass, attrname))
        testFnNames = list(filter(isTestMethod, dir(testCaseClass)))
        if self.sortTestMethodsUsing:
            testFnNames.sort(key=functools.cmp_to_key(self.sortTestMethodsUsing))
        return testFnNames

コメントにそのまま記述されているとおり,引数として受け取ったtestCaseClassを継承したクラス内からテスト用に定義したメソッドを抽出することが,このメソッドの目的のようです。ここで2つのことに目が行きます。1つは前回の処理と同様,対象候補を抽出するためにdirを利用していることです。前回は,ファイル(モジュール)内のクラスを抽出する方法としてdirを利用していましたが,これと似た方法として,クラスからメソッドを抽出する際にもdirを利用しています。もう1つは,メソッド内に定義されたisTestMethodメソッドです。filter関数の第1引数として利用されていることからも分かるとおり,このメソッドは判定に使用され,その判定条件は以下になります。

  • dirで取得した属性名の先頭が"test"で始まるかどうか
  • その属性名はコール可能か(すなわちメソッドかどうか)

上記の1つ目は,isTestMethod内でstartswith(prefix)を利用していることからわかります。2つ目は,getattrを利用して属性名からオブジェクトを取り出し,callableによって呼び出し可能かどうかを判定していることからわかります。

まとめ

以上のように,ファイル(モジュール)から特定のクラスを抽出するケースやクラス内の特定のメソッドを抽出するケースでは,以下の流れで進めることが有効であることがわかりました。

  1. dir関数でそのモジュール(クラス)の属性名のリストを取得する
  2. getattr関数で属性名からオブジェクトを取得し,そのオブジェクトが持つ性質を判定できるようにする
  3. if文やfilter関数を利用して,条件にマッチする属性名を抽出する

個人的には,getattr関数をどの場面で場面で利用すればよいのかよくわかっていなかったため,上記のように,"dirで取得した属性名が抽出したい対象かどうかを判定するため,その属性名のオブジェクトを取得する"場面で利用できることがわかったのは大きな収穫でした。