Skip to content

Python unittest:ユニットテストの書き方と実行方法(完全ガイド)

Updated on

ユーザーデータを処理する関数を書いたとします。いくつかの入力で手動テストした限りでは動いている。ところが、依存しているヘルパー関数を同僚が変更し、あなたのコードは3週間もの間、誰にも気づかれないまま誤った結果を返し続けました。バグは本番に入り、顧客レコードが破損します。原因はたった1行の変更で、既存の挙動に対して誰も検証していませんでした。これはユニットテストが防ぐ典型的な失敗パターンです。Python には標準ライブラリとして unittest が同梱されています。サードパーティ依存ゼロで、回帰を本番到達前に検知できるフル機能のテストフレームワークです。

📚

unittest とは?なぜ使うのか?

unittest は Python 組み込みのテストフレームワークで、Java の JUnit をモデルにしています。Python 2.1 から標準ライブラリに含まれているため、どの Python 環境にも最初から入っています。pip install 不要。依存関係の管理も不要。テストクラスを書き、テストメソッドを定義し、コマンドラインから実行します。

ユニットテストは、コードの個々の部品(関数・メソッド・クラス)が「単体として」正しく振る舞うことを検証します。各ユニットが単独で正しければ、統合時に見えないバグが紛れ込む確率は大きく下がります。

unittest が標準で提供する主な機能は次のとおりです。

  • テストケースクラス(自動テストディスカバリ対応)
  • 豊富なアサーションメソッド(等価性、真偽、例外、警告など)
  • setUp/tearDown フック(メソッド単位/クラス単位の両方)
  • テストスイート(テストの整理・グルーピング)
  • モックunittest.mock、Python 3.3 で追加)
  • コマンドラインテストランナー(詳細度の制御など)

はじめてのユニットテスト

まずはテスト対象の関数と、それに対応するテストクラスを用意します。

# calculator.py
def add(a, b):
    return a + b
 
def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
# test_calculator.py
import unittest
from calculator import add, divide
 
class TestCalculator(unittest.TestCase):
 
    def test_add_positive_numbers(self):
        self.assertEqual(add(2, 3), 5)
 
    def test_add_negative_numbers(self):
        self.assertEqual(add(-1, -1), -2)
 
    def test_add_zero(self):
        self.assertEqual(add(0, 0), 0)
 
    def test_divide_normal(self):
        self.assertEqual(divide(10, 2), 5.0)
 
    def test_divide_by_zero_raises(self):
        with self.assertRaises(ValueError):
            divide(10, 0)
 
if __name__ == "__main__":
    unittest.main()

実行します:

python -m unittest test_calculator.py -v

出力:

test_add_negative_numbers (test_calculator.TestCalculator) ... ok
test_add_positive_numbers (test_calculator.TestCalculator) ... ok
test_add_zero (test_calculator.TestCalculator) ... ok
test_divide_by_zero_raises (test_calculator.TestCalculator) ... ok
test_divide_normal (test_calculator.TestCalculator) ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

名前が test_ で始まるメソッドは自動的に検出され、実行されます。クラスは unittest.TestCase を継承しており、各種アサーションメソッドが使えるようになります。

アサーションメソッド一覧

unittest.TestCase には包括的なアサーションメソッドが用意されています。いずれも、条件が満たされない場合に分かりやすい失敗メッセージを出します。

等価性・同一性のアサーション

MethodChecksExample
assertEqual(a, b)a == bself.assertEqual(result, 42)
assertNotEqual(a, b)a != bself.assertNotEqual(result, 0)
assertIs(a, b)a is bself.assertIs(singleton, instance)
assertIsNot(a, b)a is not bself.assertIsNot(obj1, obj2)
assertIsNone(x)x is Noneself.assertIsNone(result)
assertIsNotNone(x)x is not Noneself.assertIsNotNone(user)

真偽・メンバーシップのアサーション

MethodChecksExample
assertTrue(x)bool(x) is Trueself.assertTrue(is_valid)
assertFalse(x)bool(x) is Falseself.assertFalse(has_errors)
assertIn(a, b)a in bself.assertIn("admin", roles)
assertNotIn(a, b)a not in bself.assertNotIn("deleted", status)
assertIsInstance(a, b)isinstance(a, b)self.assertIsInstance(result, dict)

