- Python
Python unittestの使い方|assert一覧とsetUp・mock
unittestとは、Pythonに最初から入っているテスト用の標準ライブラリで、unittest.TestCase を継承したクラスに test_ で始まるメソッドを書き、python -m unittest で実行する仕組みです。 追加のインストールが要らないので、Pythonが動く環境ならすぐにテストを書き始められます。
この記事では、最小のテストファイルを作るところから、assertメソッドの一覧(用途・書き方・FAIL時メッセージ)、setUp / tearDown の実行順、mock と patch、subTest、skip、pytestとの違いまでを順に扱います。掲載した出力はすべて Python 3.11.15(Linux)で実際に動かした結果です。ファイルのパスだけは /home/taro/sample に置き換えています(表示は環境に合わせて置き換えています)。
unittestの早見表
まず「これだけ見れば書ける」内容を1つの表にまとめます。
| やりたいこと | 書き方 | 補足 |
|---|---|---|
| テストクラスを作る | class TestCalc(unittest.TestCase): |
クラス名は自由。Test で始めると見分けやすい |
| テストを1つ書く | def test_add(self): |
メソッド名は test で始める(始まらないと実行されない) |
| 値が等しいか確かめる | self.assertEqual(実際, 期待) |
いちばんよく使う |
| 例外が出ることを確かめる | with self.assertRaises(ValueError): |
with の中で例外が出ればOK |
| 小数を比べる | self.assertAlmostEqual(a, b, places=3) |
小数点以下の桁数を指定 |
| テストごとの準備 | def setUp(self): |
各テストの直前に毎回実行 |
| テストごとの後片付け | def tearDown(self): |
各テストの直後に毎回実行 |
| 外部の処理を差し替える | @patch("使う側のモジュール.名前") |
「使われている場所」を指定する |
| 全部実行する | python -m unittest |
カレントフォルダから test*.py を探す |
| 詳しく表示する | python -m unittest -v |
テスト名ごとに ok / FAIL / ERROR を表示 |
| フォルダを指定して実行 | python -m unittest discover -s tests |
-p でファイル名パターンも指定可 |
| 1つだけ実行する | python -m unittest test_calc.TestCalc.test_add |
モジュール.クラス.メソッド |
| 名前で絞る | python -m unittest -k zero |
名前に zero を含むテストだけ |
| 最初の1件で止める | python -m unittest -f |
failfast |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python unittestの使い方|assert一覧とsetUp・mock」
最小のテストファイルを作って実行する
テスト対象とテストコード
フォルダを1つ作り、次の2ファイルを置きます。前提は Python 3.11 以降です(3.8 以降でも同じコードが動く書き方にしています)。
# calc.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("0では割れません")
return a / b
# 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_divide(self):
self.assertAlmostEqual(divide(1, 3), 0.333, places=3)
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
divide(1, 0)
if __name__ == "__main__":
unittest.main()
ルールは3つだけです。 ファイル名を test で始める、クラスは unittest.TestCase を継承する、メソッド名を test で始める。この3つがそろうと、unittest が自動で見つけて実行します。
python -m unittest -v の実行結果
python -m unittest -v
test_add (test_calc.TestCalc.test_add) ... ok
test_divide (test_calc.TestCalc.test_divide) ... ok
test_divide_by_zero (test_calc.TestCalc.test_divide_by_zero) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
-v を外すと、テスト1件が1文字で表示されます。
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
ファイル末尾に unittest.main() を書いてあるので、python test_calc.py でも同じ結果になります。普段は python -m unittest を使うと、複数ファイルをまとめて実行できて便利です。
実行コマンドの一覧
| コマンド | 動き |
|---|---|
python -m unittest |
カレントフォルダ以下から test*.py を探して全部実行(discover と同じ) |
python -m unittest -v |
テスト名と結果を1行ずつ表示 |
python -m unittest test_calc |
モジュールを指定 |
python -m unittest test_calc.TestCalc |
クラスを指定 |
python -m unittest test_calc.TestCalc.test_add |
メソッドを1つ指定 |
python -m unittest -k zero |
名前に zero を含むテストだけ |
python -m unittest -f |
最初の FAIL / ERROR で止める |
python -m unittest -b |
テストが通ったときの print 出力を隠す |
python -m unittest discover -s tests -p "test_*.py" |
探すフォルダとファイル名パターンを指定 |
python test_calc.py |
unittest.main() を書いたファイルを直接実行 |
-k zero を実際に付けると、3件のうち該当する1件だけが動きました。
test_divide_by_zero (test_calc.TestCalc.test_divide_by_zero) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
OK・FAIL・ERRORの違いを出力で確かめる
unittest の結果は大きく3種類です。FAIL は「assert の期待と結果が違った」、ERROR は「assert 以外の場所で例外が出た」という違いがあります。3つが同時に出るテストを用意しました。
# test_result.py
import unittest
from calc import add, divide
class TestResult(unittest.TestCase):
def test_ok(self):
self.assertEqual(add(2, 3), 5)
def test_fail(self):
self.assertEqual(add(2, 3), 6)
def test_error(self):
self.assertEqual(divide(1, 0), 0)
python -m unittest -v test_result
test_error (test_result.TestResult.test_error) ... ERROR
test_fail (test_result.TestResult.test_fail) ... FAIL
test_ok (test_result.TestResult.test_ok) ... ok
======================================================================
ERROR: test_error (test_result.TestResult.test_error)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/taro/sample/test_result.py", line 14, in test_error
self.assertEqual(divide(1, 0), 0)
^^^^^^^^^^^^
File "/home/taro/sample/calc.py", line 7, in divide
raise ValueError("0では割れません")
ValueError: 0では割れません
======================================================================
FAIL: test_fail (test_result.TestResult.test_fail)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/taro/sample/test_result.py", line 11, in test_fail
self.assertEqual(add(2, 3), 6)
AssertionError: 5 != 6
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1, errors=1)
| 表示 | 1文字表示 | 意味 | 次にすること |
|---|---|---|---|
| ok | . |
期待どおり | そのまま進む |
| FAIL | F |
assert の期待と結果が違う(AssertionError) | 実際 != 期待 の行を読み、実装か期待値を見直す |
| ERROR | E |
テスト中に AssertionError 以外の例外が出た | Traceback の最後の行(例外の種類)から原因をたどる |
| skipped | s |
skip 指定で実行しなかった | 理由の文字列を確認 |
| expected failure | x |
通らないと分かっているテストが、予想どおり通らなかった | 直ったら expectedFailure を外す |
読み方のポイントは次の3つです。
- テストはメソッド名のアルファベット順に実行されます(test_error → test_fail → test_ok)。書いた順ではありません。
AssertionError: 5 != 6は「実際の値 != 期待した値」の順です。assertEqual(実際, 期待)の順に書いておくと読みやすくなります。- 1件でも FAIL か ERROR があると、終了コードが 1 になります(上の実行では
echo $?が 1 でした)。CI はこの終了コードで合否を判断します。
assertメソッド一覧(用途・書き方・FAIL時メッセージ)
よく使う assert メソッドを、わざと通らない値で実行して、表示されるメッセージを集めました。メッセージの形を知っておくと、テストが止まったときに原因をすぐ読み取れます。
| メソッド | 用途 | 書き方の例 | FAIL時メッセージ(実行結果) |
|---|---|---|---|
| assertEqual | 等しい | assertEqual(1 + 1, 3) |
AssertionError: 2 != 3 |
| assertNotEqual | 等しくない | assertNotEqual(2, 2) |
AssertionError: 2 == 2 |
| assertTrue | 真と評価される | assertTrue([]) |
AssertionError: [] is not true |
| assertFalse | 偽と評価される | assertFalse("a") |
AssertionError: 'a' is not false |
| assertIsNone | None である | assertIsNone(0) |
AssertionError: 0 is not None |
| assertIs | 同じオブジェクト | assertIs([1], [1]) |
AssertionError: [1] is not [1] |
| assertIn | 含まれる | assertIn("d", ["a", "b", "c"]) |
AssertionError: 'd' not found in ['a', 'b', 'c'] |
| assertIsInstance | 型が合う | assertIsInstance("1", int) |
AssertionError: '1' is not an instance of <class 'int'> |
| assertAlmostEqual | 小数がほぼ等しい | assertAlmostEqual(0.1 + 0.2, 0.31) |
AssertionError: 0.30000000000000004 != 0.31 within 7 places (0.009999999999999953 difference) |
| assertGreater | より大きい | assertGreater(1, 2) |
AssertionError: 1 not greater than 2 |
| assertLessEqual | 以下 | assertLessEqual(3, 2) |
AssertionError: 3 not less than or equal to 2 |
| assertRaises | 例外が出る | with assertRaises(ValueError): int("10") |
AssertionError: ValueError not raised |
| assertRegex | 正規表現に合う | assertRegex("abc", r"\d+") |
AssertionError: Regex didn't match: '\\d+' not found in 'abc' |
| assertCountEqual | 順番を無視して要素が同じ | assertCountEqual([1, 2, 2], [2, 1, 1]) |
AssertionError: Element counts were not equal:(続けて差分) |
対になるメソッドとして、assertIsNotNone / assertIsNot / assertNotIn / assertNotIsInstance / assertNotAlmostEqual / assertLess / assertGreaterEqual / assertNotRegex もあります。使い方は上の表と同じです。
assertEqualはリストや辞書の差分も見せてくれる
assertEqual は型に合わせて差分を表示します。リストと辞書で試した結果です。
self.assertEqual([1, 2, 3], [1, 2, 4])
AssertionError: Lists differ: [1, 2, 3] != [1, 2, 4]
First differing element 2:
3
4
- [1, 2, 3]
? ^
+ [1, 2, 4]
? ^
self.assertEqual({"id": 1, "name": "tanaka"}, {"id": 1, "name": "suzuki"})
AssertionError: {'id': 1, 'name': 'tanaka'} != {'id': 1, 'name': 'suzuki'}
- {'id': 1, 'name': 'tanaka'}
? ^^^^ ^
+ {'id': 1, 'name': 'suzuki'}
? ^^^^ ^
どの要素が違うかまで出るので、assertTrue(a == b) より assertEqual(a, b) を選ぶと原因を追いやすくなります。 assertTrue(a == b) だと、表示は False is not true だけになります。
小数はassertAlmostEqualで比べる
0.1 + 0.2 は 2進数の都合で 0.30000000000000004 になります。assertEqual だと次のように止まります。
AssertionError: 0.30000000000000004 != 0.3
assertAlmostEqual(0.1 + 0.2, 0.3) に変えると通ります。既定では小数点以下7桁に丸めて比べ、places=3 のように桁数を、delta=0.01 のように許容する差を指定できます。
msg引数で「何を確かめているか」を添える
すべての assert メソッドは msg を受け取れます。
self.assertEqual(add_tax(100), 110, msg="税込み計算を確認")
AssertionError: 108 != 110 : 税込み計算を確認
元のメッセージの後ろに : 区切りで追加されます。
例外・ログ・警告を確かめる
import logging
import unittest
import warnings
class TestContext(unittest.TestCase):
def test_raises_message(self):
with self.assertRaises(ValueError) as cm:
int("x")
self.assertIn("invalid literal", str(cm.exception))
def test_raises_regex(self):
with self.assertRaisesRegex(ValueError, "invalid literal"):
int("x")
def test_logs(self):
with self.assertLogs("app", level="INFO") as cm:
logging.getLogger("app").info("started")
self.assertEqual(cm.output, ["INFO:app:started"])
def test_warns(self):
with self.assertWarns(DeprecationWarning):
warnings.warn("old", DeprecationWarning)
| メソッド | 確かめること |
|---|---|
| assertRaises(例外) | with の中でその例外が出る。cm.exception で例外オブジェクトを取り出せる |
| assertRaisesRegex(例外, 正規表現) | 例外の種類に加えて、メッセージが正規表現に合う |
| assertLogs(ロガー名, level) | 指定レベル以上のログが1件以上出る。cm.output は レベル:ロガー名:本文 のリスト |
| assertWarns(警告) | その警告が出る |
assertRaisesRegex・assertLogs・assertWarns は、同じ形のコードを Python 3.11.15 で動かして通ることを確かめています。
setUp・tearDown・setUpClassの実行順
テストの前後に行う準備と後片付けを、unittest ではフィクスチャと呼びます。どの順番で呼ばれるかを print で確かめます。
# test_order.py
import unittest
def setUpModule():
print("setUpModule")
def tearDownModule():
print("tearDownModule")
class TestOrder(unittest.TestCase):
@classmethod
def setUpClass(cls):
print(" setUpClass")
@classmethod
def tearDownClass(cls):
print(" tearDownClass")
def setUp(self):
print(" setUp")
self.items = ["pen"]
self.addCleanup(print, " cleanup")
def tearDown(self):
print(" tearDown")
def test_a(self):
print(" test_a", self.items)
self.items.append("note")
def test_b(self):
print(" test_b", self.items)
python -m unittest test_order
setUpModule
setUpClass
setUp
test_a ['pen']
tearDown
cleanup
. setUp
test_b ['pen']
tearDown
cleanup
. tearDownClass
tearDownModule
----------------------------------------------------------------------
Ran 2 tests in 0.002s
OK
出力の途中にある . は、テスト1件が通ったことを表す印です。この結果から次のことが分かります。
- setUp と tearDown は、テストメソッド1つごとに毎回呼ばれます。
- test_a で
self.itemsに "note" を足しても、test_b では['pen']に戻っています。テストごとに新しいインスタンスが作られ、setUp がやり直されるので、テスト同士が影響し合いません。 - setUpClass / tearDownClass はクラスにつき1回、setUpModule / tearDownModule はファイルにつき1回です。
addCleanupで登録した処理は、tearDown の後に実行されます。
flowchart TD
A[setUpModule] --> B[setUpClass]
B --> C[setUp]
C --> D[test_a]
D --> E[tearDown]
E --> F[addCleanupで登録した処理]
F --> G[setUp]
G --> H[test_b]
H --> I[tearDown]
I --> J[addCleanupで登録した処理]
J --> K[tearDownClass]
K --> L[tearDownModule]
どのフィクスチャを使うかの判断表
| フィクスチャ | 呼ばれる回数 | 向いている用途 |
|---|---|---|
| setUp / tearDown | テストメソッドごと | テスト用データの用意、一時ファイル、毎回まっさらにしたいもの |
| setUpClass / tearDownClass | クラスごとに1回 | 作るのに時間がかかり、読み取りだけに使うもの |
| setUpModule / tearDownModule | ファイルごとに1回 | ファイル全体で共有する準備 |
| addCleanup | 登録した数だけ | setUp の途中で確保したものを確実に片付けたいとき |
迷ったら setUp を選ぶと、テスト同士が独立して保てます。 setUpClass で作ったものをテストの中で書き換えると、実行順によって結果が変わることがあるためです。
setUpで例外が出るとtearDownは呼ばれない
# test_setup_err.py
import unittest
class TestSetupError(unittest.TestCase):
def setUp(self):
print("setUp")
raise RuntimeError("準備でエラー")
def tearDown(self):
print("tearDown")
def test_a(self):
print("test_a")
setUp
E
======================================================================
ERROR: test_a (test_setup_err.TestSetupError.test_a)
----------------------------------------------------------------------
Traceback (most recent call last):
raise RuntimeError("準備でエラー")
RuntimeError: 準備でエラー
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (errors=1)
(Traceback のうち File の行は省略しています。)tearDown も test_a も出力されていません。setUp が途中で止まると tearDown は呼ばれないので、片付けが必要なものは確保した直後に addCleanup で登録しておくと安心です。
import tempfile
import unittest
from pathlib import Path
class TestFile(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup) # 確保した直後に片付けを登録
self.path = Path(self.tmp.name) / "memo.txt"
def test_write(self):
self.path.write_text("hello", encoding="utf-8")
self.assertEqual(self.path.read_text(encoding="utf-8"), "hello")
mockとpatchの使い方
外部API・現在時刻・メール送信のように、テストのたびに本物を動かしにくい処理は、unittest.mock で差し替えます。これも標準ライブラリです。
Mockの基本(戻り値と呼び出しの記録)
from unittest.mock import Mock, call
m = Mock(return_value=10)
print(m(1, key="x"), m.call_count, m.call_args)
# 10 1 call(1, key='x')
m = Mock(side_effect=[1, 2, ValueError("3回目")])
print(m(), m())
# 1 2 (3回目の呼び出しでは ValueError が出る)
m = Mock()
m("a")
m("b")
m.assert_has_calls([call("a"), call("b")])
print(m.call_args_list)
# [call('a'), call('b')]
| 属性・メソッド | 役割 |
|---|---|
return_value |
呼ばれたときに返す値 |
side_effect |
リストなら順番に返す。例外を置くとその回で送出。関数を渡すと代わりに実行 |
call_count |
呼ばれた回数 |
call_args / call_args_list |
最後の呼び出しの引数 / すべての呼び出しの引数 |
assert_called() |
1回以上呼ばれた |
assert_called_once() |
ちょうど1回呼ばれた |
assert_called_with(引数) |
最後の呼び出しがその引数 |
assert_called_once_with(引数) |
ちょうど1回、その引数で呼ばれた |
assert_not_called() |
一度も呼ばれていない |
assert_has_calls([call(...), ...]) |
その並びの呼び出しが含まれる |
patchは「使われている場所」を指定する
patch でいちばんつまずきやすいのが、どこをパッチするかです。次の構成で再現します。
sample/
├── shop/
│ ├── __init__.py
│ ├── api.py
│ └── order.py
└── tests/
├── __init__.py
├── test_order_ng.py
└── test_order_ok.py
# shop/api.py
def fetch_price(item_id):
raise ConnectionError("外部APIには接続しません")
# shop/order.py
from shop.api import fetch_price
def total(item_id, qty):
return fetch_price(item_id) * qty
total をテストしたいので、fetch_price を 120 を返す Mock に差し替えます。まず、定義されている場所(shop.api)をパッチした例です。
# tests/test_order_ng.py(うまくいかない例)
import unittest
from unittest.mock import patch
from shop.order import total
class TestTotalNg(unittest.TestCase):
@patch("shop.api.fetch_price", return_value=120)
def test_total(self, mock_fetch):
self.assertEqual(total("A-1", 3), 360)
python -m unittest tests.test_order_ng
E
======================================================================
ERROR: test_total (tests.test_order_ng.TestTotalNg.test_total)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/lib/python3.11/unittest/mock.py", line 1378, in patched
return func(*newargs, **newkeywargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/taro/sample/tests/test_order_ng.py", line 10, in test_total
self.assertEqual(total("A-1", 3), 360)
^^^^^^^^^^^^^^^
File "/home/taro/sample/shop/order.py", line 5, in total
return fetch_price(item_id) * qty
^^^^^^^^^^^^^^^^^^^^
File "/home/taro/sample/shop/api.py", line 2, in fetch_price
raise ConnectionError("外部APIには接続しません")
ConnectionError: 外部APIには接続しません
----------------------------------------------------------------------
Ran 1 test in 0.003s
FAILED (errors=1)
パッチしたはずなのに、本物の fetch_price が呼ばれています。理由は、shop/order.py が from shop.api import fetch_price を実行した時点で、order モジュールの中に fetch_price という名前が別に作られているからです。shop.api.fetch_price を差し替えても、shop.order.fetch_price は元の関数を指したままになります。
直し方は、使われている場所(shop.order)を指定することです。
# tests/test_order_ok.py(直した後)
import unittest
from unittest.mock import patch
from shop.order import total
class TestTotalOk(unittest.TestCase):
@patch("shop.order.fetch_price", return_value=120)
def test_total(self, mock_fetch):
self.assertEqual(total("A-1", 3), 360)
mock_fetch.assert_called_once_with("A-1")
test_total (tests.test_order_ok.TestTotalOk.test_total) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
| 使う側の import の書き方 | patch に書く文字列 |
|---|---|
from shop.api import fetch_price(order.py 内) |
"shop.order.fetch_price" |
import shop.api として shop.api.fetch_price(...) と呼ぶ |
"shop.api.fetch_price" |
from shop import api として api.fetch_price(...) と呼ぶ |
"shop.api.fetch_price" |
「テスト対象のコードが、その名前をどこから探すか」を考えると決まります。
patchの3つの書き方
from unittest.mock import patch
# 1. デコレータ:テストメソッド全体で差し替える
@patch("shop.order.fetch_price", return_value=120)
def test_total(self, mock_fetch):
...
# 2. with 文:ブロックの中だけ差し替える
def test_context_manager(self):
with patch("shop.order.fetch_price") as mock_fetch:
mock_fetch.return_value = 50
print(total("B-2", 2)) # 100
mock_fetch.assert_called_once_with("B-2")
# 3. setUp で開始し、addCleanup で戻す:クラスの全テストで使う
def setUp(self):
patcher = patch("shop.order.fetch_price", return_value=120)
self.mock_fetch = patcher.start()
self.addCleanup(patcher.stop)
3つとも実行して通ることを確認しています(2の print は 100 でした)。デコレータを複数重ねたときは、下に書いたデコレータの Mock が先の引数になります。
呼び出しの検証で見るメッセージ
引数が期待と違うと、期待と実際が並んで表示されます。
with patch("shop.order.fetch_price", return_value=50) as mock_fetch:
total("B-2", 2)
mock_fetch.assert_called_once_with("B-9")
AssertionError: expected call not found.
Expected: fetch_price('B-9')
Actual: fetch_price('B-2')
メソッド名を打ち間違えた場合も、Python 3.11 では教えてくれます。
m = Mock()
m("a")
m.assert_called_once_wiht("zzz") # with の綴りが違う
AttributeError: 'assert_called_once_wiht' is not a valid assertion. Use a spec for the mock if 'assert_called_once_wiht' is meant to be an attribute.
autospecで引数の数も確かめる
Mock はどんな引数でも受け取るので、本物と違う呼び方をしても通ってしまいます。autospec=True を付けると、本物の関数と同じ引数の形を守るようになります。
with patch("shop.order.fetch_price", autospec=True) as mock_fetch:
mock_fetch("A-1", "extra") # 本物は引数1つ
TypeError: too many positional arguments
subTestで複数の入力をまとめて確かめる
同じ確認を入力を変えて繰り返すときは subTest が便利です。for 文だけで書くと最初に止まった入力しか分かりませんが、subTest で囲むと最後まで回り、通らなかった入力がすべて表示されます。
# tests/test_sub.py
import unittest
def is_even(n):
return n % 2 == 0
class TestSub(unittest.TestCase):
def test_even(self):
for n in [0, 2, 3, 4, 7]:
with self.subTest(n=n):
self.assertTrue(is_even(n))
FF
======================================================================
FAIL: test_even (tests.test_sub.TestSub.test_even) (n=3)
----------------------------------------------------------------------
AssertionError: False is not true
======================================================================
FAIL: test_even (tests.test_sub.TestSub.test_even) (n=7)
----------------------------------------------------------------------
AssertionError: False is not true
----------------------------------------------------------------------
Ran 1 test in 0.003s
FAILED (failures=2)
(Traceback の行は省略しています。)見出しに (n=3) (n=7) と付くので、どの入力で止まったかがひと目で分かります。テストの数は 1 件のまま数えられます。
skipで一時的に実行を外す
# tests/test_skip.py
import sys
import unittest
class TestSkip(unittest.TestCase):
@unittest.skip("外部サービスの準備ができてから実行")
def test_later(self):
self.fail("ここは実行されません")
@unittest.skipIf(sys.platform.startswith("linux"), "Linuxでは対象外")
def test_not_linux(self):
pass
@unittest.skipUnless(sys.version_info >= (3, 11), "3.11以降が必要")
def test_new_python(self):
pass
@unittest.expectedFailure
def test_known_issue(self):
self.assertEqual(0.1 + 0.2, 0.3)
def test_runtime_skip(self):
self.skipTest("実行中に条件を見てスキップ")
python -m unittest -v tests.test_skip
test_known_issue (tests.test_skip.TestSkip.test_known_issue) ... expected failure
test_later (tests.test_skip.TestSkip.test_later) ... skipped '外部サービスの準備ができてから実行'
test_new_python (tests.test_skip.TestSkip.test_new_python) ... ok
test_not_linux (tests.test_skip.TestSkip.test_not_linux) ... skipped 'Linuxでは対象外'
test_runtime_skip (tests.test_skip.TestSkip.test_runtime_skip) ... skipped '実行中に条件を見てスキップ'
----------------------------------------------------------------------
Ran 5 tests in 0.001s
OK (skipped=3, expected failures=1)
| 書き方 | 動き |
|---|---|
@unittest.skip("理由") |
必ずスキップ |
@unittest.skipIf(条件, "理由") |
条件が真ならスキップ |
@unittest.skipUnless(条件, "理由") |
条件が偽ならスキップ |
self.skipTest("理由") |
テストの途中でスキップ |
@unittest.expectedFailure |
通らないことを想定。通らなければ全体は OK のまま |
理由の文字列は実行結果にそのまま出るので、「いつ戻すか」が分かる言葉にしておくと後で助かります。
discoverでフォルダごと実行する
テストが増えたら tests フォルダにまとめ、discover で一括実行します。
python -m unittest discover -s tests -p "test_o*.py" -v
今回の構成では、test_order_ng.py と test_order_ok.py の2ファイルが対象になり、末尾は次のようになりました。
----------------------------------------------------------------------
Ran 2 tests in 0.002s
FAILED (errors=1)
| オプション | 意味 | 既定値 |
|---|---|---|
-s |
探し始めるフォルダ | .(カレント) |
-p |
テストファイル名のパターン | test*.py |
-t |
import の基準にする最上位フォルダ | -s と同じ |
-v |
詳細表示 | なし |
discover でテストが見つからないときは、次の表を上から確認します。
| 確認すること | 直し方 |
|---|---|
ファイル名が test で始まっているか |
calc_test.py のような名前なら -p "*_test.py" を付ける |
メソッド名が test で始まっているか |
check_add → test_add に変える |
クラスが unittest.TestCase を継承しているか |
継承を追加する |
サブフォルダに __init__.py があるか |
空のファイルを置くと確実に見つかる |
| テスト対象を import できる場所で実行しているか | 最上位のフォルダで実行する。ModuleNotFoundError が出たらここを見直す |
非同期の関数をテストする
async def の関数は unittest.IsolatedAsyncioTestCase でテストできます。
# test_async.py
import asyncio
import unittest
async def fetch_name():
await asyncio.sleep(0.01)
return "tanaka"
class TestAsync(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
self.prefix = "user:"
async def test_fetch(self):
self.assertEqual(self.prefix + await fetch_name(), "user:tanaka")
test_fetch (test_async.TestAsync.test_fetch) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.018s
OK
unittestとpytestの違い
Python のテストでは、外部ライブラリの pytest もよく使われます。pytest は unittest で書いたテストをそのまま実行できるので、どちらかを選んだら戻れない、という関係ではありません。上の test_calc.py と test_result.py を pytest 9.1.1 で実行した結果の末尾です。
python -m pytest test_calc.py test_result.py -q
def test_fail(self):
> self.assertEqual(add(2, 3), 6)
E AssertionError: 5 != 6
test_result.py:11: AssertionError
=========================== short test summary info ============================
FAILED test_result.py::TestResult::test_error - ValueError: 0では割れません
FAILED test_result.py::TestResult::test_fail - AssertionError: 5 != 6
2 failed, 4 passed in 0.05s
pytest では FAIL と ERROR の区別がなく、どちらも failed と表示されました。
| 比べる点 | unittest | pytest |
|---|---|---|
| 入手 | 標準ライブラリ(追加インストールなし) | pip install pytest が必要 |
| テストの書き方 | TestCase を継承したクラスのメソッド | 関数でもクラスでも書ける |
| 確認の書き方 | self.assertEqual(a, b) などのメソッド |
assert a == b |
| 準備と後片付け | setUp / tearDown / setUpClass | fixture(関数の引数で受け取る) |
| 入力を変えて繰り返す | subTest |
@pytest.mark.parametrize |
| モック | unittest.mock(標準) |
unittest.mock をそのまま使える。monkeypatch もある |
| スキップ | @unittest.skip |
@pytest.mark.skip |
| 実行コマンド | python -m unittest |
pytest |
| 命名の決まり | メソッドは camelCase(setUp、assertEqual) | snake_case |
| こんなとき | 選び方の目安 |
|---|---|
| 外部ライブラリを入れずに始めたい | unittest |
| 標準ライブラリだけで配布するツールのテスト | unittest |
| 既に unittest のテストがたくさんある | そのまま続ける。pytest で実行することもできる |
| 短く書きたい、プラグインを使いたい | pytest |
unittest で覚えた考え方(準備・実行・確認、モック、スキップ)は、pytest でもそのまま生きます。
つまずきやすい点と直し方
| 起きること | 理由 | 直し方 |
|---|---|---|
Ran 0 tests と表示される |
ファイル名・メソッド名が test で始まっていない |
名前を test_ で始める |
| テストの順番が書いた順と違う | メソッド名のアルファベット順に実行される | 順番に頼らず、各テストを独立させる |
| patch したのに本物が呼ばれる | 定義側をパッチしている | 使われている側のモジュール名を指定する |
| 小数の比較が通らない | 2進数の誤差 | assertAlmostEqual を使う |
assertTrue(a == b) の表示が False is not true だけ |
比較の中身が渡っていない | assertEqual(a, b) に変える |
| setUp で止まると後片付けが動かない | tearDown は setUp が成功したときだけ呼ばれる | addCleanup を使う |
| 前のテストの影響で結果が変わる | setUpClass で作ったものを書き換えている | setUp に移して毎回作り直す |
よくある質問
Q. unittest.main() は書いたほうがよいですか?
A. python -m unittest で実行するなら、なくても動きます。python test_calc.py のようにファイルを直接実行したい場合に書いておくと便利です。
Q. assertEqual の引数は、どちらが期待値ですか?
A. unittest としての決まりはありません。メッセージは 1つ目 != 2つ目 の順で出るので、「実際の値, 期待する値」の順にそろえると読みやすくなります。チームで順番を決めておくと、表示の読み違いが減ります。
Q. テストの実行に時間がかかるものを見つけるには?
A. Python 3.12 以降には --durations オプションがあり、時間のかかったテストを表示できます(この記事の検証環境は 3.11 のため未実行です)。3.11 以前では、pytest の --durations=5 で同じことができます。
Q. print の出力が邪魔なときは?
A. python -m unittest -b を付けると、通ったテストの標準出力を隠せます。上の test_order.py に -b を付けて実行すると、print の行が消えて、.. と Ran 2 tests、OK だけが表示されました。
まとめ
- unittest は標準ライブラリ。
unittest.TestCaseを継承し、testで始まるメソッドを書いてpython -m unittest -vで実行する - FAIL は assert の期待と違ったとき、ERROR はそれ以外の例外。1件でもあると終了コードは 1
- assertEqual は差分まで表示する。小数は assertAlmostEqual、例外は assertRaises
- setUp / tearDown はテストごとに毎回、setUpClass はクラスに1回。片付けは addCleanup が確実
- patch は「使われている場所」を指定する。
from A import fと書いたモジュール側の名前を差し替える - subTest で通らなかった入力を全部表示、skip は理由つきで一時的に外す
- pytest は unittest のテストをそのまま実行できるので、後から移ることもできる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python unittestの使い方|assert一覧とsetUp・mock」
株式会社RJCでは、Pythonのテストの書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。