Python

【Python3】unittest使い方まとめ

※分量が予想以上に長くなってしまったので先に注意点だけ書かせてください。

  • 3章以降は1〜2章が理解できている前提で話を進めています。
  • 説明のため、mock等をunittest.mock.patchのように冗長に記述していますが、実際はfrom unittest.mock import patchとして問題ありません。

以下、本文になります。

1.unittestとは?

公式の説明に「unittestとは」の全てが含まれているので引用します。

unittest ユニットテストフレームワークは元々 JUnit に触発されたもので、 他の言語の主要なユニットテストフレームワークと同じような感じです。 テストの自動化、テスト用のセットアップやシャットダウンのコードの共有、テストのコレクション化、そして報告フレームワークからのテストの独立性をサポートしています。

https://docs.python.org/ja/3/library/unittest.html

また、実際の使用感としてはかなり簡単に自動試験を作成でき、他のxUnit系のツールよりもお手軽だと感じました。
(Pythonという言語自体にそういう側面があるので一概には言えませんが)

そもそもxUnitが分からない、という方は「単体テストを自動化して楽するためのツールなんだな〜」ぐらいに考えておいてください。

テストコードを作ったりメンテナンスしたりするコストはかかりますが、バグ修正や仕様変更で後から修正が入った時に再試験の手間が圧倒的に少なくなります。
(リグレッションテストってやつです。デグレで泣きを見たくなければやっておきましょう)

Pythonのunittestは、作成コスト・学習コストがかなり低いので、この機会に勉強しておくといずれハッピーになれます。
少なくとも単体試験が工程として存在するのであれば絶対に作るべきです。

2.基本的な使い方

一番基本的な使い方は、以下です。
①試験したい関数を呼び出す
②その結果が想定通りかを判定する


例えば以下の関数の試験を考えてみます。

def func_hoge(x):
    if x < 0:
        return 'a'
    else:
        return 'b'

この関数の試験で確認したいのは以下ですね。
・-1を入力した時に’a’が返ってくること
・0を入力した時に’b’が返ってくること

この確認をunittestで自動化すると以下になります。

詳しい説明はコードのコメントに書きましたが、まずは以下を抑えておきましょう。
①テストはunittest.TestCaseを継承したクラスの中に書く。
②テストの関数名はtest〜で始める。
③判定はself.assertEqual()等、unittestで用意されている判定を使用する。


①〜③をやっておけば後は”unittest.main()”を実行するだけで記述したテストを全て自動でやってくれるのです。

また、③の判定はNotEqualやRegex等、他にも色々な種類があるので判定したい内容に合わせて変えていくことになるかと思います。

ちなみにこの判定関数はアサートメソッドと言うのが一般的です。

末尾によく使うものを一覧にしたので必要に応じて確認してください。
アサートメソッド一覧

ここまで説明を忘れていましたがテストケース等の語句は以下の意味で使用しています。

テストdef test_xx()のこと。
1つのテスト項目に対応。
テストケースunittest.TestCaseを継承したクラスのこと。
テストの論理的なまとまり。
テストスイートテストケースのまとまり。

unittestの基本的な使い方は以上です。

どうですか?かなり簡単でしょう?

これであなたもテストホリックの仲間入りですね!!(死んだ魚の目)

※ここから先は具体的な使い方等をつらつらと書いていきます。ここの内容は前提知識として進めるので、まずはここまでをしっかりと押さえてから読み進めると良いかと思います。

3.テストケースの分け方

テストコードを書いているとテストケースをどの単位でまとめるかを迷うことは多々あるかと思います。

これは本当に人それぞれだと思うので私の個人的な方針だけ書いておきます。

①1試験項目につき1つのテスト(test〜の関数)を作成
(subTest()を使用する場合を除く)

②1関数に1つのテストケース(Test〜のクラス)を作成
(たとえテストが一つしかない関数でもテストケースは個別に作るべき)

③同じクラス・モジュールに対するテストケースはなるべくテストスイートにまとめる