数値・コレクションのアサーション

MethodChecksExample
assertAlmostEqual(a, b)round(a-b, 7) == 0self.assertAlmostEqual(0.1 + 0.2, 0.3)
assertGreater(a, b)a > bself.assertGreater(len(results), 0)
assertLess(a, b)a < bself.assertLess(latency, 1.0)
assertCountEqual(a, b)要素が同じ(順不同)self.assertCountEqual([3,1,2], [1,2,3])
assertListEqual(a, b)リストが等しいself.assertListEqual(result, expected)
assertDictEqual(a, b)dict が等しいself.assertDictEqual(config, defaults)

例外・警告のアサーション

import unittest
import warnings
 
class TestExceptions(unittest.TestCase):
 
    def test_raises_value_error(self):
        """assertRaises checks that the exception is raised."""
        with self.assertRaises(ValueError):
            int("not_a_number")
 
    def test_raises_with_message(self):
        """assertRaisesRegex checks both the exception and its message."""
        with self.assertRaisesRegex(ValueError, "invalid literal"):
            int("not_a_number")
 
    def test_warns_deprecation(self):
        """assertWarns checks that a warning is issued."""
        with self.assertWarns(DeprecationWarning):
            warnings.warn("old function", DeprecationWarning)

assertTrue よりも、可能な限り「具体的なアサーション」を使うのが基本です。self.assertTrue(result == 42) の代わりに self.assertEqual(result, 42) を使いましょう。具体的な方は 42 != 41 のように原因が明確な失敗メッセージになりますが、assertTrue だと False is not true しか分かりません。

setUp と tearDown:テストフィクスチャ

多くのテストでは、初期状態が必要です。たとえばデータベース接続、一時ファイル、事前設定されたオブジェクトなど。setUptearDown は各テストメソッドの前後で実行され、テストごとに「新鮮な開始点」を用意できます。

import unittest
import os
import tempfile
 
class TestFileProcessor(unittest.TestCase):
 
    def setUp(self):
        """Runs before EACH test method."""
        self.test_dir = tempfile.mkdtemp()
        self.test_file = os.path.join(self.test_dir, "data.txt")
        with open(self.test_file, "w") as f:
            f.write("line1\nline2\nline3\n")
 
    def tearDown(self):
        """Runs after EACH test method."""
        os.remove(self.test_file)
        os.rmdir(self.test_dir)
 
    def test_read_lines(self):
        with open(self.test_file, "r") as f:
            lines = f.readlines()
        self.assertEqual(len(lines), 3)
 
    def test_file_exists(self):
        self.assertTrue(os.path.exists(self.test_file))
 
    def test_first_line_content(self):
        with open(self.test_file, "r") as f:
            first_line = f.readline().strip()
        self.assertEqual(first_line, "line1")

各テストメソッドはそれぞれ setUp が呼ばれます。test_read_lines がファイルを変更しても、test_first_line_contentsetUp が作り直した元の内容を確認できます。

setUpClass と tearDownClass:一度だけのセットアップ

作成コストの高いリソース(データベース接続、大きなフィクスチャ、サーバープロセスなど)には、クラス全体で一度だけ作る setUpClass / tearDownClass を使います。

import unittest
import sqlite3
 
class TestDatabase(unittest.TestCase):
 
    @classmethod
    def setUpClass(cls):
        """Runs ONCE before all tests in this class."""
        cls.conn = sqlite3.connect(":memory:")
        cls.cursor = cls.conn.cursor()
        cls.cursor.execute("""
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name TEXT NOT NULL,
                email TEXT UNIQUE NOT NULL
            )
        """)
        cls.cursor.executemany(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            [
                ("Alice", "alice@example.com"),
                ("Bob", "bob@example.com"),
                ("Charlie", "charlie@example.com"),
            ],
        )
        cls.conn.commit()
 
    @classmethod
    def tearDownClass(cls):
        """Runs ONCE after all tests in this class."""
        cls.conn.close()
 
    def test_user_count(self):
        self.cursor.execute("SELECT COUNT(*) FROM users")
        count = self.cursor.fetchone()[0]
        self.assertEqual(count, 3)
 
    def test_find_user_by_email(self):
        self.cursor.execute(
            "SELECT name FROM users WHERE email = ?",
            ("bob@example.com",),
        )
        name = self.cursor.fetchone()[0]
        self.assertEqual(name, "Bob")
 
    def test_unique_emails(self):
        with self.assertRaises(sqlite3.IntegrityError):
            self.cursor.execute(
                "INSERT INTO users (name, email) VALUES (?, ?)",
                ("Duplicate", "alice@example.com"),
            )
