Python 単体テストフレームワーク unittest の概要と実践

unittest を学ぶ理由

テストは実施フェーズごとに単体テスト、結合テスト、システムテスト、受入テストなどに分類されます。単体テスト(unit test)とは、ソフトウェアの最小単位(関数やクラスなど)を他のコンポーネントから切り離して検証する作業です。これは通常、開発者がモジュール実装後に実施します。

単体テストを早期に実施することで、バグを发现・修正しやすくなり、安定性向上やFizzBuzzレベルの初期不具合を低コストで解消できます。また、単体レベルでの検証が済んでいれば、後の結合テストやシステムテストの信頼性も高まります。

unittest は Python 標準で備わっている単体テスト用フレームワークです。構造が明確で習得が容易であり、テストの整理・実行を効率化できます。

unittest のアーキテクチャ

unittest フレームワークの核心的な構成要素は以下の4つです:

  • テストケース(Test Case):単一のテスト実行単位。unittest.TestCase を継承したクラス内に test_ で始まるメソッドを定義し、その1つ1つがテストケースになります。メソッド名の ASCII 並び順で実行順序が決まります。
  • テストフィクスチャ(Test Fixture):テスト実行前の準備(setUp / setUpClass)と実行後の後始末(tearDown / tearDownClass)。例えば、テストごとに外部システムに接続して token を取得し、その後切断するといった状態管理に利用されます。
  • テストスイート(Test Suite):複数のテストケースを1つにまとめた実行単位です。TestLoader クラスを使って、クラスやモジュール単位でスイートに追加できます。
  • テストランナー(Test Runner):テストスイート内のテストケースを実行し、結果を出力します。標準では console 出力ですが、HTMLTestRunner などのサードパーティ拡張により HTML レポートを生成可能です。

アサーション(検証)

テスト結果が期待通りであるかを検証するための仕組みです。unittest では assert 処理が失敗した場合、AssertionError が送出され、そのテストケースは fail になります。

以下は代表的なアサーションメソッドです(すべて msg パラメータに対応):

メソッド検証内容
assertEqual(a, b)a == b
assertNotEqual(a, b)a != b
assertTrue(x)bool(x) is True
assertFalse(x)bool(x) is False
assertIs(x, y)x is y
assertIsNot(x, y)x is not y
assertIsNone(x)x is None
assertIsNotNone(x)x is not None
assertIn(item, container)item in container
assertNotIn(item, container)item not in container
assertIsInstance(obj, cls)isinstance(obj, cls)
assertNotIsInstance(obj, cls)not isinstance(obj, cls)

TestCase の実装例

以下は、単純なユーザー登録ロジックを対象としたテストクラスの例です。

テスト対象モジュール(register.py):

# register.py
users = [{'username': 'testing', 'password': '123456'}]

def register(username, password1, password2):
    if not all([username, password1, password2]):
        return {'code': 0, 'msg': 'すべてのパラメータは必須です。'}

    for existing_user in users:
        if username == existing_user['username']:
            return {'code': 0, 'msg': 'このユーザー名はすでに使用されています。'}

    if password1 != password2:
        return {'code': 0, 'msg': '入力されたパスワードが一致しません。'}

    if not (6 <= len(username) <= 18 and 6 <= len(password1) <= 18):
        return {'code': 0, 'msg': 'ユーザー名とパスワードは6~18文字で入力してください。'}

    users.append({'username': username, 'password': password2})
    return {'code': 1, 'msg': '登録が完了しました。'}

テストケース(test_register.py):

# test_register.py
import unittest
from register import register

class RegisterTest(unittest.TestCase):
    def test_registration_success(self):
        input_data = ('newuser', 'pass1234', 'pass1234')
        actual = register(*input_data)
        expected = {'code': 1, 'msg': '登録が完了しました。'}
        self.assertEqual(actual, expected)

    def test_duplicate_username(self):
        input_data = ('testing', '123456', '123456')
        actual = register(*input_data)
        expected = {'code': 0, 'msg': 'このユーザー名はすでに使用されています。'}
        self.assertEqual(actual, expected)

    def test_missing_parameter(self):
        input_data = ('', 'pass1234', 'pass1234')
        actual = register(*input_data)
        expected = {'code': 0, 'msg': 'すべてのパラメータは必須です。'}
        self.assertEqual(actual, expected)

    def test_invalid_length_username(self):
        input_data = ('thisusernameiswaytoolong', 'pass1234', 'pass1234')
        actual = register(*input_data)
        expected = {'code': 0, 'msg': 'ユーザー名とパスワードは6~18文字で入力してください。'}
        self.assertEqual(actual, expected)

    def test_mismatched_passwords(self):
        input_data = ('newuser2', 'pass1234', 'pass5678')
        actual = register(*input_data)
        expected = {'code': 0, 'msg': '入力されたパスワードが一致しません。'}
        self.assertEqual(actual, expected)