①は、試験が失敗した時にどこで失敗したのかが分からなくなるのでほぼ必須かと思います。

②と③はsetUp/tearDownの処理を制御したい場合や、テスト対象の結合度的な部分で変わってくるので、割と流動的な感じです。

4.パターンが多い試験の扱い

テストしたい関数に影響するパラメータが大量にあって、ペアワイズ法とか直行表使っても項目数が2〜3桁になってしまうことはザラにあるかと思います。

「じゃあ、for文回せばよくね?」と思うかもしれませんが、for文でテストを実行すると以下のデメリットがあります。
途中で失敗した場合はそのテストケースはそこで終了する
テストケースの失敗として扱われるのでどのパターンで失敗したのかが分からない

かと言って、1パターン毎にテストを書くのはしんどいですよね。

そんな時のベストプラクティスがunittestには用意されています。

それがsubTestという仕組みです。

コードを見てもらった方が早いと思いますので、まずは以下をどうぞ。

このようにsubTestを利用した書き方をすると、一つのテストケース内で複数のパタンを個別にテストすることができます。

もちろん途中で失敗のパタンがあっても最後まで実行してくれますし、失敗したパタンも個別で表示してくれます。

subTestの実行結果はsubTest内で複数実行しても1としてカウントされてしまう例。

このように良いことずくめですので、パタンの多いテストを実行する際は是非subTestを利用してみてください。

4-A subTestの結果をカウントしたい場合

subTestは便利なのですが、Ranの数にカウントされないのが難点なんですよね。

subTestの実行結果はsubTest内で複数実行しても1としてカウントされてしまう例。


パターン多い試験でテスト数を水増ししたい時等に困りますよね。(←オイ)

そういう時は、unittestの結果を管理するTextTestResultをチョチョイといじってあげましょう。

このコードを入れて実行すると、subTestが1としてカウントされるようになるので、以下のようにRanの数を水増しすることができます。

TextTestResulをいじってRanの数をsubTest数分水増しした例。

Ranの数よりfailuresの数が多くなる悲しい事態も回避できるので是非お試しあれ。

5.例外の試験

例外の試験の確認方法はそんなに難しくないです。

コードのコメントで全部書いてしまったので特に補足はないです。
以上です。

6.標準入力・標準出力の試験

標準入力の試験

標準入力を試験したい、というか標準入力を想定した試験がしたい場合は特に変わったことをする必要はありません。

標準入力する関数(input()等)にパッチを当てるだけです。

モックやパッチについてはこちらで説明しているのでそちらをご確認ください。

標準出力の試験

続いて標準出力の確認ですが、こちらは少し小手先のテクニックが必要です。

import sys
from io import StringIO
from contextlib import redirect_stdout

io = StringIO()
with redirect_stdout(io):
    # このwith句内での標準出力はioに格納される
    print('hoge')

# 標準出力なので改行文字等に注意
self.assertEqual(io.getvalue(), 'hoge\n')

ポイントは以下です。
・StringIO()を用意し、redirect_stdout()で標準出力をリダイレクトする
・redirect_stdout()のwith句内でテストしたい標準出力を実行する
・getvalue()で標準出力された文字列を取得し比較する※改行文字に注意

7.main関数の試験

unittestでmainを試験すべきかどうかの議論もありますが、今回それは置いておいて、main関数を試験するテクニックをサクッとご紹介します。

ではまずサンプルです。

コード内のコメントで大体説明してしまっていますが、まあ以下の2点を押さえておけば問題ないでしょう。
・コマンドライン引数を擬似るためにsys.argvに自前で値を格納する
・mainの標準出力をリダイレクトしてチェックできるようにする


pythonスクリプトだとあまり見かけないので省略しましたが、スクリプトの終了ステータスをチェックしたい場合はsys.exit()等をモックにしてチェックすることになります。

この時の注意点としては、pythonだと“main()の返り値”“スクリプトの終了ステータス”が一致しないと言う点です。