HookRunsDecoratorUse Case
setUp各テストメソッドの前なし新しいオブジェクト作成、状態のリセット
tearDown各テストメソッドの後なしファイル掃除、モックのリセット
setUpClassクラス内の全テストの前に1回@classmethodDB接続、重いフィクスチャ
tearDownClassクラス内の全テストの後に1回@classmethod接続クローズ、共有リソース削除

unittest.mock によるモック

実際のアプリは DB、API、ファイルシステム、ネットワークサービスに依存します。ユニットテストで本番 API を叩いたり、DB が起動していないと動かないテストにしたりは避けたいところです。unittest.mock は依存先を「制御可能な代替」に置き換えます。

基本的な Mock オブジェクト

from unittest.mock import Mock
 
# Create a mock object
api_client = Mock()
 
# Configure return values
api_client.get_user.return_value = {"id": 1, "name": "Alice"}
 
# Use it like a real object
user = api_client.get_user(user_id=1)
print(user)  # {'id': 1, 'name': 'Alice'}
 
# Verify it was called correctly
api_client.get_user.assert_called_once_with(user_id=1)

@patch によるパッチ

@patch デコレータは、テスト実行中だけ特定モジュール内のオブジェクトを差し替えます。依存からユニットを切り離すための主要ツールです。

# user_service.py
import requests
 
def get_user_name(user_id):
    response = requests.get(f"https://api.example.com/users/{user_id}")
    response.raise_for_status()
    return response.json()["name"]
# test_user_service.py
import unittest
from unittest.mock import patch, Mock
from user_service import get_user_name
 
class TestUserService(unittest.TestCase):
 
    @patch("user_service.requests.get")
    def test_get_user_name_success(self, mock_get):
        """Mock the requests.get call to avoid hitting the real API."""
        mock_response = Mock()
        mock_response.json.return_value = {"id": 1, "name": "Alice"}
        mock_response.raise_for_status.return_value = None
        mock_get.return_value = mock_response
 
        result = get_user_name(1)
 
        self.assertEqual(result, "Alice")
        mock_get.assert_called_once_with("https://api.example.com/users/1")
 
    @patch("user_service.requests.get")
    def test_get_user_name_http_error(self, mock_get):
        """Verify that HTTP errors propagate correctly."""
        import requests
        mock_get.return_value.raise_for_status.side_effect = (
            requests.exceptions.HTTPError("404 Not Found")
        )
 
        with self.assertRaises(requests.exceptions.HTTPError):
            get_user_name(999)

コンテキストマネージャとしてのパッチ

import unittest
from unittest.mock import patch
 
class TestConfig(unittest.TestCase):
 
    def test_reads_environment_variable(self):
        with patch.dict("os.environ", {"DATABASE_URL": "sqlite:///test.db"}):
            import os
            self.assertEqual(os.environ["DATABASE_URL"], "sqlite:///test.db")

MagicMock と Mock の違い

MagicMockMock のサブクラスで、マジックメソッド(__len____iter____getitem__ など)があらかじめ扱えるように設定されています。テスト対象コードがモックに対して dunder メソッドを呼ぶ場合に MagicMock を使います。

from unittest.mock import MagicMock
 
mock_list = MagicMock()
mock_list.__len__.return_value = 5
mock_list.__getitem__.return_value = "item"
 
print(len(mock_list))     # 5
print(mock_list[0])       # item

複雑な挙動のための side_effect

