今回のお題
今回はpythonのunittest
についてまとめます。
unittest
とはpythonの標準モジュールの一つであり、ここにはテストコードの実行に必要なクラスや関数がまとめられています。
すなわちこのモジュールをimportすることで手軽にテストコードの実装が可能になるというわけです。
本記事で扱うこと
- unittestの基本的な仕組み
- unittestの実行方法
- unittestの結果の見方
本記事で扱わないこと
- DOM操作関連
- パラメータテスト
- テストで用いるDBの設定方法
アプリケーションにテストを実装するとなるとDBの設定などでややこしくなるので、まずはテストのスクリプト単体で動作させることを目指します。
また、パラメータテストについてはパッケージのインストールが必要なので別で取り上げます。
unittestの仕組み
unittestの仕組みをごく単純化すると、
- テストコードが書かれたファイルを用意する
- そのファイルを
% python3 ファイル名.py
として実行する
これだけです。
そしてテストコードファイルの中身はこんな感じ。
# unittestモジュールのimport
import unittest
# TestCaseクラスの定義
class MyTestCase(unittest.TestCase):
# テスト実行関数の定義
def test_test1(self):
print("hoge")
# テストの実行判定
if __name__ == "__main__":
unittest.main()
基本的にはunittest.TestCase
を継承したクラスを作成し、その中でtest_xxx
というインスタンスメソッドを定義します。
このtest_xxx
がテストの実行関数であり、% python3 tests.py
でテストを実行した際に呼び出されます。
最後の
if __name__ == "__main__":
unittest.main()
についてですが、unittest.main()
はモジュール内の全てのtest_xxx
メソッドを呼び出して実行する役割、if __name__ == "__main__"
はこのモジュールが他のファイルにimportされた際にはunittest.main()
が実行されないようにするための役割です。
今回は詳しい説明は省きますが、おまじないだと思って書いてください。
さて、上記のtests.pyを実行しても今回はhoge
と出力されるだけです。
実行関数の中身がprint("hoge")
なので当たり前ですね。
実際にはこのテスト実行関数の中身を、プログラムが期待通りに動いているかどうかを調べるメソッドにすることでテストを行います。
テスト用のメソッド達
assertEqual(a, b)
a == b
であることを確かめる関数。
import unittest
class MyTestCase(unittest.TestCase):
def test_test1(self):
self.assertEqual((10 + 5), 15) # assertEqualはTestCaseクラスの関数なのでself.の形で呼び出す。
if __name__ == "__main__":
unittest.main()
assertNotEqual(a, b)
a != b
であることを確かめる関数。
assertTrue(a)
a == True
であることを確かめる関数。
assertFalse(a)
a == False
であることを確かめる関数。
assertIn(a, b)
aがbに含まれるかどうかを調べる関数。
aとbの関係性は文字列対文字列
, 文字列対リスト
, リスト対リスト
のいずれでも可(辞書は使えない。成功失敗判定の例は以下参照)。
class MyTestCase(unittest.TestCase):
def test_test3(self):
self.assertIn("hoge", "hogehoge") # これはOK
def test_test4(self):
self.assertIn("hoge", ["hoge", "fuga"]) # これもOK
def test_test5(self):
# self.assertIn(["hoge", "fuga"], ["hoge", "fuga", "fuga"]) # これはNG
# self.assertIn(["hoge", "fuga"], ["hoge", "fuga"]) # これでもNG
pass
def test_test6(self):
self.assertIn(["hoge", "fuga"], [["hoge", "fuga"] ,["hoge", "hoge", "fuga", "fuga"]]) # これはOK
def test_test7(self):
self.assertIn(("hoge", "fuga"), (("hoge", "fuga"), ("hoge", "hoge", "fuga", "fuga"))) # これもOK
with self.assertRaises
引数として与えた例外の有無を確認する。
class MyTestCase(unittest.TestCase):
def test_div(self):
with self.assertRaises(ZeroDivisionError):
return (5 / 0) # これはテストOK
# return (5 / 3) これは失敗
setUpとtearDown
setUp系メソッドはテスト実行前、tearDown系メソッドはテスト実行後に呼び出される。
呼び出しのタイミングはモジュールの呼び出し前後、クラスの呼び出し前後、メソッドの呼び出し前後の計6通り。
詳細は以下。
関数名 | 実行タイミング | 関数の種類 |
---|---|---|
setUp | テスト関数実行前 | インスタンスメソッド |
tearDown | テスト関数実行後 | インスタンスメソッド |
setUpClass | クラスの呼び出し前 | クラスメソッド |
tearDownClass | クラスの呼び出し後 | クラスメソッド |
setUpModule | モジュールの呼び出し前 | クラスとは独立 |
tearDownModule | モジュールの呼び出し後 | クラスとは独立 |
import unittest
from unittest import TestCase
def setUpModule():
print("set up module")
def tearDownModule():
print("tear down module")
class MyTestCase1(TestCase):
@classmethod
def setUpClass(cls) -> None:
print("set up class1")
return super().setUpClass()
@classmethod
def tearDownClass(cls) -> None:
print("tear down class1")
return super().tearDownClass()
def test_test1(self):
print("test1")
def test_test2(self):
print("test2")
def setUp(self) -> None:
print("set up")
return super().setUp()
def tearDown(self) -> None:
print("tear down")
return super().tearDown()
class MyTestCase2(TestCase):
@classmethod
def setUpClass(cls) -> None:
print("set up class2")
return super().setUpClass()
@classmethod
def tearDownClass(cls) -> None:
print("tear down class2")
return super().tearDownClass()
def test_test1(self):
print("test1")
def test_test2(self):
print("test2")
def setUp(self) -> None:
print("set up")
return super().setUp()
def tearDown(self) -> None:
print("tear down")
return super().tearDown()
if __name__ == "__main__":
unittest.main()
実行結果は以下。
set up module
set up class1
set up
test1
tear down
.set up
test2
tear down
.tear down class1
set up class2
set up
test1
tear down
.set up
test2
tear down
.tear down class2
tear down module
.
がついている部分は、直前にテスト実行関数が成功していることを示しています。
@unitest.skip
直後のテストをスキップする。
スキップの範囲はこの記述を配置した場所によって決まり、関数の前においたのであればその関数(及びその前後のsetUpメソッド、tearDownメソッド)のみを、クラスの前においたのであればそのクラス(と付随するsetUpメソッド、tearDownメソッド、setUpClassメソッド、tearDownClassメソッド)全体をスキップする。
# @unittest.skip("not implemented yet")
class MyTestCase2(TestCase):
@classmethod
def setUpClass(cls) -> None:
print("set up class2")
return super().setUpClass()
@classmethod
def tearDownClass(cls) -> None:
print("tear down class2")
return super().tearDownClass()
@unittest.skip("not implimented yet") # ここをスキップ
def test_test1(self):
print("test1")
def test_test2(self):
print("test2")
def setUp(self) -> None:
print("set up")
return super().setUp()
def tearDown(self) -> None:
print("tear down")
return super().tearDown()
if __name__ == "__main__":
unittest.main()
()内にはスキップの理由を記載することが可能。
また、テスト実行結果のログにおいてはスキップ部分はs
に置き換わる。
set up module
set up class2
sset up # ここの先頭のsがスキップ箇所
test2
tear down
.tear down class2
tear down module
また、テスト実行時に-v
をオプションでつけると詳細なログが表示され、その際には@unittest.skip()
の括弧内のスキップ理由も表示される。
% python3 testcase.py -v
# 結果
set up module
set up class2
test_test1 (__main__.MyTestCase2) ... skipped 'not implimented yet'
test_test2 (__main__.MyTestCase2) ... set up
test2
tear down
ok
tear down class2
tear down module
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK (skipped=1)