Skip to content

Instantly share code, notes, and snippets.

@float1251
Last active August 29, 2015 14:01
Show Gist options
  • Save float1251/1b839a559a53dd74bbb7 to your computer and use it in GitHub Desktop.
Save float1251/1b839a559a53dd74bbb7 to your computer and use it in GitHub Desktop.

unittestのコードリーディングメモ

今回読んだのは3.3のunittestのソース。 ソースを読む前に以下のドキュメントは確認しておく。 とりあえず以下の4つの概念を知っておくだけで理解が早まると思う。

http://docs.python.jp/3.3/library/unittest.html

テストフィクスチャ
test fixture とは、テスト実行のために必要な準備や終了処理を指します。例: テスト用データベースの作成・ディレクトリ・サーバプロセスの起動など。
テストケース
test case はテストの独立した単位で、各入力に対する結果をチェックします。テストケースを作成する場合は、 unittest が提供する TestCase クラスを基底クラスとして利用することができます。
テストスイート
test suite はテストケースとテストスイートの集まりで、同時に実行しなければならないテストをまとめる場合に使用します。
テストランナー
test runner はテストの実行と結果表示を管理するコンポーネントです。ランナーはグラフィカルインターフェースでもテキストインターフェースでも良いですし、何も表示せずにテスト結果を示す値を返すだけの場合もあります。

構成

testsのディレクトリを除くと以下のような構成。

.
├── __init__.py
├── __main__.py
├── case.py
├── loader.py
├── main.py
├── mock.py
├── result.py
├── runner.py
├── signals.py
├── suite.py
└── util.py

__init__.py

help(unittest)したときに表示されるdocはここに書かれている。 TextTestResultというのがあるのは初めて知った。deprecatedと書かれているが。

__main__.py

python -m unittestとかでよばれたときの処理が書かれてるだけなので省略。

main.py

unittestのメインプログラム。 unittest.main()とやるので、何かしらのmainの関数があるのかと思ったが、ファイル末尾で

main = TestProgram

とやっているのでTestProgramクラスを初期化してる模様。

ほかにclassがあるのかと思ったが、これだけであとはprivateな関数だけだった。

しょっぱな__import__という使ったことのない組み込み関数に出くわしたので調べてみた。

http://docs.python.jp/3.3/library/functions.html#__import__

細かいことはわかっていないが、ひとまずmoduleをimportしている模様。

http://docs.python.jp/3.3/library/unittest.html#unittest.main

ここを見る限りだと、exit=Falseでsys.exit()を呼ばない、resultにテスト結果が保持されているようだ。

class TestProgram(object):
    """A command-line program that runs a set of tests; this is primarily
       for making test modules conveniently executable.
    """
    USAGE = USAGE_FROM_MODULE

    # defaults for testing
    failfast = catchbreak = buffer = progName = warnings = None

    def __init__(self, module='__main__', defaultTest=None, argv=None,
                    testRunner=None, testLoader=loader.defaultTestLoader,
                    exit=True, verbosity=1, failfast=None, catchbreak=None,
                    buffer=None, warnings=None):
        if isinstance(module, str):
            self.module = __import__(module)
            for part in module.split('.')[1:]:
                self.module = getattr(self.module, part)
        else:
            self.module = module
        if argv is None:
            argv = sys.argv

        self.exit = exit
        self.failfast = failfast
        self.catchbreak = catchbreak
        self.verbosity = verbosity
        self.buffer = buffer
        if warnings is None and not sys.warnoptions:
            # even if DreprecationWarnings are ignored by default
            # print them anyway unless other warnings settings are
            # specified by the warnings arg or the -W python flag
            self.warnings = 'default'
        else:
            # here self.warnings is set either to the value passed
            # to the warnings args or to None.
            # If the user didn't pass a value self.warnings will
            # be None. This means that the behavior is unchanged
            # and depends on the values passed to -W.
            self.warnings = warnings
        self.defaultTest = defaultTest
        self.testRunner = testRunner
        self.testLoader = testLoader
        self.progName = os.path.basename(argv[0])
        self.parseArgs(argv)
        self.runTests()