side_effect を使うと、例外を投げさせたり、呼び出しごとに異なる戻り値を返したり、カスタム関数を実行させたりできます。

from unittest.mock import Mock
 
# Raise an exception
mock_db = Mock()
mock_db.connect.side_effect = ConnectionError("Database unreachable")
 
# Return different values on successive calls
mock_api = Mock()
mock_api.fetch.side_effect = [{"page": 1}, {"page": 2}, StopIteration]
 
print(mock_api.fetch())  # {'page': 1}
print(mock_api.fetch())  # {'page': 2}
 
# Custom function
def validate_input(x):
    if x < 0:
        raise ValueError("Negative input")
    return x * 2
 
mock_fn = Mock(side_effect=validate_input)
print(mock_fn(5))  # 10

テストディスカバリ(自動検出)

すべてのテストファイルを手動で列挙する必要はありません。Python のテストディスカバリは命名規則に従うテストを見つけて実行します。

# Discover and run all tests in the current directory tree
python -m unittest discover
 
# Specify a start directory and pattern
python -m unittest discover -s tests -p "test_*.py"
 
# Verbose output
python -m unittest discover -v

テストディスカバリは test_*.py(デフォルトパターン)に一致するファイルを探し、import して、unittest.TestCase を継承するクラス内のテストを実行します。

推奨プロジェクト構成

my_project/
    src/
        calculator.py
        user_service.py
        utils.py
    tests/
        __init__.py
        test_calculator.py
        test_user_service.py
        test_utils.py
    setup.py

プロジェクトルートからすべてのテストを実行:

python -m unittest discover -s tests -v

テストスイートでテストを整理する

どのテストを実行するかを細かく制御したい場合は、手動でテストスイートを組み立てます。

import unittest
from test_calculator import TestCalculator
from test_user_service import TestUserService
 
def fast_tests():
    """Suite of tests that run quickly (no I/O, no network)."""
    suite = unittest.TestSuite()
    suite.addTest(TestCalculator("test_add_positive_numbers"))
    suite.addTest(TestCalculator("test_add_negative_numbers"))
    suite.addTest(TestCalculator("test_divide_normal"))
    return suite
 
def all_tests():
    """Full test suite."""
    loader = unittest.TestLoader()
    suite = unittest.TestSuite()
    suite.addTests(loader.loadTestsFromTestCase(TestCalculator))
    suite.addTests(loader.loadTestsFromTestCase(TestUserService))
    return suite
 
if __name__ == "__main__":
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(fast_tests())

テストのスキップと「失敗が期待されるテスト」

特定の条件でのみ実行したいテストもあります。たとえば OS、Python バージョン、外部サービスが利用可能かどうかなど。

import unittest
import sys
 
class TestPlatformSpecific(unittest.TestCase):
 
    @unittest.skip("Temporarily disabled while refactoring")
    def test_feature_under_construction(self):
        pass
 
    @unittest.skipIf(sys.platform == "win32", "Not supported on Windows")
    def test_unix_permissions(self):
        import os
        self.assertTrue(os.access("/tmp", os.W_OK))
 
    @unittest.skipUnless(sys.platform.startswith("linux"), "Linux only")
    def test_proc_filesystem(self):
        import os
        self.assertTrue(os.path.exists("/proc"))
 
    @unittest.expectedFailure
    def test_known_bug(self):
        """This test documents a known bug. It SHOULD fail."""
        self.assertEqual(1 + 1, 3)
DecoratorEffect
@unittest.skip(reason)このテストを常にスキップ
@unittest.skipIf(condition, reason)condition が True のときスキップ
@unittest.skipUnless(condition, reason)condition が True のときのみ実行(それ以外はスキップ)
@unittest.expectedFailure失敗が期待されるテストとしてマーク(通った場合はエラー扱い)

subTest によるパラメータ化テスト

同じロジックを異なる入力でテストするのはよくあることです。ケースごとに別のテストメソッドを書く代わりに、subTest を使って1つのメソッド内でパラメータ化できます。

import unittest
 
def is_palindrome(s):
    cleaned = s.lower().replace(" ", "")
    return cleaned == cleaned[::-1]
 
