TL;DR
TextTestRunner を上手に使おう!
はじめに
みなさん、ユニットテスト書いてますか?
この記事が目に留まったあなたは、少なくとも ユニットテストに興味があり、Pythonの unittest モジュール について調べたことがある のではないでしょうか。
この unittest モジュールは、非常に強力である一方、最小限の労力で利用することもできるスグレモノです。
この後者の「最小限の労力で利用することもできる」というのが曲者で、一度ラクな手順を覚えてしまったが最後、本来備わっているポテンシャルを引き出すことなく、惰性でラクな手順を続けてしまう……ということがまれによくあります。
タイトルにも書いた unittest.main() はその代表とも言え、この1ステップをスクリプトの末尾に書くだけで、スクリプト実行時に同ファイル内のテストコードを走査してくれるシロモノです。
そんなエンジニアをダメにする unittest.main() ですが、引数をよしなに渡すだけで、テスト結果をファイルに出力したり、出力する内容の詳細度を変更したり、より便利にカスタマイズが可能です。
詳解は、公式ドキュメントに譲ることとして、ここでは具体的な実装例を示します。
検証環境
- Windows10 Pro 64bit
- Python 3.6.0 | Anaconda 4.3.0 (64-bit)
サンプルコード&実行結果
出力される内容の確認のため、あえてERRORとなるようなテストを書いています(TestArithmetic.test_div)。
# coding=utf-8
import enum
import pathlib
import unittest
from datetime import datetime
class TestArithmetic(unittest.TestCase):
"""Test of arithmetic."""
def test_add(self):
"""Test of addition."""
self.assertEqual(1 + 2, 3)
def test_div(self):
"""Test of division."""
# error!
self.assertEqual(1 / 0, 1)
def _main():
"""The main process at script execution."""
@enum.unique
class Verbosity(enum.IntEnum):
NOTHING = 0
SIMPLE = 1
DETAIL = 2
CURRENT_PATH = pathlib.Path(__file__)
EXEC_DATETIME = datetime.today()
FILE_MOD_DATETIME = datetime.fromtimestamp(CURRENT_PATH.stat().st_mtime)
LOG_PATH = CURRENT_PATH.with_name(
CURRENT_PATH.stem + f'_{EXEC_DATETIME.date()}.log')
with pathlib.Path(LOG_PATH).open('w') as fw:
fw.write(
f'[Date of script modification] {str(FILE_MOD_DATETIME)}\n'
f'[Date of this test execution] {str(EXEC_DATETIME)}\n'
'\n')
unittest.main(
testRunner=unittest.TextTestRunner(
stream=fw,
descriptions=False,
verbosity=Verbosity.DETAIL))
if __name__ == '__main__':
_main()
出力されるログファイルはこちら:
[Date of script modification] 2018-06-25 07:52:19.137042
[Date of this test execution] 2018-06-25 07:53:06.383679
test_add (__main__.TestArithmetic) ... ok
test_div (__main__.TestArithmetic) ... ERROR
======================================================================
ERROR: test_div (__main__.TestArithmetic)
----------------------------------------------------------------------
Traceback (most recent call last):
File "sample.py", line 20, in test_div
self.assertEqual(1 / 0, 1)
ZeroDivisionError: division by zero
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (errors=1)
解説
このサンプルコードのうち、本題にとって重要な部分は、以下の1ステップに凝縮されています。
unittest.main(
testRunner=unittest.TextTestRunner(
stream=fw,
descriptions=False,
verbosity=Verbosity.DETAIL))
この5行のコードについて、1行ずつ見ていきます。
1行目は、言わずとしれた unittest.main() の呼び出しです。ここは説明不要でしょう。
2行目は、testRunner引数の指定をしています。ここでは、標準モジュールの unittest.TextTestRunnerを指定しています。ここで指定する、TextTestRunnerクラスの設定に依って、unittest.main()の挙動が変わります。
3行目は、テスト結果の出力先(出力ストリーム)を指定しています。デフォルトでは sys.stderr が出力先となります。
4行目は、テスト結果として、テストの説明(テストメソッドのdocstringの1行目)を出力するか否かを指定します。
5行目は、出力する内容の詳細度を3段階(0~2)で指定します。
記事を書きながら調べている最中に知ったのですが、ここで指定する3つの引数は、 unittest.TextTestResult をインスタンス化する際に利用されるようです。
だからどう、ということがあるのかないのか…… それは君の目で確かめてみてほしい。
あとがき
私自身は、Visual Stduio Codeのスニペットに def _main():
以下を登録して、使いまわしています。
今回は、最も単純な、単一ファイルのユニットテストが対象でしたが、複数ファイルを対象にする場合の記事も、いずれ書きたいと思います。