関数を書いたあと、本当に正しく動くのか不安になったことはありませんか。とりあえずprintで結果を表示して、目で見て確かめている方も多いと思います。
最初のうちはそれでも困りません。けれど、コードを直すたびに同じ確認を手作業でくり返すのは、だんだん負担になっていきます。
そこで登場するのが、確認作業そのものをプログラムにしてしまう自動テストです。Pythonには、そのための道具としてunittestが標準で用意されています。
今回は、unittestの基本的な使い方を、手元のPython 3.11で実際に動かした結果とともに紹介します。テストを書くのが初めての方でも読み進められるように、ひとつずつ説明していきますね。
unittestとは何か¶
まずは、unittestがどんな道具なのかを整理しておきましょう。unittestは、関数やクラスが期待どおりに動くかを自動で確かめるための、Python標準ライブラリのテストフレームワークです。
フレームワークとは、決まった書き方に沿えばあとの処理を引き受けてくれる仕組みのことです。unittestの場合は、テストを集めて実行し、成功と失敗を一覧で報告するところまでを任せられます。
公式ドキュメントによると、unittestはもともとJavaのJUnitというテストフレームワークに影響を受けて作られました。そのため、クラスを作ってその中にテストを並べるという、少しかっちりした書き方になっています。
標準ライブラリなので、pipでのインストールは必要ありません。Pythonが入っている環境なら、import unittest と書くだけですぐに使い始められます。
自動テストがあると何がうれしいのか¶
自動テストの価値は、一度書けば何度でも同じ確認をやり直せる点にあります。コードを少し直したときに、ほかの部分を壊していないかをコマンド1つで確かめられるのです。
この、直したつもりが別の場所を壊してしまう現象は、デグレードやリグレッションと呼ばれます。人の目だけで防ぐのは難しく、テストがいちばん頼りになる防波堤です。
私は10年ほどエンジニアとして開発に関わってきましたが、テストのない古いコードを修正するのはいまでも緊張します。以前、日付の計算を1行直しただけで、月末の請求処理が丸ごと止まってしまったことがありました。
その関数を使っている場所が別のファイルにもあり、そちらの動きまで変わっていたのが原因です。もしテストが1本でもあれば、リリース前に気づけたはずでした。
はじめてのテストを書いてみよう¶
それでは、実際にテストを書いてみましょう。テストする対象として、足し算と割り算をする小さな関数をcalc.pyというファイルに用意しました。
# calc.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("0では割れません")
return a / b
割り算のほうは、0で割ろうとしたときにValueErrorという例外を出すようにしています。この動きもあとでテストしていきます。
次に、テスト用のファイルをtest_calc.pyという名前で作ります。中身は次のとおりです。
# test_calc.py
import unittest
from calc import add, divide
class TestCalc(unittest.TestCase):
def test_add(self):
self.assertEqual(add(2, 3), 5)
def test_add_negative(self):
self.assertEqual(add(-1, -1), -2)
def test_divide(self):
self.assertAlmostEqual(divide(1, 3), 0.333333, places=5)
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
divide(10, 0)
if __name__ == "__main__":
unittest.main()
ポイントは3つあります。unittest.TestCaseを継承したクラスを作ること、テストのメソッド名をtestで始めること、そして中でassertから始まるメソッドを呼ぶことです。
最後の2行は、このファイルを直接実行したときにテストを走らせるためのおまじないです。if __name__ == "__main__": の意味が気になる方は、こちらの記事も参考にしてみてください。
【関連記事】Pythonのname == mainって何?初心者が必ず疑問に思うこと
テストを実行して結果を読む¶
用意ができたら、ターミナルでtest_calc.pyを実行してみます。手元で python test_calc.py を実行すると、次のように表示されました。
....
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
1行目のドットは、成功したテスト1つにつき1個ずつ表示されます。4つのテストがすべて通ったので、最後にOKと出ているわけです。
もっと詳しく見たいときは、python -m unittest -v のように -v を付けます。するとテストの名前と結果が1行ずつ並び、どのテストが動いたのかがひと目でわかります。
失敗したときの表示を見てみよう¶
テストは、失敗したときにこそ役に立ちます。わざと間違った期待値を書いて、どんな表示になるのかを確かめておきましょう。
たとえば self.assertEqual(add(2, 2), 5) というテストを書いて実行すると、次のような結果になりました。
F
======================================================================
FAIL: test_add_wrong (test_fail.TestFail.test_add_wrong)
----------------------------------------------------------------------
Traceback (most recent call last):
File ".../test_fail.py", line 6, in test_add_wrong
self.assertEqual(add(2, 2), 5)
AssertionError: 4 != 5
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)
ドットの代わりにFが表示され、どのテストの何行目で失敗したのかが示されます。AssertionErrorの行からは実際の値が4で期待値が5だとわかり、読み方はふだんのエラー文と同じです。
【関連記事】Pythonのエラー文はどこを読めばいい?初心者向けtracebackの見方
よく使うassertメソッド¶
unittestには、値を確かめるためのassertメソッドがたくさん用意されています。まずは、出番の多いものを表で押さえておきましょう。
| メソッド | 確かめる内容 | 使いどころの例 |
|---|---|---|
| assertEqual(a, b) | aとbが等しい | 関数の戻り値の確認 |
| assertNotEqual(a, b) | aとbが等しくない | 値が変わったことの確認 |
| assertTrue(x) | xが真とみなされる | 条件を満たすかの確認 |
| assertFalse(x) | xが偽とみなされる | 条件を満たさないことの確認 |
| assertIn(a, b) | aがbに含まれる | リストや文字列の中身の確認 |
| assertIsNone(x) | xがNoneである | 何も返さない関数の確認 |
| assertAlmostEqual(a, b) | aとbがほぼ等しい | 小数の計算結果の確認 |
| assertRaises(例外) | 指定した例外が起きる | エラー処理の確認 |
迷ったら、まずはassertEqualを使えば大丈夫です。多くのテストは、この関数にこれを渡したらこれが返る、という形で書けるからです。
小数とエラーのテストは少し特別¶
小数の計算には注意が必要です。コンピューターの中では小数がぴったり表現できないことがあり、0.1 + 0.2 は0.3ちょうどにはなりません。
そのため、小数を比べるときはassertEqualではなく、assertAlmostEqualを使います。placesで指定した桁数まで丸めて比べてくれるので、誤差に振り回されずに済みます。
小数の誤差がなぜ起きるのかは、こちらの記事でくわしく解説しています。
【関連記事】Pythonの丸め誤差とは?初心者にもわかる原因と対策
もう一つ特別なのが、例外のテストです。assertRaisesをwith文と組み合わせると、そのブロックの中で指定した例外が起きたときだけテストが成功します。
逆に、例外が起きなかった場合はテストが失敗します。0で割ったのにエラーにならなかった、というバグをきちんと見つけられるわけですね。
setUpとtearDownで準備と後片付けをする¶
テストが増えてくると、毎回同じ準備をくり返すことになります。そんなときに便利なのが、setUpとtearDownという特別なメソッドです。
setUpは各テストの直前に、tearDownは各テストの直後に、自動で呼ばれます。動きを確かめるために、print文を入れたサンプルを用意しました。
import unittest
class TestCart(unittest.TestCase):
def setUp(self):
print("setUp")
self.cart = []
def tearDown(self):
print("tearDown")
def test_add_item(self):
self.cart.append("りんご")
self.assertEqual(len(self.cart), 1)
def test_empty(self):
self.assertEqual(self.cart, [])
これを実行すると、setUpとtearDownがテストの数だけ交互に表示されました。テストごとに新しい空のリストが用意されるので、test_add_itemで追加したりんごがtest_emptyに残ることはありません。
ここがとても大切なところです。テスト同士は互いに影響し合わないように作るのが、unittestに限らずテストの基本ルールです。
ちなみに、クラス全体で1回だけ準備したい場合は、setUpClassとtearDownClassを使います。データベースへの接続のように、毎回作り直すと時間がかかるものに向いています。
subTestで似たテストをまとめる¶
同じ確認を、値だけ変えて何度もくり返したいことがあります。そんなときはfor文で回したくなりますが、そのままだと最初の失敗でテストが止まってしまいます。
そこで使いたいのがsubTestです。for文の中で with self.subTest(n=n): と書くと、1つ失敗しても残りの値の確認を続けてくれます。
def test_even(self):
for n in [2, 4, 7, 8]:
with self.subTest(n=n):
self.assertEqual(n % 2, 0)
実行すると、失敗の表示に (n=7) と添えられ、どの値でつまずいたのかがすぐにわかりました。7のあとの8もきちんと確認されています。
テストの見つけ方と実行のコツ¶
テストファイルが増えたら、1つずつ実行するのは大変です。プロジェクトのフォルダで python -m unittest と打つだけで、unittestがテストを探して全部まとめて実行してくれます。
この自動で探す仕組みは、テストディスカバリーと呼ばれます。ただし、見つけてもらうにはいくつかの決まりを守る必要があります。
| 決まり | 既定の条件 | 守らないとどうなるか |
|---|---|---|
| ファイル名 | test*.py に一致する | ファイルごと無視される |
| メソッド名 | testで始まる | そのメソッドは実行されない |
| クラス | unittest.TestCaseを継承する | テストとして扱われない |
手元で確かめたところ、cart_check.pyという名前のファイルは見つけてもらえませんでした。また、check_totalという名前のメソッドは、中身がわざと失敗する内容でも実行されずにOKになりました。
名前のルールを外れたテストは、失敗ではなく静かに無視されるという点に注意してください。テストを書いたのに件数が増えていないときは、まず名前を疑うのが近道です。
実は私も、新人のころにこの罠にはまりました。tset_のようなタイポのまま何週間も放置していて、レビューで指摘されるまで、そのテストが一度も動いていなかったことに気づかなかったのです。
それ以来、テストを追加したら -v で名前が並ぶかを必ず確かめるようにしています。ほかにも、-k empty のように指定すると名前に empty を含むテストだけを実行でき、-f を付けると最初の失敗で止められます。
unittestとpytestの違い¶
テストについて調べると、pytestという名前もよく見かけると思います。pytestはpipでインストールする外部ライブラリで、より短い書き方でテストを書けるのが特徴です。
両者の違いを、表で比べておきます。
| 比較のポイント | unittest | pytest |
|---|---|---|
| インストール | 不要(標準ライブラリ) | pipで追加が必要 |
| テストの書き方 | クラスとassertメソッド | 関数とassert文だけで書ける |
| 準備と後片付け | setUpとtearDown | フィクスチャという仕組み |
| 向いている場面 | 追加ライブラリを入れにくい環境 | 新しく始めるプロジェクト |
どちらを選ぶべきか迷うかもしれませんね。うれしいことに、pytestの公式ドキュメントによると、pytestはunittestで書いたテストもそのまま実行できます。
つまり、unittestで覚えた知識はむだになりません。まずは標準ライブラリだけで書き始めて、必要になったらpytestを実行役に使う、という段階的な移行もできます。
pytestについては、こちらの記事でくわしく紹介しています。
【関連記事】pytest入門|Pythonでテストを書く基本を初心者向けに解説
外部APIなど、テストのたびに呼び出したくないものを差し替える方法も知っておくと便利です。unittest.mockの使い方は、こちらの記事が参考になります。
【関連記事】Pythonのモックとは?unittest.mockで外部APIに頼らないテストを書く基本
まとめ¶
unittestは、Pythonに標準で入っている自動テストのフレームワークです。インストール不要で、TestCaseを継承したクラスにtestで始まるメソッドを並べるだけで使い始められます。
値の確認にはassertEqualを基本にして、小数はassertAlmostEqual、エラーはassertRaisesで確かめましょう。準備と後片付けはsetUpとtearDownに任せ、値を変えたくり返しにはsubTestが便利です。
そして、ファイル名とメソッド名のルールを外れると、テストは黙って無視されます。テストを書いたら、本当に実行されているかを -v で確かめる習慣をつけておくと安心です。
まずは、いま書いている関数のどれか1つに、テストを1本だけ足してみてください。コードを直すときの不安が小さくなるのを、きっと実感できるはずです。
ここまでお読みいただきありがとうございました。