引数を設定していって、最後にrunTestでテストを実行している。

def runTests(self):
    if self.catchbreak:
        installHandler()
    if self.testRunner is None:
        self.testRunner = runner.TextTestRunner
    if isinstance(self.testRunner, type):
        try:
            testRunner = self.testRunner(verbosity=self.verbosity,
                                         failfast=self.failfast,
                                         buffer=self.buffer,
                                         warnings=self.warnings)
        except TypeError:
            # didn't accept the verbosity, buffer or failfast arguments
            testRunner = self.testRunner()
    else:
        # it is assumed to be a TestRunner instance
        testRunner = self.testRunner
    self.result = testRunner.run(self.test)
    if self.exit:
        sys.exit(not self.result.wasSuccessful())

catchbreakは同名のコマンドラインオプションと同じ効果。

コマンドラインオプションについては以下。

http://docs.python.jp/3.3/library/unittest.html#command-line-options

catchbreakがTrueだとinstallHandlerがnewされている。

installHandlerはsignalsの中の関数なので後ほど確認する。

軽く見たところsignalモジュールを使用して、<C-c>等をハンドルしてる模様。

signalについては以下

http://docs.python.jp/3.3/library/signal.html

デフォルトではTextTestRunnerを使用する。

isinstance(XXX, type)でXXXがクラスかインスタンスか判別できるっぽい。

class A:
    pass

isinstance(A, type)
> True
isinstance(A(), type)
> False

testRunnerのrunを実行してるが、self.testはどこで定義してたのか見逃した。。。

戻って見てみると、createTestsで設定していた、createTestsはparseArgs内で呼ばれていた。

とりあえずこの2つは以下のようになっている。

def parseArgs(self, argv):
    if ((len(argv) > 1 and argv[1].lower() == 'discover') or
        (len(argv) == 1 and self.module is None)):
        self._do_discovery(argv[2:])
        return

    parser = self._getOptParser()
    options, args = parser.parse_args(argv[1:])
    self._setAttributesFromOptions(options)

    if len(args) == 0 and self.module is None:
        # this allows "python -m unittest -v" to still work for
        # test discovery. This means -c / -b / -v / -f options will
        # be handled twice, which is harmless but not ideal.
        self._do_discovery(argv[1:])
        return

    if len(args) == 0 and self.defaultTest is None:
        # createTests will load tests from self.module
        self.testNames = None
    elif len(args) > 0:
        self.testNames = _convert_names(args)
        if __name__ == '__main__':
            # to support python -m unittest ...
            self.module = None
    else:
        self.testNames = (self.defaultTest,)
    self.createTests()

def createTests(self):
    if self.testNames is None:
        self.test = self.testLoader.loadTestsFromModule(self.module)
    else:
        self.test = self.testLoader.loadTestsFromNames(self.testNames,
                                                       self.module)

createTestsを見ると、testloaderがtestを返してるのがわかる。 おそらくテストスイートあたりの処理をloaderがやってると思うが詳細はあとにしよう。

あとはtestRunnerが実行して、sys.exitを呼んでいる。

テストランナーは最初の概念で書かれているとおり、テストの実行と、結果出力を担っている。

runner.py

from . import result
from .signals import registerResult

これ見ると、結果の表示か何かをresult.pyがやってるのかな。 ただ、registerResultとの違いがわからんな。

ひとまずTexTestRunnerを見ていこう。

class TextTestRunner(object):
    """A test runner class that displays results in textual form.

    It prints out the names of tests as they are run, errors as they
    occur, and a summary of the results at the end of the test run.
    """
    resultclass = TextTestResult

    def __init__(self, stream=None, descriptions=True, verbosity=1,
                 failfast=False, buffer=False, resultclass=None, warnings=None):
        if stream is None:
            stream = sys.stderr
        self.stream = _WritelnDecorator(stream)
        self.descriptions = descriptions
        self.verbosity = verbosity
        self.failfast = failfast
        self.buffer = buffer
        self.warnings = warnings
        if resultclass is not None:
            self.resultclass = resultclass