class TestPalindrome(unittest.TestCase):
 
    def test_palindromes(self):
        test_cases = [
            ("racecar", True),
            ("hello", False),
            ("A man a plan a canal Panama", True),
            ("", True),
            ("ab", False),
            ("madam", True),
            ("Nurses Run", True),
        ]
        for text, expected in test_cases:
            with self.subTest(text=text):
                self.assertEqual(is_palindrome(text), expected)

subTest を使うと、あるケースが失敗しても残りのケースの実行は止まりません。また、どのサブケースが失敗したかが出力で特定できます:

FAIL: test_palindromes (test_palindrome.TestPalindrome) (text='Nurses Run')
AssertionError: False != True

subTest がなければ、最初の失敗でメソッド全体が中断され、他のケースがどうだったか分からなくなります。

unittest vs pytest vs doctest

Python には組み込み、または広く使われるテストツールが3つあります。それぞれ目的が異なります。

Featureunittestpytestdoctest
Included in stdlibYesNo (pip install)Yes
Test styleClass-based (TestCase)Function-based (plain def test_)docstring に埋め込み
Assertionsself.assertEqual, self.assertTrue, etc.assert期待出力とのマッチ
FixturessetUp/tearDown, setUpClass@pytest.fixture(DI)なし
Parameterized testssubTest(限定的)@pytest.mark.parametrize(強力)docstring あたり1例
Mockingunittest.mock(組み込み)unittest.mock + monkeypatch該当なし
Test discoverypython -m unittest discoverpytest(自動検出)python -m doctest module.py
Output on failure基本的な diff文脈つきで詳細な diff期待 vs 実際の出力
Pluginsなし1000+(coverage、fixture など)なし
Learning curve中程度(OOP パターン)低い(関数ベース)非常に低い
Best for標準ライブラリ寄り、依存ゼロが必要なプロジェクト多くの Python プロジェクト、複雑なテストセットアップドキュメント中の簡単な例

unittest を選ぶとき:

  • 外部依存をゼロにしたい
  • 組織/プロジェクトがすでに unittest を使っている
  • クラスベースでテストを整理したい
  • 追加設定なしで unittest.mock を使いたい

pytest を選ぶとき:

  • よりシンプルな文法と、より良い失敗出力がほしい
  • 強力なパラメータ化や fixture が必要
  • pytest のプラグインエコシステム(coverage、async、Django など)を活用したい

doctest を選ぶとき:

  • ドキュメント中のコード例が今も動くことを検証したい
  • テストが単純な入出力ペアで済む

pytest は unittest 形式のテストも変更なしで実行できます。多くのチームは unittest で始め、既存のテストクラスを保ったまま、ランナーだけ pytest に切り替えることもあります。

実務的なクラスをテストする:完全な例

ショッピングカート実装をテストする完全例です。

# cart.py
class Product:
    def __init__(self, name, price):
        if price < 0:
            raise ValueError("Price cannot be negative")
        self.name = name
        self.price = price
 
class ShoppingCart:
    def __init__(self):
        self.items = []
 
    def add(self, product, quantity=1):
        if quantity <= 0:
            raise ValueError("Quantity must be positive")
        self.items.append({"product": product, "quantity": quantity})
 
    def total(self):
        return sum(
            item["product"].price * item["quantity"]
            for item in self.items
        )
 
    def remove(self, product_name):
        self.items = [
            item for item in self.items
            if item["product"].name != product_name
        ]
 
    def item_count(self):
        return sum(item["quantity"] for item in self.items)
# test_cart.py
import unittest
from cart import Product, ShoppingCart
 
class TestProduct(unittest.TestCase):
 
    def test_create_product(self):
        p = Product("Widget", 9.99)
        self.assertEqual(p.name, "Widget")
        self.assertAlmostEqual(p.price, 9.99)
 
    def test_negative_price_raises(self):
        with self.assertRaises(ValueError):
            Product("Bad", -5.00)
 
