exp-test-maintainability について
.NET のテストコードを分析し、ファイルを変更せずに保守性の問題と具体的なリファクタリング案を示すスキルです。重複の実態を見極め、テストの明瞭さと独立性にも配慮します。
主な機能
- 01構築処理とアサーションの重複検出
- 02コピーされたテストメソッドの検出
- 03初期化処理とテスト基盤の確認
- 043回以上の出現を報告対象とする基準
- 05リファクタリング例とトレードオフの提示
活用シーン
- 01.NET テストプロジェクトの重複評価
- 02パラメーター化テスト候補の特定
- 03複数テストクラスの共通初期化の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill exp-test-maintainabilityインストール後、Claude Code で/exp-test-maintainabilityを入力して読み込まれたか確認します。
詳細
テストの保守性評価
.NET のテストコードを分析し、保守性に関する問題(定型コードの重複、コピー&ペーストされたテストメソッド、テストメソッドやクラス間の構造的な繰り返し)を特定します。具体的な修正前・修正後の提案を含む、リファクタリングの機会に関するレポートを作成します。目的は分析のみです。ファイルは変更しないでください。
使用する場面
- テスト内の重複コードや定型コードの特定を依頼された場合
- テストコードの重複を減らす方法を知りたい場合
- テストの重複を減らしたい、テストの可読性を高めたい、またはテストの定型コードを整理したいと依頼された場合
- テストスイートのリファクタリングの機会を尋ねられた場合
- 共通のセットアップや後処理の候補を特定したい場合
- 「テスト全体で繰り返されているパターンは何ですか?」と尋ねられた場合
- テストデータを一元化したい、ビルダーやヘルパーを導入したい場合
使用しない場面
- 新しいテストを一から作成したい場合(
writing-mstest-testsを使用してください) - アンチパターンやコードの臭いを検出したい場合(
test-anti-patternsを使用してください) - 実際にリファクタリングを行いたい場合(直接支援してください。このスキルは分析のみを行います)
入力
| 入力 | 必須 | 説明 |
|---|---|---|
| テストコード | はい | 分析対象の1つ以上のテストファイル、またはテストプロジェクトのディレクトリ |
| プロダクションコード | いいえ | 抽象化が役立つかどうかを判断するための、テスト対象コード |
| 範囲 | いいえ | 単一のクラス内で分析するか、複数のクラスにまたがって分析するか |
ワークフロー
ステップ 1:テストコードを収集する
ユーザーが提供または参照するすべてのテストファイルを読みます。ディレクトリまたはプロジェクトが指定された場合は、次のフレームワークのマーカーを使ってすべてのテストファイルをスキャンします。
| フレームワーク | テストクラスのマーカー | テストメソッドのマーカー |
|---|---|---|
| MSTest | [TestClass] | [TestMethod]、[DataTestMethod] |
| xUnit | (なし — 慣例に基づく) | [Fact]、[Theory] |
| NUnit | [TestFixture] | [Test]、[TestCase]、[TestCaseSource] |
| TUnit | (なし — 慣例に基づく) | [Test] |
ステップ 2:保守性の問題を特定する
次のカテゴリに沿ってスキャンします。
カテゴリ 1:オブジェクト生成の繰り返し
同一またはほぼ同一のパラメーターで、同じオブジェクトが3つ以上のテストメソッド内で生成されている箇所を探します。
指標:
- 複数のテストで同一の引数を指定した
new ClassName(...)が使われている - 複数のテストで、同じ「テスト対象」を似た設定で作成している
- 同じセットアップでモック/フェイク/スタブを繰り返し作成している
リファクタリング案:
- ファクトリーメソッドやテストヘルパーを抽出する(例:
CreateSut()、CreateDefaultOrder()) - 共通の生成処理に
[TestInitialize]/コンストラクター/[SetUp]を使う - 多くのバリエーションがある複雑なオブジェクトには、ビルダーパターンを導入する
例 — 修正前:
[TestMethod]
public void Process_ValidOrder_Succeeds()
{
var logger = new FakeLogger();
var email = new FakeEmailService();
var inventory = new FakeInventory(stock: 100);
var processor = new OrderProcessor(logger, email, inventory);
// ...
}
[TestMethod]
public void Process_EmptyItems_Fails()
{
var logger = new FakeLogger();
var email = new FakeEmailService();
var inventory = new FakeInventory(stock: 100);
var processor = new OrderProcessor(logger, email, inventory);
// ...
}
修正後 — ファクトリーを抽出:
private static OrderProcessor CreateProcessor(int stock = 100)
{
return new OrderProcessor(new FakeLogger(), new FakeEmailService(), new FakeInventory(stock));
}
カテゴリ 2:アサーションパターンの繰り返し
3つ以上のテストメソッド内で、同じ一連のアサーションが使われている箇所を探します。
指標:
- 複数のテストで、結果オブジェクトの同じプロパティ一式を検証している
- null チェック後に値をチェックする同じ処理が繰り返されている
- メソッド間で同じ
Assert.AreEqualの呼び出し一式が使われている
リファクタリング案:
- 独自のアサーションヘルパーを抽出する(例:
AssertValidOrder(order, expectedTotal, expectedStatus)) - フレームワーク固有のアサーション拡張を使う
- 標準のプロパティ一式を検証する
Verifyメソッドを導入する
カテゴリ 3:コピー&ペーストされたテストメソッド
入力値または単一のパラメーターのみが異なり、ほぼ同一の本体を持つテストメソッドを探します。
指標:
- 構造が同じで、リテラル値だけが異なるメソッドが 3 つ以上ある
[DataRow]/[Theory]/[TestCase]にまとめられるメソッドMethod_Input1_Result、Method_Input2_Resultのようなパターンに従ったテスト名
考えられるリファクタリング:
[DataRow]/[InlineData]/[TestCase]を使ったパラメーター化テストに変更する- 複雑な入力には
[DynamicData]/[MemberData]/[TestCaseSource]を使う - すべての値がコンパイル時定数である場合は、
[DynamicData]よりDisplayNameを指定した[DataRow]を優先します。[DynamicData]は計算が必要な値や複雑な値のために使用します。 - パラメーター値が直感的にわかりにくい場合は
DisplayNameを追加します。[DataRow("Gold", 100.0, 90.0)]は見れば内容がわかりますが、[DataRow(3, 7, 42)]はわかりにくい値です。
カテゴリ 4:セットアップ/ティアダウン処理の重複
テストクラス間で繰り返されている初期化コードやクリーンアップコードを探します。
指標:
- 本体が似ている
[TestInitialize]/[SetUp]メソッドが複数ある - データベースへのデータ投入、ファイル作成、HTTP クライアント設定の繰り返し
- クラス間で同じ
using/IDisposableクリーンアップパターンを使用している
考えられるリファクタリング:
- 共通のテスト基底クラスまたはフィクスチャを抽出する
- 共通のヘルパークラスを使ったコンポジションを利用する
- テストコンテキストファクトリーを作成する
カテゴリ 5:テストインフラストラクチャの繰り返し
テストクラス間で共通する構造パターンを探します。
指標:
- 複数のクラスで同じモックインターフェースを同じように設定している
- 類似した
DelegatingHandlerパターンを使ったHttpClient設定の繰り返し - テストクラス間で同じロギング/構成の土台を使用している
考えられるリファクタリング:
- 共通のテストフィクスチャまたはヘルパーライブラリを抽出する
- 再利用可能なフェイク実装を作成する
- テストハーネスクラスを導入する
ステップ 3:評価基準を適用する
報告する前に、次のルールに照らして検出結果を絞り込みます。
- 3 回以上の出現のみを報告します。 似たセットアップが 2 つあるだけなら、定型コードとは限りません。意図的に明確さを保っている可能性があります。
- 単純なコンストラクターは指摘しません。
new Calculator()やnew List<int>()は意味のある定型コードではありません。また、new User(1, "Alice")にビルダーを推奨しないでください。 - 意図的な冗長さを尊重します。 各テストが自己完結しており、単独で明確に読める場合は、テストごとにセットアップを明示することも有効な選択です。その点には触れても、問題として指摘しないでください。
- 構造上の類似性と実際の重複を区別します。 AAA(Arrange-Act-Assert)に従うテストは、当然ながら似た構造になります。実際のコードが(構造だけでなく)重複している場合に限り指摘してください。
- リファクタリングの影響範囲を考慮します。 20 個のテストで共有するヘルパーは結合を生みます。そのトレードオフに触れてください。
- テストがすでによく保守されている場合は、その旨を伝えてください。 軽微な改善の機会のみを見つけた報告でも、十分に妥当です。すでに優れている点も認めてください。
ステップ 4:検出結果を報告する
次の構成で検出結果を提示します。
- 概要 — カテゴリ別に、見つかったパターンの数を示します。テストスイートに問題がなければ、その旨から始めてください。
- カテゴリ別の検出結果 — 見つかったパターンごとに、次の内容を記載します。
- カテゴリ名と説明
- 該当箇所:対象となる具体的なテストメソッドとファイルの一覧
- 重複しているコードパターン(代表的な例を提示)
- 具体的な変更前/変更後の例を含む、推奨するリファクタリング
- 推定される効果:簡略化できる行数/メソッド数
- リファクタリングの優先順位 — 次の基準に従って検出結果を並べます。
- 出現回数(多いほど効果が大きい)
- 重複コードの複雑さ(複雑なセットアップを単純な構築より優先)
- リスク(リスクの低い抽出を優先)
- トレードオフ — 提案ごとに次の点を記載します。
- 得られる可読性
- 損なわれる局所性/独立性
- 出現回数を考慮して、実施する価値があるかどうか
検証
- すべての指摘に、具体的なファイルとメソッドの位置が含まれている
- すべての指摘に、説明だけでなく実際に重複しているコードが示されている
- すべての提案に、具体的な変更前/変更後の例が含まれている
- 指摘は3回以上の出現というしきい値に照らして絞り込まれている
- シンプルなコンストラクターは指摘対象にしない
- それぞれの提案について、トレードオフが明示されている
- テストに問題がない場合は、レポートの冒頭でその旨を伝えている
よくある落とし穴
| 落とし穴 | 解決策 |
|---|---|
| AAA構造を重複として指摘する | Arrange-Act-Assertパターンはボイラープレートではありません。実際のコードが繰り返されている場合にのみ指摘します |
| 出現回数が2回のコードの抽出を提案する | 抽出を推奨するのは3回以上出現してからにします |
| あらゆるものに基底クラスを推奨する | 継承よりもコンポジション(ヘルパー、ファクトリー)を優先します |
| 可読性への影響を無視する | 抽出するたびに間接性が増すため、そのトレードオフを明記します |
シンプルな new X() をボイラープレートとして指摘する | 複数のパラメーターや設定を伴う複雑な生成のみを指摘します |
| テストの分離性を犠牲にしてDRYを推奨する | ヘルパーを介して可変状態を共有するテストは密結合になるため、注意を促します |