この記事は Qiita Tech Festa 2026 の投稿記事です。
【連載】レガシーWebシステムを改修する技術:Feature Toggle, シャドーイング, リファクタリング
- 第1回:環境構築とFeature Toggle(フラグ管理)編
- 第2回:typing.Protocolとシャドーイング(ダークローンチ)編
- 第3回:仕様化テスト・TDD実践と足場のクリーンアップ編(本記事)
- 第4回:LLM Agentを活用した高速・安全なリファクタリング実践編
はじめに
第3回では、テスト手法と本番移行について解説します。
安全な改修には、コードの挙動を保証するテストが不可欠です。また、改修は新コードを稼働させて終わりではなく、「不要になった旧コードと足場を撤去する」ところまでが1つのタスクです。
実際の修正フローとの相違
トピックごとの説明にまとめた都合上、テストコードを順番に書いていますが、実際にはテストを作成で失敗させ、第2回の実装をしてテストの成功を確認するという変更のサイクルを繰り返してRed-Green TDDの形式で進めていきます
本題・実装:テストの実装
1. 既存コードへの外付けパッチテスト(仕様化テスト)
既存コードを変えずに pytest-mock (mocker フィクスチャ) を用いて、外部への副作用(print等)や戻り値のパターンの確認をします。
from app.services import OldDataProcessingService
def test_old_service_characterization(mocker):
service = OldDataProcessingService()
# 副作用を mocker でキャプチャしつつ、現在の振る舞い(仕様)を確定させるテスト
mock_print = mocker.patch('builtins.print')
payload = {"data": "hello"}
result = service.execute_pipeline(user_id=42, payload=payload)
# 現在の戻り値の構造をアサーションして保護(仕様化テスト)
assert result == {"status": "success", "legacy_id": 1142, "score": 50}
mock_print.assert_called_once_with("[SideEffect] Writing to Legacy DB for user 42")
ここだけは既存のコードがあるのでコード->テストの順番で作成し、テストを書くことで仕様を把握をすることになります。
2. 新コードの Red-Green TDD
typing.Protocol を定義したおかげで、テスト時には外部依存を容易にモックやフェイクに差し替えて、テストを実行しやすくなりました。
import pytest
from app.services import NewDataProcessingService, DataProcessingService
def test_new_service_conforms_to_protocol():
# 静的型チェッカー(mypy等)に適合しているかを明示的に示すアサーション代わり
# NewDataProcessingService が Protocol を満たしているかチェック
service: DataProcessingService = NewDataProcessingService()
assert service is not None
def test_new_service_logic_with_tdd():
# 新コードの単体テスト。外部依存がないので容易に Red -> Green のサイクルを回せる
service = NewDataProcessingService()
# 正常系のテスト
res1 = service.execute_pipeline(user_id=1, payload={"data": "abc"})
assert res1["score"] == 30
# 境界値・異常系のテスト (例: 空文字の時の仕様変更などをTDDで追加)
res2 = service.execute_pipeline(user_id=1, payload={"data": ""})
assert res2["score"] == 0
typing.Protocol を使うことで既存のコードの継承関係などを変更することなく、シグネチャの一致をさせることである程度の対応保証ができるので後からの追加や設計の変更にも対応が比較的しやすいと思います(最初から要件が分かっていれば abc.ABC で作っておくのももちろんよいと思いますが)。
アプリケーションのエントリポイント
FastAPIでリクエストを受け付けるルーター部分です。ここからは具体的な旧サービス・新サービスを意識せず、トグル付きのラッパーサービスを1つ呼び出す形に収めます。
import os
from fastapi import FastAPI
from app.services import FeatureToggledDataProcessingService
app = FastAPI(title="Safe Refactoring Web Server")
TOGGLE_URL = os.getenv("TOGGLE_SERVER_URL", "http://localhost:8001/toggles/use-new-service")
# ラップされたサービスをグローバルまたは依存注入(DI)で保持
processing_service = FeatureToggledDataProcessingService(toggle_url=TOGGLE_URL)
@app.post("/process/{user_id}")
def process_endpoint(user_id: int, payload: dict):
# 呼び出し箇所は一層Wrapされているため、コントローラーはリファクタリングの影響を受けない
result = processing_service.execute_pipeline(user_id=user_id, payload=payload)
return result
最終ステップ:安定稼働後の正式コード化(クリーンアップ)
シャドウモードやフラグONでの本番稼働において、ログに [Mismatch Detected] が一切出なくなり、不具合がないことを十分確認できたら、以下のクリーンアップを行います。
-
OldDataProcessingServiceクラスを丸ごと削除 -
FeatureToggledDataProcessingServiceを撤去し、NewDataProcessingServiceを直接呼ぶか、トグルを介さないシンプルな構成に書き換え - トグルサーバーから該当フラグの定義を削除
この 「足場の解体作業」までを「1つの修正タスク」として組み込む ことが重要です。
この対応パターンにおいて工程が多い分時間がかかりがちという弱点以上に、 使い終わった部分の削除処理が行われないために、以降のコードベースに不要なコードが残り続ける という負債化しがちな要素があります。
前回の FeatureToggledDataProcessingService もそれなりに複雑なコードでしたし、これを残したまま NewDataProcessingService の改修が入ってしまったり、同じ個所で複数のフラグを使うとフラグの削除でトラブルになってしまうこともあります。
ここはプロセスの重さと改修箇所が一回で修正して失敗した場合のリスクとの比較での決定だと思います。
おわりに
ここまで全3回にわたり、レガシーシステムを無停止で改修するためのリファクタリング手法の「土台作り」から「クリーンアップ」までを解説しました。
-
typing.Protocolによる抽象化 - Feature Toggle、シャドーイング
- テスト
これらを組み合わせることで、安心感を持ってコードを改善できます。
次回の 第4回(最終回) では、「LLM Agent」を活用した場合、今回の対応はどのようなプロンプトやフローで対応が可能かについて書こうと思います。