class TestShoppingCart(unittest.TestCase):
 
    def setUp(self):
        self.cart = ShoppingCart()
        self.apple = Product("Apple", 1.50)
        self.bread = Product("Bread", 3.00)
 
    def test_empty_cart_total(self):
        self.assertEqual(self.cart.total(), 0)
 
    def test_add_single_item(self):
        self.cart.add(self.apple)
        self.assertEqual(self.cart.item_count(), 1)
        self.assertAlmostEqual(self.cart.total(), 1.50)
 
    def test_add_multiple_items(self):
        self.cart.add(self.apple, quantity=3)
        self.cart.add(self.bread, quantity=2)
        self.assertEqual(self.cart.item_count(), 5)
        self.assertAlmostEqual(self.cart.total(), 10.50)
 
    def test_remove_item(self):
        self.cart.add(self.apple, quantity=2)
        self.cart.add(self.bread)
        self.cart.remove("Apple")
        self.assertEqual(self.cart.item_count(), 1)
        self.assertAlmostEqual(self.cart.total(), 3.00)
 
    def test_remove_nonexistent_item(self):
        self.cart.add(self.apple)
        self.cart.remove("Nonexistent")
        self.assertEqual(self.cart.item_count(), 1)
 
    def test_add_zero_quantity_raises(self):
        with self.assertRaises(ValueError):
            self.cart.add(self.apple, quantity=0)
 
    def test_add_negative_quantity_raises(self):
        with self.assertRaises(ValueError):
            self.cart.add(self.apple, quantity=-1)
 
if __name__ == "__main__":
    unittest.main()

unittest のベストプラクティス

1. 1つの概念につき1つのアサーション(を意識する)

各テストメソッドは、1つの論理的な概念を検証するべきです。同じ操作の別側面を確認する複数のアサーションは問題ありません。

# GOOD -- multiple assertions about the same operation
def test_user_creation(self):
    user = create_user("alice", "alice@example.com")
    self.assertEqual(user.name, "alice")
    self.assertEqual(user.email, "alice@example.com")
    self.assertIsNotNone(user.id)
 
# BAD -- testing unrelated things in one method
def test_everything(self):
    user = create_user("alice", "alice@example.com")
    self.assertEqual(user.name, "alice")
    users = list_all_users()
    self.assertGreater(len(users), 0)  # Unrelated assertion

2. 説明的なテスト名を使う

テスト名は「シナリオ」と「期待結果」が分かるようにします。CI で落ちたとき、名前だけで何が壊れたか判断できる状態が理想です。

# GOOD
def test_divide_by_zero_raises_value_error(self):
    ...
 
def test_empty_cart_returns_zero_total(self):
    ...
 
# BAD
def test_divide(self):
    ...
 
def test1(self):
    ...

3. テストは独立であること

あるテストが別のテストの結果や実行順に依存してはいけません。各テストは自分で状態を用意し、後片付けも自分で行うべきです。

4. テストを高速に保つ

ユニットテストはミリ秒単位で終わるのが理想です。DB が必要ならモックにし、API が必要ならモックにします。遅い統合テストは別スイートとして分けましょう。

5. エッジケースをテストする

空入力、ゼロ値、None、非常に大きな入力、無効な型など、境界条件は必ずテストします。

def test_edge_cases(self):
    test_cases = [
        ([], 0),          # empty list
        ([0], 0),         # single zero
        ([-1, -2], -3),   # all negative
        ([999999999], 999999999),  # large number
    ]
    for inputs, expected in test_cases:
        with self.subTest(inputs=inputs):
            self.assertEqual(sum(inputs), expected)

6. 実装詳細をテストしない

内部状態や private メソッドではなく、公開インターフェースと振る舞いをテストします。内部をリファクタしてもテストが通り続けるのが良いテストです。

コマンドラインからテストを実行する

# Run a specific test file
python -m unittest test_calculator.py
 
# Run a specific test class
python -m unittest test_calculator.TestCalculator
 
# Run a specific test method
python -m unittest test_calculator.TestCalculator.test_add_positive_numbers
 
# Verbose output (shows each test name and result)
python -m unittest -v
 
# Discover all tests in a directory
python -m unittest discover -s tests -p "test_*.py" -v
 
# Stop on first failure (fail-fast mode)
python -m unittest -f

RunCell を使ったテスト作成とデバッグ