main()でreturn 1としても終了ステータスが1になるわけではないのです。

main()の試験として確認すべきは、sys.exit()が呼ばれたかどうかと、sys.exit()の返り値です。

8.mock

まずはmockとはなんぞや、と言う話ですが、「テスト対象のコード内で使用している別のオブジェクト(テスト対象ではないシステムの関数等)を置き換える機能」と考えて貰えば良いかと思います。

誤解を恐れずに言えばスタブです。

何が嬉しいかと言うと、例えばsocketを使用しているコードの試験をする際に、socketをmockにしておけばテストのたびに接続先を用意する必要がなくなり、環境構築とかいう糞みたいな作業から解放されます。
(極端な例です)

pythonのunittestの世界ではモック(mock)パッチ(patch)と言う概念があり、それぞれ以下のような認識です。

mock引数や呼び出された回数をチェックする機能を持ち、返り値や例外送出も自在に設定可能なオブジェクト。
patchシステムの関数やクラスをmockオブジェクトに置き換えること。またその仕組み。

mock&patchを使用すれば異常系の試験も楽々です◎

8-A mockの基本的な使い方

まずは簡単なモックを作りos.path.abspath()にパッチを当ててみたいと思います。

unittestと同じで方法さえ分かってしまえばmockも使い方はとても簡単なのです。

コード内のコメントで説明し尽くした感はありますが、ポイントをまとめると以下です。
①モックを作る
②動作を置き換えたいオブジェクトにパッチを当てる
③テスト対象のコードを実行する
④テスト対象のコードがモックを想定通りに呼び出したかをチェックする


④のチェックで今回はassert_called_with()を使用しましたが、このアサートメソッドは他にも種類があります。

末尾の表にまとめておいたので必要に応じて確認してください。
mockのアサートメソッド一覧

8-B mockをカッコよく綺麗に書く方法(我流)

このページの解説では分かりやすさ重視で冗長に書いていますが、デコレータ等を使用するとmockの記述はもっとすっきり書くことができます。

あくまで私流のやり方ですが、mockのお作法的な部分で悩んでいる方は是非ご一読いただければ嬉しいです。

一重にmockと言いましても、テスト単位で当てるのか、テストケース単位で当てるのか等、状況によってベストプラクティスは変わってくるものです。

ここではありがちなパターンでの「ぼくのかんがえたさいきょうのもっくのかきかた」をご紹介したいと思います。

特定のテスト内でパッチを当てる

この場合は愚直に定義する方法の他に2つのアプローチが取れます。

①コンテキストマネージャ(with句)を使う方法

with unittest.mock.patch('os.path.abspath', side_effect=time.sleep(3)) as m:
    m.return_value = 'hoge'
    path = print_abspath('hoge')

書き方としてはpatch()の第一引数にパッチを当てたいオブジェクト名の文字列、キーワード引数でそれぞれ設定したい項目です。

with〜as m:にして、m.return_value等で設定することも可能です。

この書き方であればwith句を出れば自動的にパッチが解除されるのでお掃除系を考えなくてよくて楽です。

②テストに対してデコレータを使用する方法

@unittest.mock.patch('os.path.abspath', return_value='hoge')
def test_hoge(self, mock):
    mock.side_effect = OSError("dummy_error")
    path = os.path.abspath()

この書き方であればデコレータを設定したテスト全体でmockが適用されます。

テストの第二引数以降にパッチが指定されてくるのですが、自分は複数指定した場合にハマってしまいました。

例えば以下のように2つのパッチを設定するとします。

@unittest.mock.patch('func1')
@unittest.mock.patch('func2')
def test_hoge(self, p1, p2):
    pass

この時、p1, p2には何が入っていると思います?
p1→func2のパッチ
p2→func1のパッチ

ですよ?

「「「ざっけんなぁぁぁぁ」」」

で、公式ドキュメント見に行ったら、デコレータはそんなもんだぜ、って書いてました。
はい、似非パイソニスタでごめんなさい。

