pythonのunittestのコア部分を自作してみる
はじめに
python の組み込みのテストライブラリであるunittest
を自作してみました。
といってもすべての機能ではなく、
具体的には、以下の画像のように実行したテスト件数と失敗したテスト件数が表示される、テストライブラリのコア部分のみの実装になります。
元はというと、ソフトウェアエンジニアの世界では超有名な本「テスト駆動開発」を読んでみたのがきっかけでした。
この本の第3章「xUnit」でまさにunittest
のコア部分を TDD で実装するのですが、
その内容をより python のunittest
に似せた形にしたのと、型ヒントの付与などのリファクタリングを加えました。
(あとは python でクラスのバリデーションを強化するpydantic
に最近ハマっているのでそれも適用しました)
コードはすべてこちらの github リポジトリに格納しています。
実装したコード
使い方
今回スクラッチで作成したのがmy_unittest
というパッケージです。
先に使い方を載せた方がわかりやすいと思うのでそうします。
見てわかるとおり、 組み込みのunittest
とほぼ同じ使い方になっています。
import my_unittest
# テスト対象の関数
def add(x: int, y: int) -> int:
return x + y
class TestSample(my_unittest.TestCase):
def setUp(self):
# setUpの処理が必要な場合はここに追記
...
def tearDown(self):
# tearDownの処理が必要な場合はここに追記
...
def test_add(self):
assert add(1, 2) == 3
def test_add_failed(self):
assert add(1, 2) == 2
if __name__ == "__main__":
my_unittest.main(TestSample)
コマンドラインで実行すると以下のようになります。
my_unittest
パッケージの中身
まずは、ロジックのコアとなるTestCase
クラスです。
インターフェイスとしてITestCase
を定義している点と、
getattr(test_case, test_case.name)
の箇所でメソッド名を動的に取得している点がポイントです。
(ただし、getattr
はコードの静的解析が難しくなるので多用しない方が良いらしい)
import abc
from pydantic import BaseModel
from my_unittest.TestResult import TestResult
class ITestCase(abc.ABC):
name: str
@abc.abstractmethod
def setUp(self): ...
@abc.abstractmethod
def tearDown(self): ...
@abc.abstractmethod
def run(self, result: TestResult, test_case: "ITestCase") -> TestResult: ...
class TestCase(BaseModel, ITestCase):
"""ロジックのコアとなるクラス"""
name: str
def setUp(self): ...
def tearDown(self): ...
def run(self, result: TestResult, test_case: ITestCase) -> TestResult:
result.testStarted()
test_case.setUp()
try:
method = getattr(test_case, test_case.name)
method()
except Exception as e:
print(e)
result.testFailed()
test_case.tearDown()
return result
テストの実行結果を保存する責務をもつTestResult
クラスです。
from pydantic import BaseModel
class TestResult(BaseModel):
run_count: int = 0
failed_count: int = 0
def testStarted(self):
self.run_count += 1
def testFailed(self):
self.failed_count += 1
def summary(self):
return f"{self.run_count} run, {self.failed_count} failed"
複数のテストをまとめて実行できるようにするTestSuite
クラスです。
from my_unittest.TestCase import ITestCase
from my_unittest.TestResult import TestResult
from pydantic import BaseModel
class TestSuite(BaseModel):
tests: list[object] = []
def add(self, test: ITestCase):
self.tests.append(test)
def run(self, result: TestResult):
[t.run(result, t) for t in self.tests if isinstance(t, ITestCase)]
return result
テストを実行するmain
関数です。
from typing import Type
from my_unittest.TestCase import TestCase
from my_unittest.TestResult import TestResult
from my_unittest.TestSuite import TestSuite
HR_LINE = "=" * 40
def main(cls: Type[TestCase]):
print(HR_LINE + " test session starts " + HR_LINE)
suite = TestSuite()
for method in get_test_methods(cls):
suite.add(cls(name=method))
result = TestResult()
suite.run(result)
print(HR_LINE + f" {result.summary()} " + HR_LINE)
def get_test_methods(cls: object):
return [
method_name
for method_name in cls.__dict__
if method_name.startswith("test") and callable(getattr(cls, method_name))
]
おわりに
python でunittest
のコア部分をスクラッチで実装してみました。
個人的にはなかなか面白い実装だなと感じました。
(ただ、やっぱり「テスト駆動開発」の内容で行われているテストライブラリをテスト駆動で開発する、という試みは TDD の初学者には挑戦的すぎるよな、というのも感じました。)
また、オブジェクト指向の色が強い実装になっているので、関数型のパラダイムの言語ではテストフレームワークはどのような実装になっているのか気になりました。
Discussion