Jupyter notebook(データサイエンスや探索的プログラミングで一般的)で開発していると、ユニットテストの作成・実行はやや扱いづらく感じがちです。notebook はセルを対話的に実行しますが、unittest はモジュールとテストランナーを前提にしています。その結果、notebook とテストファイル間でコードをコピペすることになり、対話的なフィードバックループが崩れてしまいます。

RunCell (opens in a new tab) は、このギャップを埋める Jupyter 向けの AI agent です。notebook セルで定義した関数に対して unittest 互換のテストケースを生成し、notebook 環境内で実行し、失敗を文脈込みで説明できます。モック設定が誤っていたりアサーションが失敗したりした場合も、RunCell はライブ変数を調べて、単なるアサーションエラーメッセージではなく「実際の値が何だったか」を示します。DataFrame 変換が期待通りの shape と値を持つかを検証したいデータパイプラインでは、RunCell がテスト構造とアサーションを下準備してくれるため、ボイラープレートではなくロジックに集中できます。

FAQ

unittest と pytest の違いは?

unittest は Python 組み込みのテストフレームワークで、クラスベースの API を持ちます。pytest はサードパーティのフレームワークで、通常は関数と assert 文を使います。pytest はより豊富なプラグインエコシステムと優れた失敗出力を備えますが、インストールが必要です。unittest は追加依存なしで、Python が動く場所ならどこでも使えます。

unittest で単一のテストメソッドだけを実行するには?

python -m unittest test_module.TestClass.test_method を使います。例:python -m unittest test_calculator.TestCalculator.test_add_positive_numbers。これにより、同ファイル内の他のテストを実行せず、指定したメソッドだけが実行されます。

setUp と setUpClass の違いは?

setUp は各テストメソッドの前に毎回実行され、毎回新しい状態を作ります。setUpClass はクラス内の全テストの前に1回だけ実行される @classmethod です。DB 接続のような高コストな準備には setUpClass、テストごとに軽量な状態が必要な場合には setUp を使います。

unittest で外部 API をモックするには?

unittest.mock.patch で API 呼び出しを行う関数を差し替えます。パッチするのは「定義元」ではなく「使用している import パス」です。たとえば user_service.pyrequests.get を import して使っているなら、requests.get ではなく user_service.requests.get をパッチします。

pytest は unittest のテストを実行できる?

はい。pytest は unittest 形式のテストクラスと完全に互換です。unittest.TestCase を使っているプロジェクトでも修正なしで pytest を実行できます。移行を段階的に行えるため、既存の unittest テストを維持しつつ新規テストを pytest スタイルで書けます。

例外が発生することをテストするには?

self.assertRaises(ExceptionType) をコンテキストマネージャとして使います。with ブロック内のコードが指定例外を投げればテストは成功し、例外が出ない/別の例外の場合は失敗します。例外メッセージも検証したい場合は assertRaisesRegex を使います。

まとめ

Python の unittest フレームワークは、すべての Python 環境に同梱されている完全なテストツールキットです。テストケースクラス、豊富なアサーションメソッド、メソッド/クラスレベルの setup・teardown フック、unittest.mock によるモック機能、そして組み込みのテストディスカバリを提供します。信頼できるテストを書き始めるのに、何かをインストールする必要はありません。

基本はシンプルです。TestCase を継承し、メソッド名を test_ で始め、具体的なアサーションを使い、python -m unittest で実行します。プロジェクトが成長したら、テスト分離のために setUp/tearDown を追加し、外部依存をモックするために @patch を使い、パラメータ化には subTest を使いましょう。テストは tests/ ディレクトリに整理し、あとはテストディスカバリに任せれば十分です。

テストを書くには最初に時間がかかります。しかし後工程では、それ以上の時間を大きく節約します。ユニットテストが検知した回帰は、「起きなかった本番障害」であり、「届かなかった顧客クレーム」であり、「始まらなかったデバッグ」です。unittest を使い続ける場合でも、いずれ pytest に移行する場合でも、unittest のパターンで身につくテスト習慣(分離、明確なアサーション、依存のモック、エッジケースの網羅)は、Python のテスト全般に共通して通用します。

📚