はじめに
go界隈でtable driven testと呼ばれる形式でunit testを書く事が好まれているらしい(とは言え、他の言語でも同様のものは存在したりはしていたので発明したというわけではないけれど)。
pythonでtable driven testを書くならどうすると良いかと言うことを考えてみた。
unittestで普通に書くと長い
例えば、以下のようなaddという関数をテストするとする。
def add(x, y):
return x + y
pythonのunittestモジュールの範囲で愚直に書くと以下の様な感じ。
import unittest
class Tests(unittest.TestCase):
def _callFUT(self, x, y):
from add import add
return add(x, y)
def test_with_positive(self):
actual = self._callFUT(10, 10)
self.assertEqual(actual, 20)
def test_with_zero(self):
actual = self._callFUT(10, 0)
self.assertEqual(actual, 10)
def test_with_negative(self):
actual = self._callFUT(10, -10)
self.assertEqual(actual, 0)
def test_with_biiiiiiiiiig(self):
actual = self._callFUT(
10,
10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,
)
self.assertEqual(
actual,
10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010,
)
長い。だるい。
(callFUTについては信仰的なもの。詳しくはこの辺を参照)
table driven test
table driven testにするとシュッとした感じになる。subTestを使うと良さそう。
import unittest
from collections import namedtuple
class Tests(unittest.TestCase):
def _callFUT(self, x, y):
from add import add
return add(x, y)
def test_it(self):
C = namedtuple("C", "msg x y expected")
candidates = [
C(msg="with positive", x=10, y=10, expected=20),
C(msg="with zero", x=10, y=0, expected=10),
C(msg="with negative", x=10, y=-10, expected=0),
C(
msg="with biiiiiiig",
x=10,
y=10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,
expected=10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010,
),
]
for c in candidates:
with self.subTest(msg=c.msg):
actual = self._callFUT(c.x, c.y)
self.assertEqual(actual, c.expected)
以下は個人的な好み
- namedtupleを利用している
- msgも一緒に書く
- _callFUT(testメソッドが一つで終わるのなら分けなくても良いかもしれない)
subTestの件数の集計方法
成功したときには1件としてカウントされる。
.
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
失敗したときにはN件としてカウントされるので良い。
======================================================================
FAIL: test_it (__main__.Tests) [with positive]
----------------------------------------------------------------------
Traceback (most recent call last):
File "tests.py", line 26, in test_it
self.assertEqual(actual, c.expected + 1)
AssertionError: 20 != 21
======================================================================
FAIL: test_it (__main__.Tests) [with zero]
----------------------------------------------------------------------
Traceback (most recent call last):
File "tests.py", line 26, in test_it
self.assertEqual(actual, c.expected + 1)
AssertionError: 10 != 11
======================================================================
FAIL: test_it (__main__.Tests) [with negative]
----------------------------------------------------------------------
Traceback (most recent call last):
File "tests.py", line 26, in test_it
self.assertEqual(actual, c.expected + 1)
AssertionError: 0 != 1
======================================================================
FAIL: test_it (__main__.Tests) [with biiiiiiig]
----------------------------------------------------------------------
Traceback (most recent call last):
File "tests.py", line 26, in test_it
self.assertEqual(actual, c.expected + 1)
AssertionError: 10000[33 chars]000000000000000000000000000000000000000000000000000000000000010 != 10000[33 chars]000000000000000000000000000000000000000000000000000000000000011
----------------------------------------------------------------------
Ran 1 test in 0.003s
FAILED (failures=4)
pytestを使った場合
pytestを使ったtable driven testについては誰か書いてくれたりしないかな。