モジュールの単体テスト

プログラムが大きくなり、機能(モジュールや関数)が増えてくると、「一部のコードを修正したせいで、全く関係のない別の場所が動かなくなった」という先祖返りやデグレード(品質低下)のバグが発生しやすくなります。
こうした事態を防ぎ、コードの品質を高い状態で担保し続けるために欠かせない開発プロセスが単体テスト(ユニットテスト:Unit Test)です。Pythonには標準ライブラリとして unittest モジュールが組み込まれており、外部ツールを導入することなく、すぐに本格的な自動テスト環境を構築できます。本記事では、基本的なテストコードの書き方から実務的なアサーション(検証)の使い分けまでを詳しく解説します。
単体テストの基本思想
単体テストとは、プログラム全体を動かすのではなく、関数やクラスといった「これ以上分割できない最小単位(ユニット)」に対して、意図した通りの入力(引数)を渡したときに、期待通りの出力(戻り値)が返ってくるかを自動で検証する仕組みです。
一度テストコードを書いておけば、コマンド1つで何百件もの検証を一瞬で再実行できるため、安心してコードの機能追加やリファクタリング(美化)を行えるようになります。
unittestモジュールを使用した標準的な実装例
テスト対象となる関数と、それを検証するためのテストコードを記述した、最も標準的な実装構造です。
import unittest
# 1. テスト対象の関数(実務では別ファイルからimportすることが多いです)
def add(x, y):
return x + y
# 2. unittest.TestCase を継承したクラスを作成
class TestAddFunction(unittest.TestCase):
# テストを行うメソッド名は、必ず「test_」から始まる名前にする必要があります
# 「test_」で始まっていないメソッドは、テスト対象として認識されずスキップされます
def test_add_positive_numbers(self):
# self.assertEqual(実際の値, 期待される値) で検証する
self.assertEqual(add(2, 3), 5)
def test_add_negative_numbers(self):
self.assertEqual(add(-1, -1), -2)
# スクリプトが直接実行された場合、登録されたテストをすべて走らせる司令塔を起動
if __name__ == '__main__':
unittest.main()
知っておくべき主要なassert(アサーション)メソッド
unittest では、値が等しいかどうかを調べる assertEqual 以外にも、テストデータの型や状態に応じて、多様な検証用メソッド(アサーション)が用意されています。これらを適切に使い分けることで、より意図の明確なテストコードが書けるようになります。
| アサーションメソッド | 判定する条件式 | 実務での具体的な使いどころ |
|---|---|---|
assertEqual(a, b) |
a == b |
計算結果の数値、処理後の文字列、リストの中身が完全に一致するかどうかを確認するとき。 |
assertNotEqual(a, b) |
a != b |
「更新処理によって、データが古い状態のままになっていないこと」を裏付けたいとき。 |
assertTrue(x) |
bool(x) is True |
「ユーザーがログイン状態であるか(is_authenticated)」などのフラグが有効かを検証するとき。 |
assertFalse(x) |
bool(x) is False |
「エラーフラグが立っていないこと」「削除フラグがまだ無効であること」を確認するとき。 |
assertIsNone(x) |
x is None |
データベース検索で「該当するデータが見つからなかった場合に、正しく None が返るか」を見るとき。 |
assertIn(a, b) |
a in b |
生成されたテキストの中に、特定のキーワードがちゃんと含まれているか(包含関係)を検証するとき。 |
テストの実行方法と結果の読み方
作成したテストファイルを実行するには、ターミナル(コマンドライン)から以下のように入力します。
# テストファイルを直接実行する場合 python test_module.py # または、unittestのテストランナー機能を使ってディレクトリ内のテストを自動探索して実行 python -m unittest test_module.py
テストが実行されると、コンソールに結果のインジケータが表示されます。
.(ドット): テストが正常に通過(Pass)したことを示します。F(Failure): アサーションが不一致だった(バグまたは期待値の計算ミス)ことを示します。どこがどう違ったかの詳細な差分(Diff)が出力されます。E(Error): テストコードの実行途中で、予期せぬ例外(NameErrorやTypeErrorなど)が発生してクラッシュしたことを示します。
実務での重要テクニック:テストの前処理と後処理
実際のシステム開発では、「データベースにテスト用のダミーデータを事前に登録し、テストが終わったら綺麗に削除する」といった、テストの前後の環境整備が必要不可欠です。
unittest では、クラス内に setUp() と tearDown() という特別なメソッドを定義しておくことで、それぞれのテストメソッドが実行される前後に、これらの初期化・片付け処理を自動で割り込ませることができます。
import unittest
class TestDatabaseModule(unittest.TestCase):
def setUp(self):
# 各テストメソッドが実行される「直前」に毎回必ず動く
print("テスト用データベースの接続を開き、初期データを投入します。")
self.test_db_connection = "Connected"
def tearDown(self):
# 各テストメソッドが終了した「直後」に毎回必ず動く(エラーが起きても実行されます)
print("テスト用データを削除し、接続を閉じます。")
self.test_db_connection = "Disconnected"
def test_insert_user(self):
print("ユーザー登録機能のテストを実行中...")
# setUpで用意した接続を使って検証を行う
self.assertEqual(self.test_db_connection, "Connected")
まとめ
- 自動テストは開発者の安全ネット: 単体テストを充実させておくことで、将来的なコードの機能追加や大規模な書き換え(リファクタリング)を、既存の機能を壊す恐怖なしで大胆に進められるようになります。
- 「test_」の命名規則と適切なアサーション: テストケースは必ず
unittest.TestCaseを継承し、メソッド名をtest_で始めるというルールを厳守してください。データ型に合わせた適切なアサーションメソッドを選ぶことで、テストの信頼性が高まります。 setUpとtearDownでテスト環境をクリーンに保つ: 外部ファイルやデータベースを扱うテストでは、前処理・後処理の共通化を徹底し、それぞれのテストケースが他のテスト結果に影響を与えない「独立性」を保つ設計が極めて重要です。