環境

引き続き, 以下のような環境でやってます.

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.6
BuildVersion:   15G19009

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.4 LTS"

$ python --version
Python 3.6.4

前回の記事の続きです.

環境 知見 (1) ランダムな情報を返す関数をテストする 関数 テスト 知見 (2) とある日付の前月を返す関数をテストする 関数 テスト 知見 (3) テスト結果を XML 形式で出力する メモ 以上 環境 以下のような環境でやってます. $ sw_vers ProductName: Mac OS X Produc...

inokara.hateblo.jp

知見 (4) テストの実行順序を制御する (ということ自体が正しいのか否かについても)

疑問

以下のようなテストコードがあったとします.

import unittest

class SampleTest(unittest.TestCase):
   def test_sample_one(self):
       self.assertTrue(True)

   def test_sample_two(self):
       self.assertTrue(True)

   def test_sample_three(self):
       self.assertTrue(True)

この場合, 上から test_sample_one → test_sample_two → test_sample_three という順番でテストメソッドが実行されるのか…きっと, 順番どおりに上から実行されるんだろうなあと思い, 確認してみましたが… 以下のように想定とは異なる結果になりました.

$ python -m unittest tests.test_sample -v
test_sample_one (tests.test_sample.SampleTest) ... ok
test_sample_three (tests.test_sample.SampleTest) ... ok
test_sample_two (tests.test_sample.SampleTest) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

なんで?

ドキュメント には, 以下のように書かれていました.

注釈 いろいろなテストが実行される順序は、文字列の組み込みの順序でテストメソッド名をソートすることで決まります。

なるほど, なるほど.

少し深追い

テストメソッドがどのようにソートされているのかを unittest 自体のコードで確認してみたいと思います.

テストメソッドの名前を取得する関数は unittest/loader.py の以下の関数 getTestCaseNames() で行われているようです.

 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

この関数内の sortTestMethodsUsing がどうやら, 実際にテストメソッド名をソートしている関数のようです. ドキュメントにも以下のように書かれています.

getTestCaseNames() および全ての loadTestsFrom*() メソッドでメソッド名をソートする際に使用する比較関数。

さらに, この sortTestMethodsUsing がどんなことをしているのか, これは unittest/loader.py 内の TestLoader() クラスの冒頭部分に以下のように書かれています.

class TestLoader(object):
    """
    This class is responsible for loading tests according to various criteria
    and returning them wrapped in a TestSuite
    """
    testMethodPrefix = 'test'
    sortTestMethodsUsing = staticmethod(util.three_way_cmp)
    suiteClass = suite.TestSuite
    _top_level_dir = None
...

ほう, unittest/util.py の three_way_cmp() という関数が何かをやってくれちゃっているようです.

も少しだけ深く

unittest/util.py の three_way_cmp() はどのようなことをやっているのか.

def three_way_cmp(x, y):
    """Return -1 if x < 0 if x == y and 1 if x > y"""
    return (x > y) - (x < y)

実にシンプルですな.

コメントにも書かれていますが, 以下のように引数を比較して -1 0 1 を返すだけのシンプルな関数でした.

  • 引数 x より 引数 y が大きければ -1 を返す
  • 引数 x と 引数 y が同じであれば 0 を返す
  • 引数 x より 引数 y が小さければ 1 を返す

これは簡単に手元でも試すことが出来ます.

>>> def three_way_cmp(x, y):
...    return (x > y) - (x < y)
... 
>>> three_way_cmp(1, 0)
1
>>> three_way_cmp(1, 1)
0
>>> three_way_cmp(2, 5)
-1
>>> three_way_cmp('apple', 'box')
-1
>>> three_way_cmp('ibm', 'apple')
1
>>> three_way_cmp('aws', 'azure')
-1
>>> three_way_cmp('test_sample_one', 'test_sample_two')
-1
>>> three_way_cmp('test_sample_two', 'test_sample_three')
1
>>> three_way_cmp('test_sample_one', 'test_sample_three')
-1
>>> methods = ["test_sample_one", "test_sample_two", "test_sample_three"]
>>> import functools
>>> sorted(methods, key=functools.cmp_to_key(three_way_cmp))
['test_sample_one', 'test_sample_three', 'test_sample_two']

テストメソッド名を上記の関数で比較してソートしていたわけですな.

スッキリ

で…

なぜ, あえてテストメソッド名をソートする必要があるのか. これについて, 明確に言及している文献が無いかをざっと探してみたけど, 見つけることが出来なかったので, Stackoverflow 等で識者の方が書かれているコメントを引用しつつ考えてみたいと思います.

How do I be sure of the unittest methods order? Is the alphabetical or numeric prefixes the proper way?class TestFoo(TestCase): def test_1(self): ... def test_2(self): ......

stackoverflow.com

I half agree with the idea that tests souldn’t be ordered. In some cases it helps (it’s easier damn it!) to have them in order… after all that’s the reason for the ‘unit’ in UnitTest.

  • ユニットテストユニット という言葉の意味「単位」とか「単元」
  • ユニットテストでは個々の機能 (ここでは各関数) をテストするので, そもそも順番を求めるのはちょっと違う気がする (半分は同意するけどねー)

Why do you need specific test order? The tests should be isolated and therefore it should be possible to run them in any order, or even in parallel.

  • 少し口調が強めかなー
  • なんでユニットテストに順番が必要なんですか, テストは分離されている必要があり, 任意の順番で並行して実行されるべきですよね

ナルホド

なぜ, テストメソッド名はソートされているのは, 以下の理由からだと考えました.

  • そもそもユニットテスト (単体テスト) は任意の順番で実行され, 並行で実行される必要がある
  • 順番を制御するコードがテストコードに入ってしまうことで, 何をテストすべきなのかの見通しが悪くなってしまう懸念
  • 順番に沿った挙動を確認する == プログラム全体の挙動を確認する == 結合テストになっちゃう

いかがでしょうか.

と, まあ

ユニットテストに実行順番を求めるのはナンセンスなのかな…と思いつつも, ググると上記に掲載した Stackoverflow の記事のように, 実際に順番を求めている人たちはいらっしゃるようですし (自分もその一人) , pytest には以下のような順番を定義するモジュールも提供されてるので, 用法用量は守ってテストに勤しみたいと思います.

pytest-ordering - pytest plugin to run your tests in a specific order

github.com

以上

知見でした.

元記事はこちら

直近で Python の unittest で試行錯誤していて得られた知見の幾つか (2)