Python

テストとデバッグ

Python

pytest・unittest・logging・pdb

pytest の基本

テスト関数・アサーション・フィクスチャ

test_example.py python
import pytest

# === 基本的なテスト ===
def test_add():
    assert 1 + 1 == 2

def test_string():
    s = 'hello'
    assert s.upper() == 'HELLO'
    assert 'ell' in s
    assert s.startswith('h')

# 例外のテスト
def test_raises():
    with pytest.raises(ZeroDivisionError):
        1 / 0

    with pytest.raises(ValueError, match='invalid'):
        int('abc')

# === フィクスチャ ===
@pytest.fixture
def sample_user():
    return {'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}

def test_user_name(sample_user):
    assert sample_user['name'] == 'Alice'

# スコープ付きフィクスチャ
@pytest.fixture(scope='module')  # session, package, class, function
def db_connection():
    conn = create_db_connection()
    yield conn        # テスト後の後片付け
    conn.close()

# === パラメータ化 ===
@pytest.mark.parametrize('input, expected', [
    (2,  4),
    (3,  9),
    (4,  16),
    (-1, 1),
])
def test_square(input, expected):
    assert input ** 2 == expected

# 複数パラメーター
@pytest.mark.parametrize('a, b, expected', [
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
])
def test_add(a, b, expected):
    assert a + b == expected

モック・パッチ

unittest.mock・pytest-mock

test_mock.py python
from unittest.mock import Mock, MagicMock, patch, call
import pytest

# === Mock の基本 ===
mock = Mock()
mock.method()              # 呼び出し可能
mock.method.return_value = 42  # 戻り値を設定
mock.method()              # 42

mock.method.assert_called_once()        # 1回呼ばれたことを確認
mock.method.assert_called_with(1, 2)    # 引数も確認
mock.method.assert_not_called()
mock.method.call_count    # 呼び出し回数

# 副作用
mock.side_effect = ValueError('error')  # 例外を発生
mock.side_effect = [1, 2, 3]           # 呼ぶたびに返す値

# === patch ===
# 関数・クラス・属性を一時的に置き換える

# デコレーター
@patch('mymodule.requests.get')
def test_api(mock_get):
    mock_get.return_value.json.return_value = {'id': 1}
    result = mymodule.fetch_user(1)
    assert result['id'] == 1
    mock_get.assert_called_once_with('https://api.example.com/users/1')

# コンテキストマネージャー
def test_file_read():
    with patch('builtins.open', Mock(return_value=MockFile())):
        result = read_config('config.txt')
        assert result == {'key': 'value'}

# pytest-mock(mockerフィクスチャ)
def test_with_mocker(mocker):
    mock_get = mocker.patch('mymodule.requests.get')
    mock_get.return_value.status_code = 200
    mock_get.return_value.json.return_value = {'data': []}

    result = mymodule.fetch_all()
    assert result == []

ロギングとデバッグ

logging・pdb・型チェック・プロファイリング

logging_debug.py python
import logging

# 基本設定
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S',
)

# ロガーの取得(モジュール名で階層化)
logger = logging.getLogger(__name__)

# ログレベル(低→高): DEBUG < INFO < WARNING < ERROR < CRITICAL
logger.debug('詳細なデバッグ情報')
logger.info('一般的な情報')
logger.warning('警告(処理は続行)')
logger.error('エラー(処理に問題)')
logger.critical('致命的エラー')

# 例外のスタックトレース付き
try:
    risky()
except Exception:
    logger.exception('予期しないエラー')  # exc_info=True と同等

# ファイルへの出力
handler = logging.FileHandler('app.log', encoding='utf-8')
handler.setLevel(logging.WARNING)
logger.addHandler(handler)

# structlog(構造化ログ)
import structlog
log = structlog.get_logger()
log.info('ユーザーログイン', user_id=42, ip='192.168.1.1')

# pdb(デバッガー)
import pdb; pdb.set_trace()   # ブレークポイント(旧来)
breakpoint()                   # Python 3.7+(推奨)
# コマンド: n(next) s(step) c(continue) l(list) p(print) q(quit)

# プロファイリング
import cProfile
cProfile.run('my_function()', sort='cumulative')

# timeit(マイクロベンチマーク)
import timeit
timeit.timeit('[x**2 for x in range(1000)]', number=1000)