どうもです、タドスケです。
仕事でテストを書くときに unittest を使っているのですが、初めのうちは公式リファレンスを見ても
で、それがどう役に立つの?
ってなっていました。
その後何度もテストを書き、先輩のコードレビューを受け、よく使うパターンが何となくわかってきたので、記事にまとめたいと思います。
普段 unittest を使っている人にとって、
- このケースでどうテストを書けばよいのかわからない
- もっとよい書き方はないか
といった疑問を解消する手助けになれば幸いです。
unittest 自体の説明については各所にたくさんありますので、こちらでは省略します。
Python のバージョンは 3.9 を使用しています。バージョンが違うと書き方も変わってくるかもなので、ご注意ください。
Python では関数とメソッドは微妙に違う意味を持ちますが、使い分けるのがめんどくさいので「関数」で統一します。
更新履歴
- 2023/09/09 subTest のサンプルを追加
- 2023/07/10 parameterized のサンプルを追加
- 2023/04/25 仮オブジェクトを作る方法について追記
- 2023/04/20 プロパティでモックを使う方法について追記
- 2023/04/19 公開
サンプルコード
今回説明するサンプルを一通り実装したコードを以下にアップしています。
実際に実行してみたい方はこちらからどうぞ。
テストの前後で特定の処理を呼びたい(setUp / tearDown)
テスト内で毎回初期化処理や終了処理を呼ぶのは面倒です。
「複数のテストで処理やインスタンスを使い回したい!」という場合には、setUp / tearDown が使えます。
setUp / tearDown には有効範囲の違う3種類があります。
テストクラス内の各関数ごとに毎回呼ばれる:setUp / tearDown
テストクラス内に setUp / tearDown を定義しておくと、テストクラス内の各テスト関数を呼ぶ前後に一度ずつ呼ばれます。
例えば以下のケースでは、
- setUp
- test_add
- test_sub
- tearDown
の順で呼ばれます。
class TestMyClass(unittest.TestCase):
def setUp(self):
print('\n* setUp(MyClass)')
self.my_class = basic.MyClass(3)
def tearDown(self):
print('* tearDown(MyClass)')
def test_add(self):
print(f'{self.id()}')
# setUp で生成したオブジェクトを使える
self.assertEqual(4, self.my_class.add(1))
def test_sub(self):
print(f'{self.id()}')
self.assertEqual(2, self.my_class.sub(1))
# setUpClass で入れたメンバーを使える
self.assertEqual(10, self.value)
テスト対象のクラス=テストクラスにしている場合、対象のクラスのインスタンスの生成/破棄処理を書いておくと便利です。
↑ のコード例では、setUp 内で生成した self.my_class をどのテスト内でも使えます。
テスト関数ごとに毎回呼ばれてくれるので、前のテストで初期化した状態が残りっぱなしになる心配もありません。
テストクラス内で一度だけ呼ばれる:setUpClass / tearDownClass
テストクラス内のクラス関数(@classmethod をつける)として setUpClass / tearDownClass を定義しておくと、テストクラス内のテストを実行する最初と最後に一度だけ呼ばれます。
setUpClass / tearDownClass を定義しているテストクラスが複数あれば、クラスの数だけ呼ばれます。
class TestMyClass(unittest.TestCase):
@classmethod
def setUpClass(cls):
print('** setUpClass(MyClass)')
cls.value = 10
@classmethod
def tearDownClass(cls):
print('** tearDownClass(MyClass)')
テスト関数間で状態が引き継がれるので、値を変更するインスタンスには向きません。
テストの実行順によってテスト結果が変わってしまうためです。
サーバーの起動/終了などの重たい処理があり、setUp で毎回処理が呼ばれてしまうのが嫌なケースで使っています。
テストモジュール内で一度だけ呼ばれる:setUpModule / tearDownModule
モジュール内関数として setUpModule / tearDownModule を定義しておくと、モジュール全体のテストを実行する最初と最後に一度だけ呼ばれます。
例えば以下のケースでは、
- setUpModule
- TestModuleFunc のテスト
- TestMyClass のテスト
- tearDownModule
という順で呼ばれます。
def setUpModule():
"""モジュールのテスト開始時に一度だけ呼ばれる."""
print('\n*** setUpModule(basic)')
global module_value
module_value = 5
def tearDownModule():
"""モジュールのテスト終了時に一度だけ呼ばれる."""
print('*** tearDownModule(basic)')
class TestModuleFunc(unittest.TestCase):
...
class TestMyClass(unittest.TestCase):
...
ただ、テストはできるだけ独立しているのが望ましいので、使う機会はあまり無いように思えます。(僕は一度も使ったことがありません)
テスト中のみ、内部関数の処理を置き換えたい(mock)
以下のような構造の関数があるとします。
def outer1(self) -> int:
return 1 + self._inner1()
def _inner1(self) -> int:
# なにやら複雑な処理
return (処理結果によって変わる値)
この例で outer 関数のテストを書きたいとき、_inner1 が別のクラスを使っていたり、サーバー接続時にしか正しい値を取れなかったりする関数だったりすると、テストが面倒になります。
outer 関数としては、_inner1 が何をしようが、結果に 1 を足した値になっていればOKなので、_inner1 を適当な処理で置き換えてしまえば、テストが楽になります。
この時に使えるのが mock(モック)という機能です。
関数にモックを適用するには、3通りの使い方があります。
ローカルオブジェクトとして使う
テスト内で MagicMock() のオブジェクトを作り、置き換えたい関数に渡します。
def test_outer_local(self):
mp_inner = MagicMock()
self.my_class._inner1 = mp_inner
self.my_class.outer1() # mp_inner が呼ばれる
mp_inner を作った後で色々できるのがメリットですが、関数内でモックを無効化したい場合は手動で解除処理を行う必要があります。
デコレータとして使う
テスト関数の上に @mock.patch(~) と書いておくと、そのテスト関数内でモックが有効になります。
@mock.patch('patch.MyClass._inner1')
def test_outer_decorator(self, mp_inner):
self.my_class.outer1() # mp_inner が呼ばれる
有効範囲が関数全体であることがわかりやすいですが、複数使用した場合に適用される順番(内側から順に適用される)がわかりにくいというデメリットがあります。
コンテキストマネージャー(with句)として使う
with 句と一緒に使うと、with の内部でのみ有効になります。
def test_outer_with(self):
with mock.patch.object(self.my_class, '_inner1') as mp_inner:
self.my_class.outer1() # mp_inner が呼ばれる
self.my_class.outer1() # _inner1 が呼ばれる
有効範囲を最小にできるのがメリットですが、複数使用(入れ子)するとネストが深くなって読みにくくなるというデメリットがあります。ただしこの問題は、後述の multiple で緩和できます。
個人的にはこれが一番使いやすいかなと思っています。
関数が呼ばれたかどうかを確認する
テストする関数の内部で呼ばれている別の関数がある場合、以下のようにすると「内部の関数が呼ばれたか」をテストできます。
def test_outer_with(self):
with mock.patch.object(self.my_class, '_inner1') as mp_inner:
self.my_class.outer1()
mp_inner.assert_called() # mp_inner が呼ばれたか
assert_* には以下のようなバリエーションがあります。
assert_called() | 関数が1度でも呼ばれたか |
assert_called_once() | 関数が1度だけ呼ばれたか |
assert_not_called() | 関数が呼ばれていないか |
assert_called_with(hoge) | 関数が引数 hoge で呼ばれたか |
複数の引数のうち、特定のものだけを確認したい
assert_called_with() で複数の引数を指定しているときに、特定の引数だけを確認したくて他の引数はどうでもいい場合、mock.ANY を使うとテストしたい引数以外をスルーできます。
with mock.patch.object(self.my_class, '_inner2') as mp_inner2:
self.my_class.outer2()
# ANY を指定すると全ての値に一致。引数を一部だけ見たい時に使える
mp_inner2.assert_called_with(mock.ANY, 2, mock.ANY)
引数の中に複雑な生成処理を含む場合や、ランダム要素がある場合に使えます。
関数の戻り値を書き換える
「複雑な処理を行っている内部関数の戻り値に応じて処理を変えている関数」のテストを行う場合、「内部関数がこの戻り値を返したことにする」ということができます。
def outer1(self) -> int:
return 1 + self._inner1()
def _inner1(self) -> int:
if (複雑な処理):
return 1
return 0
# _inner1 関数の戻り値を指定する
with mock.patch.object(self.my_class, '_inner1', return_value=1):
ret = self.my_class.outer1()
self.assertEqual(2, ret) # 1 を返したときのテスト
with mock.patch.object(self.my_class, '_inner1', return_value=0):
ret = self.my_class.outer1()
self.assertEqual(1, ret) # 0 を返したときのテスト
別の関数に置き換える
内部で呼んでいる関数がデータベースに接続しないと使えない機能だったりして、テスト時だけ仮の関数で置き換えたい場合、 new か side_effect が使えます。
new
def my_new(val: int): # 元の引数も受け取れる
return val + 2
# _inner の代わりに my_new が呼ばれる。モックオブジェクトは作成されない
with mock.patch.object(self.my_class, '_inner1', new=my_new) as mp_inner:
ret = self.my_class.outer1()
self.assertEqual(5, ret)
# mp_inner.assert_called() # モックオブジェクトが作られないので、assert_called などは使えない
side_effect
def my_side_effect(val: int): # 元の引数も受け取れる
return val + 2
# _inner が呼ばれるタイミングで my_side_effect が呼ばれる。モックオブジェクトが作成される
with mock.patch.object(self.my_class, '_inner1', side_effect=my_side_effect) as mp_inner:
ret = self.my_class.outer1()
self.assertEqual(5, ret)
mp_inner.assert_called()
使い分け
両者の違いは「モックオブジェクトを作るかどうか」です。assert_called() などを使わないのであれば、オブジェクトを作らないぶん、new の方が高速です。
ただし、大した違いではないので基本的には side_effect だけでも十分かなと思っています。
存在しない関数/メンバー変数にアクセスした際にエラーを出す
モックでオブジェクトを置き換えた場合、オブジェクトが持っていない関数やメンバー変数にアクセスしようとしてもエラーになりません。
モックオブジェクトが勝手に作ってしまうからです。
class MyClass:
def outer1(self):
...
mock_myclass = MagicMock()
mock_myclass.outer1()
mock_myclass.ouner1() # 本来の MyClass に存在しないメソッドでもエラーにならない
これを防ぐには、モックの引数 spec にクラスを渡します。
mock_myclass = MagicMock(spec=MyClass)
mock_myclass.outer1()
# mock_myclass.ouner1() # MyClass に存在しないメソッドを呼ぶとエラーになる
複数の関数でまとめてモックを使う
モックを使いたい関数が複数あるとき、そのまま素直に使おうとすると…
with mock.patch.object(self.my_class, '_inner1') as mp_inner1:
with mock.patch.object(self.my_class, '_inner2') as mp_inner2:
with mock.patch.object(self.my_class, '_inner3') as mp_inner3:
self.my_class.outer1()
mp_inner3.assert_called()
mp_inner2.assert_called()
mp_inner1.assert_called()
こんな感じでテストコードの階層が深くなって見にくくなってしまいます。
↑ の例のように同じオブジェクト内の関数であれば、mock.patch.multiple を使うと複数個まとめてパッチできます。
def mock_inner2(a, b, c):
...
with mock.patch.multiple(
self.my_class, # 対象のモジュールまたはオブジェクト
_inner1=mock.DEFAULT, # モックを生成する場合は DEFAULT
_inner2=mock_inner2) as mp: # 他の関数を代わりに呼びたい場合
mp_inner1 = mp['_inner1'] # 関数名をキーにしてモックを取得できる
mp_inner1.return_value = 0 # 通常のモック同様に return_value なども変更できる
ret = self.my_class.outer1()
self.assertEqual(1, ret)
mp_inner1.assert_called_with(2) # 呼び出し確認
self.assertFalse('_inner2' in mp) # DEFAULT を指定していないので、モックは生成されない
self.my_class.outer2() # _inner2 の代わりに mock_inner2 が呼ばれる
テストクラス全体でモックを流用する
テストごとに毎回同じ mock.patch を書くのが面倒な場合、setUpClass 内で呼んだ mock.patch を有効化しておくことができます。
class TestMyClassStartStop(unittest.TestCase):
# patch.start/stop を使う例
@classmethod
def setUpClass(cls):
patcher = mock.patch('patch.MyClass._inner1')
patcher.return_value = 0
patcher.start()
# setUpClass 内で例外が発生した場合、
# tearDownClass が呼ばれないので、パッチが残りっぱなしになる
# addClassCleanup に登録しておけば、例外発生時も呼ばれる
cls.addClassCleanup(mock.patch.stopall)
def test_outer(self):
# setUpClass で設定したモックが生きている
myclass = patch.MyClass(0)
self.assertIsInstance(myclass._inner1, MagicMock)
self.assertNotIsInstance(myclass._inner2, MagicMock)
コード内のコメントに書いてある通り、例外発生時にパッチが残りっぱなしにならないように、addClassCleanup でパッチの解除処理を登録しておくことが推奨されています。
プロパティにモックを使う
mock.patch のデフォルトで利用される MagicMock は、プロパティを置き換えることはできません。
# プロパティを置き換えようとするとエラーになる
with mock.patch.object(self.my_class, 'value', return_value=5):
ret = self.my_class.value
self.assertEqual(5, ret)
プロパティに対してモックを使いたい場合は、mock.patch の引数 new_callable に mock.PropertyMock を渡します。
# PropertyMock を指定すれば、プロパティを置き換えられる
with mock.patch('patch.MyClass.value', new_callable=mock.PropertyMock, return_value=5):
ret = self.my_class.value
self.assertEqual(5, ret)
ただしこのやり方の場合、mock.patch.object を使うことはできないので注意してください。
適当な仮オブジェクトを作る
オブジェクトを返す内部関数をモックする際、return_value で適当な仮オブジェクトを返すようにする必要があります。
しかし、オブジェクトの生成処理が複雑だと、テストのためだけに毎回オブジェクトを作るのは面倒です。
new_my_class = patch.MyClass([ものすごい複雑なパラメータ])
with mock.patch.object(self.my_class, '_inner_get', return_value=new_my_class):
ret_my_class = self.my_class.outer_get()
self.assertIs(new_my_class, ret_my_class)
そんなときは mock.sentinel を使うと、仮オブジェクトをその場でサクっと作れます。
# MyClass を作るのがめんどくさいので、sentinel に置き換える
sentinel = mock.sentinel # 型指定も不要
with mock.patch.object(self.my_class, '_inner_get', return_value=sentinel):
ret_my_class = self.my_class.outer_get()
self.assertIs(sentinel, ret_my_class) # Is 比較もできる
パラメーターだけが異なる複数のテストをまとめて書く
引数によって内部で処理が変わる関数をテストする際、引数の組み合わせごとに関数呼び出しを書くのは面倒です。
そんなときのために、パラメータだけが異なるテストを一つのテスト関数内で使い回す方法があります。
複数の方法があるので、好みに応じて使い分けるのがよいです。
個人的には、インストール不要な subTest のほうが環境に依存しないので、仕事現場では使いやすいと思います。
subTest
unittest の subTest を使うと、テスト関数内にテストを書けます。
import unittest
import basic
class TestMyClass(unittest.TestCase):
def test_add(self):
params = (
('name1', 0, 0, 0),
('name2', 1, 2, 3),
('name3', -1, -2, -3),
)
for name, a, b, expected in params:
with self.subTest(name, a=a, b=b, expected=expected):
my_class = basic.MyClass(a)
answer = my_class.add(b)
self.assertEqual(expected, answer)
PyCharm 上でのテストの実行結果は以下のようになります。
通常の for 文で実行するのとは違い、subTest が途中で失敗した場合でも最後まで実行してくれます。
paramererized
parameterized を使うと、テストで使うパラメーターをデコレーターとして定義できます。
subTest と違って、テスト関数の本体をあまりいじらなくて済む(引数の追加、変数化のみ)のがメリットです。
parameterized は外部ライブラリのため、pip コマンドなどを使ってインストールする必要があります。
import unittest
from parameterized import parameterized
import basic
class MyTestCase(unittest.TestCase):
@parameterized.expand([
('test_a', 1, 2, 3),
('test_b', 2, 3, 5),
('test_c', 3, 4, 7),
])
def test_add(self, name, a, b, c):
self.assertEqual(c, basic.add(a, b))
テストの実行結果は以下のようになります。
collecting … collected 3 items
test_parameterized.py::MyTestCase::test_add_0_test_a
test_parameterized.py::MyTestCase::test_add_1_test_b
test_parameterized.py::MyTestCase::test_add_2_test_c
ちゃんとテスト関数名の末尾に指定した名前がついており、特定のパラメーターで失敗した際にもわかりやすいです。
subTest は成功時にログが出ませんが、parameterized は出ていますね。
コメント
コメント一覧 (1件)
[…] 仕事で Qt を しぬまでワクワクしていたい 【Python】unittest 逆引きリファレンス | しぬまでワクワクしていたい どうもです、タドスケです。 仕事でテストを書くときに unittest […]