やはり結果表示か、保持は別のクラスでやってる。

streamはerror出力をどこで行うかなのかな。fileとか渡せばそちらに出力とかしてくれそうな気がする。

verbosity、failfast、bufferはコマンドラインのオプション通りの挙動な気がするが、 descriptionはテストの結果を詳細に表示するかどうかみたいな感じかな。

def getDescription(self, test):
    doc_first_line = test.shortDescription()
    if self.descriptions and doc_first_line:
        return '\n'.join((str(test), doc_first_line))
    else:
        return str(test)
def _makeResult(self):
    return self.resultclass(self.stream, self.descriptions, self.verbosity)

def run(self, test):
    "Run the given test case or test suite."
    result = self._makeResult()
    registerResult(result)
    result.failfast = self.failfast
    result.buffer = self.buffer
    with warnings.catch_warnings():
        if self.warnings:
            # if self.warnings is set, use it to filter all the warnings
            warnings.simplefilter(self.warnings)
            # if the filter is 'default' or 'always', special-case the
            # warnings from the deprecated unittest methods to show them
            # no more than once per module, because they can be fairly
            # noisy.  The -Wd and -Wa flags can be used to bypass this
            # only when self.warnings is None.
            if self.warnings in ['default', 'always']:
                warnings.filterwarnings('module',
                        category=DeprecationWarning,
                        message='Please use assert\w+ instead.')
        startTime = time.time()
        startTestRun = getattr(result, 'startTestRun', None)
        if startTestRun is not None:
            startTestRun()
        try:
            test(result)
        finally:
            stopTestRun = getattr(result, 'stopTestRun', None)
            if stopTestRun is not None:
                stopTestRun()
        stopTime = time.time()
    timeTaken = stopTime - startTime
    result.printErrors()
    if hasattr(result, 'separator2'):
        self.stream.writeln(result.separator2)
    run = result.testsRun
    self.stream.writeln("Ran %d test%s in %.3fs" %
                        (run, run != 1 and "s" or "", timeTaken))
    self.stream.writeln()

    expectedFails = unexpectedSuccesses = skipped = 0
    try:
        results = map(len, (result.expectedFailures,
                            result.unexpectedSuccesses,
                            result.skipped))
    except AttributeError:
        pass
    else:
        expectedFails, unexpectedSuccesses, skipped = results

    infos = []
    if not result.wasSuccessful():
        self.stream.write("FAILED")
        failed, errored = len(result.failures), len(result.errors)
        if failed:
            infos.append("failures=%d" % failed)
        if errored:
            infos.append("errors=%d" % errored)
    else:
        self.stream.write("OK")
    if skipped:
        infos.append("skipped=%d" % skipped)
    if expectedFails:
        infos.append("expected failures=%d" % expectedFails)
    if unexpectedSuccesses:
        infos.append("unexpected successes=%d" % unexpectedSuccesses)
    if infos:
        self.stream.writeln(" (%s)" % (", ".join(infos),))
    else:
        self.stream.write("\n")
    return result

runメソッドが長い。warningsというモジュールを使用している。

http://docs.python.jp/3.3/library/warnings.html#temporarily-suppressing-warnings

warningsのサンプルはこんな感じ.

import warnings

print("Before the warning")
warnings.warn("This is a warn message")
print("After the warning")

with warnings.catch_warnings():
    warnings.filterwarnings("always")
    warnings.warn("warn message")
    warnings.filterwarnings("ignore")
    warnings.warn("ignore message")

> Before the warning
> warn.py:4: UserWarning: This is a warn message
>   warnings.warn("This is a warn message")
> After the warning
> warn.py:9: UserWarning: warn message
>   warnings.warn("warn message")

testで発生したwarningを表示するかしないかを設定してるってこと。

getattrでメソッドを取得してるのは、resultにstartTestRunが実装されてない場合があるからなのかな。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment