はじめに
.NET Framework / WPF の業務アプリに、Claude Code を使って MSTest の単体テストを後付けしました。
対象は、パン屋さんの「作業実績入力アプリ(店頭タブレット版)」のような画面だと思ってください。
もとのアプリは、テストのしやすさを考えて作られてはいません。
- ViewModel の中でいきなり
new DbContext()している - static なユーティリティ(ログイン情報・ログ出力)を直接呼んでいる
- 画面にはライセンス付きのサードパーティ製コントロールが並んでいる
結果として、カバレッジ 63% → 92%、284 件のテストが合格するところまで持っていけました。
この記事では、そのときに意識したことを 3 つに絞って紹介します。
- 守るルールを先に決めて、AI に「段取り」から任せる
- ヘルパーメソッドを作りすぎない
- 検証を「強く」する
1. 守るルールを先に決めて、AI に「段取り」から任せる
進め方
① チーム共通の手順書(AGENTS.md)を用意する
② Claude に「テストを書くためのプロンプト」を作らせる
③ 人間がプロンプトをレビューして直す
④ 新しいセッションにそのプロンプトを渡して、テストを書かせる
⑤ カバレッジを見て「もう少し上げて」と追加で頼む
⑥ ハマりどころを Claude のメモリに残し、次の画面に生かす
ポイントは、いきなり「テストを書いて」と頼まないことです。
最初は、次のように「プロンプトを作って」と頼みます。
〇〇画面に MSTest 用のテストコードを作成するためのプロンプトを作成してください。
コードブロックで出力してください。
― 対象と参考資料
(手順書のパス)
(テスト対象のフォルダ)
(モックライブラリの README のパス)
Claude が対象コードと資料を読み、「何を・どの順で・何を守ってやるか」を書き出してくれます。
人間はそれを読んで直すだけなので、実行する前に方針をレビューできます。
プロンプトに必ず入れた「制約」
## 制約(必ず守る)
1. 本体のプロダクションコードは変更しない。
テストしやすくするための改修が必要なら、実施せずに提案として報告する。
2. 実 DB に接続しない。App.config に接続文字列を置かず、DB に届く経路は fake に差し替える。
3. ライセンス付きコントロールのライセンス確認を回避しない。
4. 目標に届かない箇所は、「到達できない理由」と「業務コードをどう変えればテストできるか」を報告する。
「やらないこと」を書いておかないと、AI は気を利かせて本体をリファクタリングしたり、ライセンスチェックを回避したりしかねません。
4 番目も効きました。
AI が無理に数字を合わせにいかず、生成できない画面は理由を書いて Inconclusive にする、という誠実な形で返してくれます。
「もう一押し」は数字で頼む
最初のセッションが終わった時点で、カバレッジは 63% でした。
次のセッションで頼んだのは、これだけです。
今の全体のカバレッジが 63% なので、もう少しカバレッジを上げてください
Claude はカバレッジレポートから未到達の分岐を探し、必要な仕組みを自分で足して、92% まで上げてくれました。
ゴールが数字だと、AI は次に何をすべきかを自分で判断できます。
途中でわかった落とし穴は、Claude のメモリに残させました。
- DbContext の引数なしコンストラクタは、裏で接続確認をする
- ライセンス未登録のコントロールを生成すると、テストホストが落ちる
おかげで、次の画面(事務所 PC 版)は、そこを最初から避けて始められました。
2. ヘルパーメソッドを作りすぎない
AI にテストを書かせると、共通化したがります。
ArrangeDefaultData()、AssertResultIsValid()、CreateModelWithStandardSetup() のようなヘルパーが、どんどん増えていきます。
一見きれいですが、レビューする側はつらくなります。
- テスト 1 本を読むのに、ヘルパーを何段も追いかけないといけない
- 「このテストは何を準備して、何を確認しているのか」が、テストメソッドだけを読んでもわからない
- ヘルパーにバグがあると、全テストがそろって間違った形で通ってしまう
AI が書いたコードを人間がレビューする以上、読みやすさはレビューのコストに直結します。
線引き
| 共通化した(ヘルパーにした) | 共通化しなかった(テストに直接書く) |
|---|---|
| テスト対象 DLL の読み込み | テストデータの準備 |
| STA スレッドでの実行 | テスト対象の呼び出し |
| 偽の DB・ログ・メッセージボックスへの差し替え | 期待値と Assert |
「どのテストでも同じで、テストの意味に関係しない部分」だけをヘルパーにしました。
準備・実行・検証は、テストメソッドの中に全部見えるように書きます。
[STATestMethod]
public void 焼成中か判定_該当する作業中データが無い_falseを返す()
{
// 準備:何を用意したかが、この場で全部見える
var db = new FakeBakeryDb();
db.焼成記録Set.Items.Add(new 焼成記録 { 商品番号 = 5, 担当者番号 = 10, 終了日時 = DateTime.Today });
db.焼成記録Set.Items.Add(new 焼成記録 { 商品番号 = 6, 担当者番号 = 10, 終了日時 = null });
using (var s = ShimSession.Create(db: db)) // ← 共通化しているのは差し替えの土台だけ
{
var model = s.Create(ModelType);
// 実行と検証:条件ごとに理由をメッセージで添える
Assert.AreEqual(false, Target.Call(model, "IsBaking", IntInt, 10, 5), "終了済み");
Assert.AreEqual(false, Target.Call(model, "IsBaking", IntInt, 99, 6), "別担当者");
}
}
多少ベタ書きで重複しても、テスト 1 本を上から読めば意味がわかることを優先しました。
3. 検証を「強く」する
AI に任せると、こういうテストが出てきがちです。
// 弱い検証の例
Assert.IsNotNull(requester); // 生成できた
Assert.IsNotNull(showMethod, "Show メソッドが存在すること"); // メソッドがある
// 例外が出なければ OK(Assert すらない)
これでもカバレッジは上がります。
でも、処理が間違った値を返しても通ってしまうので、テストとしての価値はほとんどありません。
そこで、「エラーが出ない」「値が存在する」ではなく、「エラーが出ずに、〇〇の値が設定されていること」まで確認するようにしました。
[STATestMethod]
public void 焼成中か判定_DB例外_falseを返しエラーログを出す()
{
var db = new FakeBakeryDb { Failure = new InvalidOperationException("DB障害") };
using (var s = ShimSession.Create(db: db))
{
var model = s.Create(ModelType);
// 例外が外に漏れないこと「だけ」ではなく、戻り値とログの中身まで確認する
Assert.AreEqual(false, Target.Call(model, "IsBaking", IntInt, 10, 5));
CollectionAssert.Contains(s.LogMessages, "例外エラー [エラーメッセージ:DB障害]");
}
}
[STATestMethod]
public void 対象日_同じ値は通知せず_違う値は設定して通知する()
{
var model = Target.Create(ModelType);
var rec = new PropertyChangedRecorder(model);
Target.Set(model, "targetDate", DateTime.Today);
Target.Set(model, "targetDate", DateTime.Today.AddDays(-1));
// 「通知が来た」ではなく、「どのプロパティが・どの順で・何回」通知されたかを確認する
Assert.AreEqual(DateTime.Today.AddDays(-1), Target.Get(model, "targetDate"));
CollectionAssert.AreEqual(new[] { "targetDate", "targetDate", "CanEdit" }, rec.Names);
}
弱い検証から強い検証へ
| 弱い検証 | 強い検証 |
|---|---|
| 例外が出ない | 例外が出ず、戻り値が false で、ログに決まった文言が出ている |
IsNotNull(result) |
AreEqual(期待値, result) |
| リストが空ではない | 件数と中身が期待どおり(CollectionAssert.AreEqual) |
| 通知が来た | どのプロパティが、どの順で、何回通知されたか |
| メッセージボックスが出た | どの文言で出たか |
この方針はプロンプトでも伝え、レビューでも「この Assert で、間違った実装を見逃さないか?」を必ず確認しました。
ログやメッセージボックスの文言まで確認できるのは、それらを「記録するだけ」の偽物に差し替えているからです。
差し替えるときに「何が渡されたか」を記録しておくと、検証をぐっと強くできます。
人間が確認したこと
AI に任せる部分が大きいほど、最後の確認は人間の仕事です。
- 実 DB につながっていないか:「接続文字列がないこと」自体をテストにしておく
-
本体のファイルが変わっていないか:ビルドのはずみで、成果物フォルダの DLL が上書きされていたことがあった。コミット前に
git statusで確認する -
Inconclusiveの理由が妥当か:「テストできない」と言っている箇所が、本当にできないのかを読む - Assert が弱くないか:上の表に当てはめて確認する
まとめ
Claude Code にテストを書かせるときに意識したのは、次の 3 つでした。
-
守るルールと終わりの条件を、最初にプロンプトで渡す
いきなり書かせず、まずプロンプトを作らせてレビューする。 -
ヘルパーを作りすぎない
共通化は「テストの意味に関係しない土台」だけにする。テスト 1 本を読めば意味がわかることを優先する。 -
検証を強くする
「エラーが出ない」「値がある」ではなく、「エラーが出ずに、〇〇の値になっている」まで確認する。
AI はテストを速く大量に書いてくれます。
だからこそ、人間が読めて、間違いを本当に見つけられるテストにすることが大事だと感じました。