話が逸れてしまいましたが、テスト単位でパッチを当てる場合はwith句を使用する方法とデコレータを使用する方法があると言うことです。

with句の方がスコープが厳密で正しいのかもしれませんが、私はインデント深くしたくないのでデコレータを愛用しています。

テストケースの単位でパッチを当てる

テストケース単位でパッチを当てる場合も2つのアプローチがあります。

①デコレータを使用する

@unittest.mock.patch('os.path.abspath')
class HogeTest(unittest.TestCase):
    def test_hoge(self, mock):
        pass

基本的な書き方はテスト単位のデコレータと同じなので割愛しますが、テストケースのクラスに対してデコレータ式を使用することでそのテストケース全体でそのパッチが使用できるようになります。

若干扱いが面倒なので以下の2点には注意が必要です。
・テストケース内の全てのテストでdef test_*(..., mock):のようにモックを引数にとる必要がある。
・テストの中でreturn_value等を設定した場合は他のテストには反映されない(モックはテスト毎に別オブジェクト扱い)

②setUpで定義する

class HogeTest(unittest.TestCase):
    def setUp(self):
        patcher = unittest.mock.patch('os.path.abspath')
        self.mock = patcher.start()
        self.addCleanup(patcher.stop)

def test_hoge(self):
    pass

setUp()内でパッチを定義&開始して、それをメンバ変数に入れています。

addCleanup()はテストケースが異常終了してtearDown()が呼ばれなかった場合でもパッチを終了させるために書いています。

①同様パッチはテスト毎に生成されて別オブジェクトになるのでテストないでパッチを弄っても他のテストには反映されません。

①、②一長一短ではありますが、テスト毎に一々モックを引数にとるのは煩わしいので私は②を愛用しています。


思ったより説明が冗長になってしまいましたが、愚直にmockを定義するよりは絶対に綺麗なテストコードになると思いますので是非お試しください。

8-C mockの戻り値や例外送出を設定する方法

方法さえ知っていれば簡単。

8-D print()等、ビルトイン関数にパッチを当てたい場合

方法さえ知っていれば簡単。

9.アサートメソッドについて

よく使うアサートメソッド一覧

メソッドどのようなチェックが行われるか
assertEqual(a, b)a == b
assertNotEqual(a, b)a != b
assertTrue(x)bool(x) is True
assertFalse(x)bool(x) is False
assertIs(a, b)a is b
assertIsNot(a, b)a is not b
assertIsNone(x)x is None
assertIsNotNone(x)x is not None
assertIn(a, b)a in b
assertNotIn(a, b)a not in b
assertIsInstance(a, b)isinstance(a, b)
assertNotIsInstance(a, b)not isinstance(a, b)
assertRaises(exc, fun, *args, **kwds)fun(*args, **kwds) が exc を送出する
assertRaisesRegex(exc, r, fun, *args, **kwds)fun(*args, **kwds) が exc を送出してメッセージが正規表現 r とマッチする
assertGreater(a, b)a > b
assertGreaterEqual(a, b)a >= b
assertLess(a, b)a < b
assertLessEqual(a, b)a <= b
assertRegex(s, r)r.search(s)
assertNotRegex(s, r)not r.search(s)

よく使うmockのアサートメソッド一覧

メソッドどのようなチェックが行われるか
assert_called()モックが少なくとも一度は呼び出された
assert_called_once()True(x)モックが一度だけ呼び出された
assert_called_once_with(*args, **kwargs)モックが一度だけ(*args, **kwargs)の引数で呼び出された
assert_not_called()モックが一度も呼び出されていない

参考

unittest全般
https://docs.python.org/ja/3/library/unittest.html
unittestのmock
https://docs.python.org/ja/3/library/unittest.mock-examples.html
応用的な色々
https://docs.python.org/ja/3/library/test.html#module-test
カバレッジ
https://blanktar.jp/blog/2015/03/python-unittest-coverage.html

コメント

タイトルとURLをコピーしました