if __name__ == '__main__':
    unittest.main()

実行結果例:

......
----------------------------------------------------------------------
Ran 6 tests in 0.002s

OK

テストフィクスチャの利用

テストの前後で共通の初期化・クリーンアップ処理を行うために、以下のメソッドが利用可能です:

  • setUp():各テストメソッド実行前に呼ばれる(インスタンススコープ)
  • tearDown():各テストメソッド実行後に呼ばれる(インスタンススコープ)
  • setUpClass():クラス内のすべてのテストメソッド実行前に1回だけ呼ばれる(クラスメソッド)
  • tearDownClass():クラス内のすべてのテストメソッド実行後に1回だけ呼ばれる(クラスメソッド)
class RegisterTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        print('[Class Level] 初期化処理 ...')

    @classmethod
    def tearDownClass(cls):
        print('[Class Level] 終了処理 ...')

    def setUp(self):
        print(f'{self._testMethodName} 開始 ...')

    def tearDown(self):
        print(f'{self._testMethodName} 終了 ...')

    # test メソッドは省略

テストスイートの構成

明示的にテストケースを集めて実行するためにテストスイートを利用できます。

  • addTest(TestCase インスタンス):単一のテストメソッドを追加
  • addTests([TestCase インスタンス...]):テストメソッドリストを追加
  • TestLoader().loadTestsFromTestCase(TestClass):クラス単位でロード
  • TestLoader().loadTestsFromModule(module):モジュール単位でロード
  • TestLoader().discover(start_dir, pattern):ディレクトリ以下からファイル名パターンに合致するテスト群を再帰的に検出
# test_runner.py
import unittest
import os
from pathlib import Path
import test_register

base_dir = Path(__file__).parent.resolve()

suite = unittest.TestSuite()
loader = unittest.TestLoader()

# discover を利用し、test_register.py を検出・追加
suite.addTests(loader.discover(start_dir=base_dir, pattern='test_*'))

# 実行(後述の runner でハンドリング可能)

テストランナーとレポート生成

unittest 単体では簡易な実行結果しか出力しません。本格的なレポート生成には外部ライブラリ(例:HTMLTestRunner/CN)を用いるのが一般的です。

HTML レポート出力例:

# report_runner.py
from HTMLTestRunner import HTMLTestRunner
import unittest
import os

base_dir = os.path.dirname(os.path.abspath(__file__))
suite = unittest.TestSuite()

loader = unittest.TestLoader()
suite.addTests(loader.discover(start_dir=base_dir, pattern='test_*.py'))

with open('report.html', 'wb') as f:
    runner = HTMLTestRunner(
        stream=f,
        verbosity=2,
        title='ソフトウェア登録機能の自動テスト',
        description='Python unittest を用いた登録処理の検証結果',
        tester='QA Team'
    )
    runner.run(suite)

HTMLTestRunner には以下の主な設定項目があります:

  • stream: レポート出力先ストリーム(fileオブジェクト or バイナリモードの io)
  • tester: レポート中に表示されるテスト担当者名
  • description: レポートの説明文
  • title: HTML レポートのタイトル
  • verbosity: 出力の詳細度(0: 結果のみ、1: 基本、2: 詳細)

よくあるエラーと対処法

  • TypeError: a bytes-like object is required, not 'str'
    • 原因:open モードが textile で open('report.html', 'w') → バイナリモードで open('report.html', 'wb') に変更
  • 日本語が文字化けする
    • 原因:HTMLTestRunner のエンコーディング指定不足
    • 対策:HTMLTestRunner を独自に修正(またはサードパーティ互換版の利用)。または日本語入力を避けるなどの工夫が有効です。

タグ: unittest Python testing automation-test assertion

8月7日 04:22 投稿