標準ライブラリunittestをマスターして、堅牢なPythonアプリケーション開発を目指しましょう。
はじめに: なぜテストが重要なのか?
ソフトウェア開発において、テストは品質を担保し、予期せぬバグや問題を早期に発見するために不可欠なプロセスです。特に、機能追加やリファクタリングを行った際に、既存の機能が壊れていないこと(デグレードしていないこと)を確認するためには、自動化されたテストが非常に有効です。
Pythonには標準で unittest
というテストフレームワークが組み込まれており、特別なライブラリをインストールすることなく、すぐにユニットテスト(単体テスト)を書き始めることができます。ユニットテストは、関数やメソッドといったプログラムの最小単位が期待通りに動作するかを確認するテストです。
この記事では、Pythonの unittest
ライブラリの基本的な使い方から、セットアップ・ティアダウン、テストのスキップ、モックを使った高度なテストテクニック、そしてテストを書く上でのベストプラクティスまで、幅広く解説していきます。さあ、一緒に unittest
の世界を探求しましょう! ✨
第1章: unittestの基本概念
unittest
を使いこなすためには、まずいくつかの基本的な概念を理解する必要があります。
1.1 テストケース (`unittest.TestCase`)
テストの基本単位は テストケース と呼ばれ、unittest.TestCase
クラスを継承して作成します。このクラスの中に、個々のテストロジックをメソッドとして実装していきます。
import unittest
class MyFirstTests(unittest.TestCase):
# ここにテストメソッドを定義していく
pass
1.2 テストメソッド
テストケースクラス内に定義される、実際のテスト処理を行うメソッドです。テストメソッドの名前は、慣習的に test_
で始まる必要があります。この命名規則に従うことで、テストランナーが自動的にテストメソッドを認識してくれます。
例えば、簡単な足し算を行う関数 add(a, b)
をテストする場合、以下のようにテストメソッドを定義します。
# calculator.py (テスト対象のコード)
def add(a, b):
return a + b
# test_calculator.py (テストコード)
import unittest
from calculator import add # テスト対象の関数をインポート
class CalculatorTests(unittest.TestCase):
def test_add_positive_numbers(self):
# このメソッドがテストメソッド
result = add(2, 3)
# アサーションを使って結果を検証 (詳細は後述)
self.assertEqual(result, 5)
def test_add_negative_numbers(self):
result = add(-1, -5)
self.assertEqual(result, -6)
# Pythonスクリプトとして直接実行した場合にテストを実行するおまじない
if __name__ == '__main__':
unittest.main()
上記の例では、test_add_positive_numbers
と test_add_negative_numbers
がテストメソッドです。
1.3 アサーションメソッド
アサーション (Assertion) は、「表明」や「断言」といった意味で、テストの結果が期待通りであるかを検証するためのメソッドです。unittest.TestCase
クラスは、様々なアサーションメソッドを提供しています。
よく使われるアサーションメソッドの例をいくつか見てみましょう。
メソッド | 説明 | 使用例 |
---|---|---|
assertEqual(a, b) |
a と b が等しいか検証します。 |
self.assertEqual(add(2, 3), 5) |
assertNotEqual(a, b) |
a と b が等しくないか検証します。 |
self.assertNotEqual(add(2, 3), 6) |
assertTrue(x) |
x が True であるか検証します。 |
self.assertTrue(is_valid(data)) |
assertFalse(x) |
x が False であるか検証します。 |
self.assertFalse(is_empty(my_list)) |
assertIs(a, b) |
a と b が同一のオブジェクトであるか検証します (a is b )。 |
self.assertIs(instance1, instance1) |
assertIsNot(a, b) |
a と b が同一のオブジェクトでないか検証します (a is not b )。 |
self.assertIsNot(instance1, instance2) |
assertIsNone(x) |
x が None であるか検証します。 |
self.assertIsNone(get_optional_value()) |
assertIsNotNone(x) |
x が None でないか検証します。 |
self.assertIsNotNone(get_required_value()) |
assertIn(a, b) |
a がコンテナ b に含まれているか検証します (a in b )。 |
self.assertIn(element, my_list) |
assertNotIn(a, b) |
a がコンテナ b に含まれていないか検証します (a not in b )。 |
self.assertNotIn(element, another_list) |
assertIsInstance(a, b) |
a がクラス b のインスタンスであるか検証します (isinstance(a, b) )。 |
self.assertIsInstance(my_obj, MyClass) |
assertNotIsInstance(a, b) |
a がクラス b のインスタンスでないか検証します (not isinstance(a, b) )。 |
self.assertNotIsInstance(my_obj, AnotherClass) |
assertRaises(exception, callable, *args, **kwds) |
特定の例外が発生するか検証します。callable に引数を渡して呼び出した際に、exception が送出されることを確認します。コンテキストマネージャとしても使用可能です。 |
with self.assertRaises(ValueError): divide(10, 0) |
assertRaisesRegex(exception, regex, callable, *args, **kwds) |
特定の例外が発生し、そのエラーメッセージが正規表現 regex にマッチするか検証します。コンテキストマネージャとしても使用可能です。 |
with self.assertRaisesRegex(ValueError, "zero"): divide(10, 0) |
assertAlmostEqual(a, b, places=7) |
a と b が指定した小数点以下の桁数 (places ) までほぼ等しいか検証します。浮動小数点数の比較に便利です。 |
self.assertAlmostEqual(0.1 + 0.2, 0.3) |
assertNotAlmostEqual(a, b, places=7) |
a と b が指定した小数点以下の桁数までほぼ等しくないか検証します。 |
self.assertNotAlmostEqual(0.1 + 0.2, 0.31) |
これらのアサーションメソッドを適切に使うことで、テストの意図を明確にし、コードの動作を正確に検証できます。
1.4 テストスイート (`unittest.TestSuite`)
テストスイートは、複数のテストケースや他のテストスイートをまとめたものです。これにより、関連するテストをグループ化して一度に実行できます。
通常、テストランナーが自動的にテストケースやモジュールからテストスイートを構築してくれるため、自分で明示的に TestSuite
オブジェクトを作成する機会は少ないかもしれません。しかし、特定のテストだけを実行したい場合や、複雑なテスト構成を管理したい場合には便利です。
import unittest
from test_calculator import CalculatorTests # 上で作ったテストケース
from test_another_module import AnotherModuleTests # 別のテストケース
def create_suite():
suite = unittest.TestSuite()
# CalculatorTestsから特定のテストメソッドだけを追加
suite.addTest(CalculatorTests('test_add_positive_numbers'))
# AnotherModuleTestsの全てのテストメソッドを追加
suite.addTest(unittest.makeSuite(AnotherModuleTests))
return suite
if __name__ == '__main__':
runner = unittest.TextTestRunner()
runner.run(create_suite())
1.5 テストランナー (`unittest.TextTestRunner`)
テストランナーは、テストスイートを受け取り、その中のテストを実行し、結果を収集してユーザーに報告するコンポーネントです。unittest
には、テキストベースの出力を行う TextTestRunner
が標準で用意されています。
テストファイル内で unittest.main()
を呼び出すと、内部的に TextTestRunner
が使われ、そのファイル内のテストが実行されます。テスト結果は、成功したテストは .
(ドット)、失敗したテストは F
、エラーが発生したテストは E
として表示されます。
第2章: テストの実行と構成
テストコードを書いたら、次はそれを実行し、テスト環境を整える方法を見ていきましょう。
2.1 コマンドラインからの実行
テストを実行する最も一般的な方法は、コマンドラインから unittest
モジュールを利用することです。
2.1.1 テストディスカバリ (Test Discovery)
プロジェクト内のテストファイルを自動的に見つけて実行する機能です。これが最も推奨される方法です。プロジェクトのルートディレクトリで以下のコマンドを実行します。
python -m unittest discover
このコマンドは、カレントディレクトリ以下を再帰的に探索し、デフォルトでは test*.py
というパターンに一致するファイルを探し、その中の unittest.TestCase
サブクラスを見つけて実行します。
探索を開始するディレクトリやファイル名のパターンはオプションで指定できます。
# testsディレクトリから探索を開始
python -m unittest discover -s tests
# *_test.py というパターンのファイルを探す
python -m unittest discover -p '*_test.py'
# testsディレクトリから探索を開始し、*_test.py パターンのファイルを探す
python -m unittest discover -s tests -p '*_test.py'
2.1.2 特定のファイルやクラス、メソッドを実行
特定のテストだけを実行したい場合もコマンドラインから指定できます。
# test_calculator.py ファイル内の全てのテストを実行
python -m unittest test_calculator
# test_calculator.py 内の CalculatorTests クラスのテストを実行
python -m unittest test_calculator.CalculatorTests
# test_calculator.py 内の CalculatorTests クラスの test_add_positive_numbers メソッドを実行
python -m unittest test_calculator.CalculatorTests.test_add_positive_numbers
また、テストファイル自体を直接実行することも可能です(ファイル内に if __name__ == '__main__': unittest.main()
が記述されている場合)。
python test_calculator.py
ただし、大規模なプロジェクトではテストディスカバリを使う方が管理しやすいでしょう。
2.2 テストのセットアップとティアダウン
テストを実行する前後に、特定の準備処理や後片付け処理を行いたい場合があります。例えば、データベース接続の確立と切断、一時ファイルの作成と削除などです。unittest
では、そのための特別なメソッドが用意されています。
2.2.1 メソッドレベルのセットアップ/ティアダウン
setUp()
: 各テストメソッドの実行直前に呼び出されます。tearDown()
: 各テストメソッドの実行直後(成功、失敗、エラーに関わらず)に呼び出されます。
import unittest
import os
class FileOperationTests(unittest.TestCase):
def setUp(self):
# 各テスト前に一時ファイルを作成
print("\nRunning setUp...")
self.filepath = 'test_temp_file.txt'
with open(self.filepath, 'w') as f:
f.write("Initial content.")
def tearDown(self):
# 各テスト後に一時ファイルを削除
print("Running tearDown...")
if os.path.exists(self.filepath):
os.remove(self.filepath)
def test_read_file(self):
print("Running test_read_file...")
with open(self.filepath, 'r') as f:
content = f.read()
self.assertEqual(content, "Initial content.")
def test_write_file(self):
print("Running test_write_file...")
with open(self.filepath, 'w') as f:
f.write("New content.")
with open(self.filepath, 'r') as f:
content = f.read()
self.assertEqual(content, "New content.")
if __name__ == '__main__':
unittest.main()
このテストを実行すると、test_read_file
の前後に setUp
と tearDown
が実行され、test_write_file
の前後にも、再度 setUp
と tearDown
が実行されます。これにより、各テストは独立した状態(クリーンな状態)で開始されます。
2.2.2 クラスレベルのセットアップ/ティアダウン
テストクラス内の全てのテストメソッドを実行する前後に、一度だけ実行したい処理がある場合は、クラスメソッドを使用します。
@classmethod setUpClass(cls)
: クラス内の最初のテストメソッド実行前に一度だけ呼び出されます。@classmethod tearDownClass(cls)
: クラス内の最後のテストメソッド実行後に一度だけ呼び出されます。
これらのメソッドは、セットアップやティアダウンに時間のかかる処理(例: データベース接続プールやWebサーバーの起動/停止)に適しています。
import unittest
import time
class ExpensiveSetupTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
print("\nRunning setUpClass...")
# 時間のかかる初期化処理 (例: 外部サービス接続)
cls.shared_resource = "Expensive Resource Initialized at {}".format(time.time())
time.sleep(1) # 時間がかかることをシミュレート
@classmethod
def tearDownClass(cls):
print("Running tearDownClass...")
# 時間のかかる後片付け処理
print("Releasing: {}".format(cls.shared_resource))
time.sleep(1)
def setUp(self):
# 各テスト前の軽い準備
print(" Running setUp...")
self.start_time = time.time()
def tearDown(self):
# 各テスト後の軽い後始末
print(f" Test duration: {time.time() - self.start_time:.4f} seconds")
print(" Running tearDown...")
def test_feature_one(self):
print(" Running test_feature_one...")
self.assertIn("Initialized", self.shared_resource)
time.sleep(0.2)
def test_feature_two(self):
print(" Running test_feature_two...")
self.assertTrue(self.shared_resource.startswith("Expensive Resource"))
time.sleep(0.3)
if __name__ == '__main__':
unittest.main()
実行結果を見ると、setUpClass
と tearDownClass
がそれぞれ一度しか呼ばれていないことがわかります。
💡 setUp/tearDown
と setUpClass/tearDownClass
を適切に使い分けることで、効率的で信頼性の高いテスト環境を構築できます。
第3章: 高度なテストテクニック
unittest
には、基本的なテスト機能に加えて、より柔軟なテストを実現するための高度な機能も用意されています。
3.1 テストのスキップ
特定の条件下でテストを実行したくない場合や、未実装の機能に対するテストを一時的に無効化したい場合があります。そのような場合にテストスキップ機能が役立ちます。
unittest
では、デコレータを使ってテストメソッドやテストクラス全体をスキップできます。
@unittest.skip(reason)
: 無条件にテストをスキップします。reason
にスキップする理由を記述します。@unittest.skipIf(condition, reason)
: 指定されたcondition
がTrue
の場合にテストをスキップします。@unittest.skipUnless(condition, reason)
: 指定されたcondition
がFalse
の場合にテストをスキップします(condition
がTrue
の場合のみ実行)。
import unittest
import sys
class SkipTests(unittest.TestCase):
@unittest.skip("この機能はまだ実装されていません")
def test_unimplemented_feature(self):
# このテストは常にスキップされる
self.fail("スキップされるはずなので、ここには到達しない")
@unittest.skipIf(sys.platform.startswith("win"), "Windowsでは動作しないためスキップ")
def test_linux_specific_feature(self):
# Windows環境で実行した場合にスキップされる
print("Linux/macOS specific test running...")
self.assertTrue(True) # 仮のアサーション
@unittest.skipUnless(sys.version_info >= (3, 8), "Python 3.8以上が必要です")
def test_requires_python38(self):
# Python 3.7以前の環境で実行した場合にスキップされる
print("Python 3.8+ specific test running...")
# Python 3.8以降の機能を使う処理
my_dict = {'a': 1, 'b': 2}
reversed_dict = dict(reversed(my_dict.items())) # 3.8+
self.assertEqual(reversed_dict, {'b': 2, 'a': 1})
def test_always_run(self):
# このテストは常に実行される
self.assertEqual(1 + 1, 2)
if __name__ == '__main__':
unittest.main(verbosity=2) # verbosity=2 で詳細表示
テスト実行結果では、スキップされたテストは s
と表示され、指定した理由も出力されます。
3.2 期待される失敗 (`@unittest.expectedFailure`)
現在バグがあることがわかっていて、修正されるまでの間、テストが失敗することを明示したい場合があります。このような場合、@unittest.expectedFailure
デコレータを使用します。
このデコレータを付けたテストが失敗した場合、テストランナーはそれを「期待された失敗 (expected failure: x
)」として扱い、テスト全体の結果には影響しません。もし、このデコレータが付いているにも関わらずテストが成功してしまった場合は、「予期せぬ成功 (unexpected success: u
)」として扱われ、テスト失敗とみなされます。これは、バグが修正された後にデコレータを削除し忘れるのを防ぐのに役立ちます。
import unittest
def buggy_function(x):
# 意図的にバグを含む関数 (0除算の可能性)
if x == 0:
return 1 / x # ZeroDivisionError
return x * 2
class ExpectedFailureTests(unittest.TestCase):
@unittest.expectedFailure
def test_buggy_function_with_zero(self):
# このテストはZeroDivisionErrorで失敗することが期待される
result = buggy_function(0)
# ここには到達しないはずだが、もし到達したらテストは「予期せぬ成功」となる
# self.assertEqual(result, 0) # 例えばこのように書いてもテストは'x'になる
def test_buggy_function_with_non_zero(self):
# こちらは正常に動作するはず
self.assertEqual(buggy_function(5), 10)
if __name__ == '__main__':
unittest.main(verbosity=2)
3.3 サブテスト (`with self.subTest(…)`)
一つのテストメソッド内で、複数の異なる入力や条件で同じロジックをテストしたい場合があります。従来の方法では、ループ内でアサーションを行い、どこかで失敗するとループ全体がそこで中断してしまい、後続のケースがテストされませんでした。
Python 3.4 で導入された サブテスト 機能を使うと、ループ内の各イテレーションを独立したテストケースのように扱えます。アサーションが失敗しても、ループは中断されずに最後まで実行され、失敗したサブテストが個別に報告されます。
with self.subTest(key=value, ...)
のようにコンテキストマネージャとして使用し、key=value
形式でパラメータを指定すると、失敗時にどのパラメータで失敗したかが分かりやすくなります。
import unittest
def power(base, exp):
return base ** exp
class SubTestExample(unittest.TestCase):
def test_power_function(self):
test_cases = [
(2, 2, 4),
(2, 3, 8),
(3, 2, 9),
(0, 5, 0),
(2, 0, 1),
(2, -1, 0.5), # 期待値が正しい
# (-2, 0.5, "ErrorExpected"), # 複素数になるケース (意図的に失敗させる例)
(5, 1, 5), # 意図的に失敗させる例 (期待値 5 だが 6 と比較)
]
for base, exp, expected in test_cases:
with self.subTest(base=base, exponent=exp):
# このブロック内のアサーションが失敗しても、ループは継続する
print(f" Testing: base={base}, exponent={exp}") # どのサブテストが実行中か確認用
# 意図的に失敗させるために、最後のケースの期待値を間違える
if base == 5 and exp == 1:
self.assertEqual(power(base, exp), 6, f"Failed for base={base}, exp={exp}")
elif isinstance(expected, str) and expected == "ErrorExpected":
with self.assertRaises(TypeError): # 例: 負数の分数乗
power(base, exp)
else:
self.assertEqual(power(base, exp), expected, f"Failed for base={base}, exp={exp}")
if __name__ == '__main__':
unittest.main(verbosity=2)
実行結果を見ると、(5, 1, 5)
のケースで失敗が報告されますが、その前のテスト ((2, -1, 0.5)
など) は問題なく実行・検証されていることがわかります。サブテストを使わない場合、最初の失敗でテストメソッド全体が停止してしまいます。
サブテストは、パラメータ化テストを簡潔に記述するのに非常に便利です。✅
第4章: モックを使ったテスト (`unittest.mock`)
ユニットテストは、テスト対象のコード単位(関数やメソッド)を隔離してテストすることが理想です。しかし、多くのコードは他のクラスやモジュール、外部サービス(データベース、APIなど)に依存しています。
これらの依存関係をそのままテストに含めてしまうと、以下のような問題が発生します。
- テストの実行が遅くなる(例: ネットワーク通信、データベースアクセス)
- テストが不安定になる(例: 外部サービスの障害、ネットワークエラー)
- テストのセットアップが複雑になる(例: テスト用データベースの準備)
- テスト対象のコード単体の問題を特定しにくくなる
そこで登場するのが モック (Mock) です。モックは、依存するオブジェクトの「偽物」や「スタブ」を作成し、本物の代わりにテストで使用するテクニックです。これにより、依存関係を切り離し、テスト対象のコードのロジックのみに集中してテストを行うことができます。
Pythonでは、標準ライブラリ unittest.mock
(Python 3.3以降)が強力なモック機能を提供しています。(それ以前のバージョンでは mock
ライブラリとして別途インストールが必要でした。)
4.1 モックの必要性: 具体例
例えば、外部の天気APIから現在の天気を取得し、それに応じてメッセージを返す関数を考えます。
# weather_service.py (外部APIと通信する想定のモジュール)
import requests
import random # 簡単化のためランダムに天気を返す
def get_current_weather(city):
# 本来はここで requests.get などでAPIを叩く
print(f" (Mock) Calling external API for {city}...")
# return requests.get(f"https://api.weather.example.com/?city={city}").json()
# 簡単化のためランダムな天気を返す
possible_weathers = ["Sunny", "Cloudy", "Rainy"]
weather_data = {
"city": city,
"temperature": random.randint(10, 30),
"description": random.choice(possible_weathers)
}
return weather_data
# greeter.py (テスト対象のコード)
from weather_service import get_current_weather
def generate_greeting(city):
try:
weather_info = get_current_weather(city)
description = weather_info.get("description")
if description == "Sunny":
return f"It's a sunny day in {city}! Enjoy! ☀️"
elif description == "Rainy":
return f"It's raining in {city}. Don't forget your umbrella! ☔"
else:
return f"Current weather in {city}: {description}."
except Exception as e:
# APIエラーなどのハンドリング
print(f"Error getting weather: {e}")
return f"Could not retrieve weather for {city}."
この generate_greeting
関数をテストしたい場合、get_current_weather
が毎回実際のAPI(またはこの例ではランダムな結果)を呼び出すと、テスト結果が安定しません。また、API呼び出しは時間がかかる可能性があります。
ここでモックを使い、get_current_weather
関数を偽の関数に置き換えます。
4.2 `Mock` オブジェクトと `MagicMock`
unittest.mock.Mock
は、最も基本的なモックオブジェクトです。呼び出しを記録したり、属性やメソッドの戻り値を設定したりできます。
unittest.mock.MagicMock
は Mock
のサブクラスで、マジックメソッド(__str__
, __len__
など)のデフォルト実装も提供するため、より多くの場面で使いやすいです。通常は MagicMock
を使うことが多いでしょう。
from unittest.mock import Mock, MagicMock
# Mockオブジェクトの作成
mock_obj = Mock()
# 属性の戻り値を設定
mock_obj.some_attribute = "Mocked Value"
print(mock_obj.some_attribute) # -> Mocked Value
# メソッドの戻り値を設定
mock_obj.some_method.return_value = 123
print(mock_obj.some_method()) # -> 123
# メソッドが呼び出されたか確認
mock_obj.some_method.assert_called_once() # 1回だけ呼ばれたか?
# mock_obj.some_method.assert_called_with(arg1, arg2) # 特定の引数で呼ばれたか?
# MagicMock はマジックメソッドも扱える
magic_mock = MagicMock()
magic_mock.__str__.return_value = "Magic Mock String"
print(str(magic_mock)) # -> Magic Mock String
# 未定義の属性やメソッドにアクセスすると、自動的に新しいMock/MagicMockが生成される
print(mock_obj.non_existent_method) # -> <Mock name='mock.non_existent_method' id='...'>
print(mock_obj.non_existent_method()) # -> <Mock name='mock.non_existent_method()' id='...'>
4.3 `patch` デコレータ/コンテキストマネージャ
特定のオブジェクト(関数、クラス、メソッドなど)をテスト期間中だけモックオブジェクトに置き換えるために unittest.mock.patch
を使います。これはデコレータとしても、コンテキストマネージャとしても利用できます。
patch
の第一引数には、置き換えたいオブジェクトを文字列で指定します。これは通常「モジュール名.オブジェクト名」の形式になります。
4.3.1 `patch` デコレータの使用例
先ほどの天気予報の例で get_current_weather
をモック化してみます。
import unittest
from unittest.mock import patch
from greeter import generate_greeting # テスト対象の関数
class GreeterTests(unittest.TestCase):
# 'greeter.get_current_weather' をモックに置き換える
# patchデコレータは、モックオブジェクトをテストメソッドの引数として渡す (順番に注意!)
@patch('greeter.get_current_weather')
def test_greeting_sunny(self, mock_get_weather): # 引数名は何でも良いが、分かりやすくする
# モックの設定: get_current_weather が呼び出されたら、特定の辞書を返すようにする
mock_get_weather.return_value = {
"city": "Tokyo",
"temperature": 25,
"description": "Sunny"
}
# テスト対象関数を実行
greeting = generate_greeting("Tokyo")
# アサーション: 期待通りのメッセージが生成されたか
self.assertEqual(greeting, "It's a sunny day in Tokyo! Enjoy! ☀️")
# モックが期待通りに呼び出されたか検証
mock_get_weather.assert_called_once_with("Tokyo")
@patch('greeter.get_current_weather')
def test_greeting_rainy(self, mock_get_weather):
mock_get_weather.return_value = {
"city": "Osaka",
"temperature": 18,
"description": "Rainy"
}
greeting = generate_greeting("Osaka")
self.assertEqual(greeting, "It's raining in Osaka. Don't forget your umbrella! ☔")
mock_get_weather.assert_called_once_with("Osaka")
@patch('greeter.get_current_weather')
def test_greeting_api_error(self, mock_get_weather):
# モックが例外を送出するように設定
mock_get_weather.side_effect = Exception("API Timeout")
greeting = generate_greeting("Fukuoka")
# エラー時のメッセージを確認
self.assertEqual(greeting, "Could not retrieve weather for Fukuoka.")
mock_get_weather.assert_called_once_with("Fukuoka")
if __name__ == '__main__':
unittest.main(verbosity=2)
💡 patch
を複数重ねて使う場合、デコレータの適用順序とテストメソッドの引数の順序は逆になります。一番内側のデコレータに対応するモックが、一番最初の引数になります。
@patch('module1.func1') # 2番目の引数 mock_func1 になる
@patch('module2.ClassA') # 1番目の引数 mock_class_a になる
def test_something(self, mock_class_a, mock_func1):
# ...
pass
4.3.2 `patch` コンテキストマネージャの使用例
テストメソッドの一部だけモックを適用したい場合は、with
文を使ったコンテキストマネージャ形式が便利です。
import unittest
from unittest.mock import patch
from greeter import generate_greeting
class GreeterContextManagerTests(unittest.TestCase):
def test_greeting_cloudy_with_context_manager(self):
# 'with' ブロック内でのみ get_current_weather がモックされる
with patch('greeter.get_current_weather') as mock_get_weather:
# モックの設定
mock_get_weather.return_value = {
"city": "Nagoya",
"temperature": 22,
"description": "Cloudy"
}
# テスト実行
greeting = generate_greeting("Nagoya")
# アサーション
self.assertEqual(greeting, "Current weather in Nagoya: Cloudy.")
mock_get_weather.assert_called_once_with("Nagoya")
# 'with' ブロックを抜けると、モックは解除され、元の関数に戻る (はず)
# 注意: ただし、元の関数が実際にAPIを叩くならテストでは実行しない方が良い
# print("\nOutside context manager:", generate_greeting("Sapporo")) # これは実際のAPIを呼ぶ可能性
if __name__ == '__main__':
unittest.main(verbosity=2)
4.4 `autospec` による仕様の強制
通常の Mock
や patch
では、存在しない属性やメソッドをモックに定義したり、元のオブジェクトと異なるシグネチャ(引数の数や種類)でメソッドを呼び出したりできてしまいます。これは、リファクタリングなどで元のコードのインターフェースが変わった場合に、テストが追随できずにパスしてしまう(偽陽性)原因となります。
autospec=True
オプションを使うと、モックが元のオブジェクトの仕様(存在する属性やメソッド、メソッドの引数シグネチャ)に基づいて作成されます。存在しない属性へのアクセスや、間違った引数でのメソッド呼び出しはエラーになります。
import unittest
from unittest.mock import patch, create_autospec
# 例のためのクラス
class RealClass:
def method(self, arg1, arg2):
return arg1 + arg2
class AutospecTests(unittest.TestCase):
# autospec=True を指定
@patch('__main__.RealClass', autospec=True)
def test_with_autospec(self, MockRealClass):
instance = MockRealClass() # モックインスタンスを作成
# 正しいシグネチャで呼び出し
instance.method.return_value = 10
result = instance.method('a', 'b')
self.assertEqual(result, 10)
instance.method.assert_called_once_with('a', 'b')
# 存在しないメソッドを呼び出そうとすると AttributeError
with self.assertRaises(AttributeError):
instance.non_existent_method()
# 間違った引数で呼び出そうとすると TypeError
with self.assertRaises(TypeError):
# RealClass.method は self 以外に 2 つの引数を取るが、1つしか渡していない
instance.method('only_one_arg')
# autospec=False (デフォルト) の場合
@patch('__main__.RealClass') # autospec=False
def test_without_autospec(self, MockRealClass):
instance = MockRealClass()
# 存在しないメソッドも呼び出せてしまう (新しいMockが返る)
print(instance.non_existent_method()) # -> <Mock name='mock.non_existent_method()' id='...'>
# 間違った引数でも呼び出せてしまう
instance.method.return_value = 5
result = instance.method('only_one_arg') # 本来 TypeError だが、モックなので通ってしまう
self.assertEqual(result, 5)
# これは成功してしまう!
instance.method.assert_called_once_with('only_one_arg')
# create_autospec を使う例 (クラス自体を置き換えるのではなく、仕様だけ使う)
def test_create_autospec(self):
# RealClassの仕様を持つモックを作成
mock_instance = create_autospec(RealClass, instance=True)
# 正しい呼び出し
mock_instance.method.return_value = 10
result = mock_instance.method(1, 2)
self.assertEqual(result, 10)
mock_instance.method.assert_called_once_with(1, 2)
# 間違った呼び出しはエラー
with self.assertRaises(TypeError):
mock_instance.method(1) # 引数が足りない
if __name__ == '__main__':
unittest.main(verbosity=2)
autospec=True
を積極的に利用することで、より信頼性の高い、リファクタリングに強いテストを書くことができます。🚀
4.5 モックのアサーション
モックオブジェクトは、自身がどのように使われたかを記録しています。unittest.mock
は、これらの記録を検証するための便利なアサーションメソッドを提供しています。
アサーションメソッド | 説明 |
---|---|
assert_called() |
モックが少なくとも1回呼び出されたか検証します。 |
assert_called_once() |
モックがちょうど1回だけ呼び出されたか検証します。 |
assert_not_called() |
モックが一度も呼び出されていないか検証します。 |
assert_called_with(*args, **kwargs) |
モックが最後に、指定された引数で呼び出されたか検証します。 |
assert_called_once_with(*args, **kwargs) |
モックがちょうど1回だけ、指定された引数で呼び出されたか検証します。 |
assert_any_call(*args, **kwargs) |
モックが少なくとも1回、指定された引数で呼び出されたことがあるか検証します(最後の呼び出しである必要はない)。 |
assert_has_calls(calls, any_order=False) |
指定された呼び出しリスト (unittest.mock.call オブジェクトのリスト) が、モックの呼び出し履歴に含まれているか検証します。any_order=True にすると順序を問いません。 |
これらのアサーションを適切に使うことで、テスト対象コードが依存オブジェクトを正しく利用しているかを確認できます。
from unittest.mock import Mock, call
mock = Mock()
mock(1, 2)
mock(key='value')
mock(3, 4, key='other')
# 検証
mock.assert_called()
# mock.assert_called_once() # -> これは失敗する (3回呼ばれている)
mock.assert_called_with(3, 4, key='other') # 最後の呼び出しを検証
mock.assert_any_call(1, 2) # (1, 2) で呼ばれたことがあるか
mock.assert_any_call(key='value') # (key='value') で呼ばれたことがあるか
expected_calls = [
call(1, 2),
call(key='value'),
call(3, 4, key='other')
]
mock.assert_has_calls(expected_calls) # 順番通りに呼ばれたか
expected_calls_any_order = [
call(key='value'),
call(1, 2),
]
mock.assert_has_calls(expected_calls_any_order, any_order=True) # 順不同で呼ばれたか
# mock.assert_not_called() # -> これは失敗する
unittest.mock
は非常に強力で多機能なライブラリです。テストにおける依存関係の問題を解決し、よりクリーンで信頼性の高いユニットテストを実現するために、ぜひマスターしましょう。
第5章: unittestのベストプラクティス
効果的なテストコードを書くためには、いくつかのベストプラクティスに従うことが推奨されます。
5.1 テストは独立させる (Isolation)
各テストメソッドは、他のテストメソッドの実行結果に依存しないように設計する必要があります。あるテストの実行が他のテストの成功/失敗に影響を与えるべきではありません。
setUp()
とtearDown()
を活用して、各テストの実行前に環境を初期化し、実行後にクリーンアップします。- テスト間で状態(グローバル変数、クラス変数など)を共有しないように注意します。どうしても必要な場合は、
setUpClass
やtearDownClass
を慎重に使用します。 - テストの実行順序に依存するようなテストは避けます。テストランナーは通常、テストメソッド名のアルファベット順に実行しますが、それに依存するべきではありません。
5.2 明確な命名規則
テストメソッドの名前は、そのテストが何を検証しているのかが一目でわかるように、明確で説明的なものにします。
test_<テスト対象の機能>_<条件や状態>_<期待される結果>
のようなパターンがよく使われます。
例:test_login_with_valid_credentials_succeeds
,test_division_by_zero_raises_error
- テストクラス名も、どのクラスやモジュールをテストしているかを示す名前にします。
例:UserModelTests
,AuthenticationServiceTests
良い名前は、テストが失敗したときに、問題の原因を素早く特定するのに役立ちます。
5.3 一つのテストメソッドでは一つのことをテストする (Single Responsibility)
一つのテストメソッド内であまりにも多くのことを検証しようとすると、テストが失敗したときに、どの部分に問題があったのかを特定するのが難しくなります。
- 理想的には、一つのテストメソッドは、一つの具体的な動作や条件、期待結果に焦点を当てるべきです。
- 関連する複数のアサーションを含むことは問題ありませんが、それらが全体として一つの論理的な関心事を検証しているようにします。
- 複雑なシナリオをテストする場合は、テストメソッドを分割するか、サブテスト (
subTest
) を使用することを検討します。
5.4 テストしやすいコードを書く (Testability)
テストの書きやすさは、元のコードの設計に大きく依存します。「テスト容易性 (Testability)」を意識してコードを書くことが重要です。
- 依存性の注入 (Dependency Injection): クラスや関数が必要とする依存オブジェクト(他のクラスのインスタンス、外部サービスへの接続など)を、外部から(コンストラクタやメソッドの引数として)渡すように設計します。これにより、テスト時にモックオブジェクトを簡単に注入できます。
- 関数/メソッドを小さく保つ: 一つの関数やメソッドが多くの責任を持ちすぎていると、テストが複雑になります。機能を小さな単位に分割します。
- 副作用を分離する: 計算ロジックと、状態を変更したり外部と通信したりする副作用(I/O操作、グローバル変数の変更など)を分離します。純粋な計算ロジックはテストしやすくなります。
- 明確なインターフェース: クラスやモジュールのインターフェース(公開メソッド、引数、戻り値)を明確に定義します。
5.5 網羅性を意識する (Coverage)
テストがコードのどの部分を実行しているか(コードカバレッジ)を測定し、テストの網羅性を高めることを目指します。
- 正常系のケースだけでなく、異常系のケース(不正な入力、境界値、エラー条件など)もテストします。
- Pythonの
coverage.py
などのツールを使って、テストカバレッジを計測できます。# coverage.py のインストール pip install coverage # カバレッジを計測しながらテストを実行 coverage run -m unittest discover # カバレッジレポートを表示 coverage report -m
- カバレッジ率 100% を盲目的に目指す必要はありませんが、重要なロジックや複雑な部分が十分にテストされているかを確認する指標として役立ちます。カバレッジが低い箇所は、テストが不足している可能性を示唆します。
5.6 テストコードも読みやすく、保守しやすく
テストコードもプロダクションコードと同様に、読みやすく、保守しやすい状態に保つことが重要です。
- DRY (Don’t Repeat Yourself) 原則を適用し、共通のセットアップロジックやヘルパー関数を適切に利用します。
- テストの意図が自明でない場合は、コメントを追加します。
- プロダクションコードのリファクタリングに合わせて、テストコードも更新します。
これらのベストプラクティスに従うことで、unittest
を使ったテストの効果を最大限に引き出し、ソフトウェアの品質向上に貢献できます。💡
まとめ
この記事では、Pythonの標準テストフレームワークである unittest
について、基本的な概念から高度なテクニック、そしてベストプラクティスまでを詳しく解説しました。
主なポイントを振り返りましょう:
unittest.TestCase
を継承してテストケースを作成し、test_
で始まるメソッドでテストを記述します。- 様々な アサーションメソッド を使って、コードの動作が期待通りか検証します。
setUp/tearDown
やsetUpClass/tearDownClass
でテストの前後の処理を定義できます。- コマンドラインから
python -m unittest discover
でテストを効率的に実行できます。 - テストのスキップ (
@unittest.skip
など) や期待される失敗 (@unittest.expectedFailure
) を使って、テストの管理を柔軟に行えます。 - サブテスト (
with self.subTest()
) で、一つのメソッド内で複数のパラメータ化テストを簡潔に記述できます。 unittest.mock
を活用して依存関係を排除し、隔離されたユニットテストを実現します。patch
やautospec
は特に重要です。- テストの独立性、明確な命名、単一責任、テスト容易性、網羅性といった ベストプラクティス を意識することで、より効果的で保守しやすいテストコードを作成できます。
unittest
はPythonに標準で組み込まれているため、追加のインストールなしにすぐに利用を開始できる点が大きな利点です。テストを書く習慣を身につけることは、バグの少ない、信頼性の高いソフトウェアを開発するための重要なステップです。
もちろん、pytest
のようなサードパーティ製のより高機能なテストフレームワークも存在し、多くのプロジェクトで採用されています。しかし、unittest
の基本的な概念やテクニックを理解しておくことは、pytest
を使う上でも、Pythonにおけるテスト全般の理解を深める上でも、非常に役立ちます。
ぜひ、今回学んだことを活かして、ご自身のプロジェクトにユニットテストを導入・改善してみてください! Happy testing! 🎉
コメント