Claude CodeでApexテストクラスのカバレッジを75%に上げる手順
sf apex run testの結果をclaude -pに渡し、カバレッジ未達のApexクラスにテストを足して再実行する手順です。75%の意味、権限設定、数字だけ満たす弱いテストの避け方まで扱います。
ApexのカバレッジをClaude Codeで75%まで上げるには、sf apex run testの結果をそのままclaude -pに渡し、未達のクラスにだけテストを足させて、同じコマンドで測り直します。測るのはSalesforce CLI、書くのはClaude、判定はもう一度の実測です。この分担にすると、Claudeに「カバレッジを上げて」とだけ頼んだときの、どのクラスが足りないのか分からないまま似たテストが増える状態を避けられます。
この記事は、Salesforce CLIが使えるサンドボックスかスクラッチ組織を前提にします。本番組織へのテスト実行やデプロイを、非対話のループに任せる構成は扱いません。
75%は何の数字で、どこで測るのか
Apexは、デプロイやパッケージ化の際にコードの75%がテストで実行されていることを求めます。Testing Best Practicesのページでも、デプロイやAppExchangeのパッケージ化の前に、ユニットテストがApexコードの75%以上をカバーし、すべて成功していることが条件として示されています。
ここで注意したいのは、測り方がテストレベルで変わる点です。sf apex run testのヘルプには、RunSpecifiedTestsを使うと、対象のクラスとトリガーそれぞれが個別に75%以上のカバレッジを求められると書かれています。組織全体の割合とは別の計算です。一方、RunLocalTestsは本番へApexを含むデプロイをするときの既定のテストレベルで、名前空間なしのローカルのテストを全部走らせます。
カバレッジの数字が指すもの
testRunCoverage
JSONとJUnit形式の結果に出る値です。今回のテスト実行で評価されたApexクラスについて、カバーされた行と全体の行から出した割合です。
RunSpecifiedTests時の75%
デプロイ対象のクラスとトリガーを、1つずつ個別に75%以上にします。全体平均で満たしても通りません。
RunLocalTests
本番へのデプロイで既定のレベルです。インストール済みの管理パッケージ由来のテストは走りません。
つまり「75%に上げる」と頼む前に、どの数字を75%にしたいのかを決めておく必要があります。クラス単位で詰めたいなら、Claudeに渡すのもクラス単位の結果です。この記事では、未達のクラスを1つずつ潰す進め方を取ります。平均を押し上げるために、すでに高いクラスを触らずに済むからです。
測定コマンドと、Claudeに渡す結果の形
測定には次のコマンドを使います。テストは組織の上で走るので、sf apex run testが見るのは組織にデプロイ済みのコードとテストです。ローカルで書いたテストは、デプロイしないと反映されません。
mkdir -p .coverage
sf apex run test \
--test-level RunLocalTests \
--code-coverage \
--result-format json \
--wait 30 \
--target-org my-sandbox > .coverage/last.jsonフラグの意味はsf apex run testのヘルプに従います。--code-coverageはカバレッジ結果の取得、--result-formatはhuman、tap、junit、jsonから選ぶ形式です。ヘルプによれば、コマンドは既定で非同期に走り、すぐテスト実行IDを返します。--waitに分数を渡すと、その時間内に終わった場合は結果まで表示します。終わらなければIDだけが返るので、sf apex get test --test-run-idで結果を取り直します。
なお、このコマンドの実行には「View All Data」システム権限が要ります。既定では無効で、管理者だけが有効にできる権限です。実行ユーザーにこの権限が無いと、ループ以前に測定で止まります。
出力は人が読む表でも構いませんが、Claudeに渡すならJSONが向きます。人が読む形式で全クラスの詳細カバレッジを出すと量が膨らむためです。単一クラスを見たいときは、--class-namesか--testsで対象を絞れます。
claude -pに渡して、未達クラスのテストを書かせる
ここからがClaude Codeの出番です。claude -pは標準入力を読むので、結果ファイルをパイプで渡せます。公式の非対話モードの説明にも、ビルドログを渡して原因を説明させる例があります。
cat .coverage/last.json | claude -p \
"このJSONはsf apex run test の結果です。カバレッジが75%未満の \
Apexクラスを一覧にし、その中から最も低いクラスを1つだけ選んで、 \
force-app 配下のテストクラスにテストメソッドを足してください。 \
本体クラスは変更しないこと。" \
--allowedTools "Read,Edit"claude -pで渡すときの注意点は次の3つです。
- 標準入力として渡せるのは10MBまでです。上限を超えると、Claude Codeは明確なエラーを出して、0以外の終了コードで終わります。大きな組織で全テストの詳細を出すと近づくので、
--class-namesで対象を絞るか、結果をファイルに保存して、パスだけをプロンプトに書く形にしてください。ログを渡す際の絞り方はclaude -pにログを渡すと10MB上限で止まるで扱っています。 --allowedTools "Read,Edit"は、読み取りと編集を確認なしで許可するだけです。シェルの実行は許可していないので、Claudeはテストを走らせられません。測定はこの後でスクリプトが担います。- v2.1.302以降の
claude -pは、権限モードが指定されていないとManualモードで始まります。許可していない操作は拒否されるため、Edit以外の操作をさせたいときは、必要な分だけ--allowedToolsに足してください。
プロンプトの「1つだけ」は意図的な制限です。未達のクラスを全部まとめて頼むと、差分が大きくなり、どのテストがどのクラスを担当しているのかが追いにくくなります。
足されたテストの例と、数字だけ満たす弱いテスト
例として、次のクラスの分岐が全く通っていない状況を考えます。
public class DiscountCalculator {
public static Decimal calc(Decimal amount, String tier) {
if (amount == null || amount <= 0) {
return 0;
}
if (tier == 'GOLD') {
return amount * 0.2;
}
if (tier == 'SILVER') {
return amount * 0.1;
}
return 0;
}
}例えば次のような形のテストが返ってくれば、分岐ごとに期待値を置けています(この例は説明用に作ったもので、Claudeの実出力ではありません)。
@IsTest
private class DiscountCalculatorTest {
@IsTest
static void goldGetsTwentyPercent() {
System.assertEquals(20, DiscountCalculator.calc(100, 'GOLD'));
}
@IsTest
static void silverGetsTenPercent() {
System.assertEquals(10, DiscountCalculator.calc(100, 'SILVER'));
}
@IsTest
static void nullOrNonPositiveAmountReturnsZero() {
System.assertEquals(0, DiscountCalculator.calc(null, 'GOLD'));
System.assertEquals(0, DiscountCalculator.calc(-5, 'GOLD'));
}
@IsTest
static void unknownTierReturnsZero() {
System.assertEquals(0, DiscountCalculator.calc(100, 'BRONZE'));
}
}逆に、カバレッジだけを満たす弱いテストは、DiscountCalculator.calc(100, 'GOLD');と呼ぶだけで、戻り値を検証しません。行は実行されるので数字は上がりますが、割引率が間違っていても落ちません。Apexのユニットテストの解説でも、サンプルのテストクラスは「カバレッジは足りているが、境界条件を確かめるテストも加えている」と説明されています。数字より、期待値を置くほうが本筋です。
そこで、プロンプトかCLAUDE.mdに、次のような規約を置いておきます。
## Apexテストの書き方
- すべてのテストメソッドに System.assertEquals か System.assertNotEquals を置く
- 戻り値が無いメソッドは、変化した項目やレコード件数を検証する
- 1つのif分岐に、1つ以上のテストメソッドを対応させる
- テストデータはテスト内(または@testSetup)で作り、SeeAllDataは使わない
- 非同期処理を検証するときは、Test.startTestとTest.stopTestで挟み、検証はstopTestの後に書く
- HTTPコールアウトは実際に飛ばさず、モックを使う
- 本体クラスは変更しない。変更が必要なら理由だけ報告するApexテストで外せない制約
テストを書かせる際にClaudeへ伝えておくべき制約は、Apexのユニットテストの解説にまとまっています。
- テストメソッドは引数を取らず、データベースにコミットせず、メールも送りません。テストで作ったデータは残らないので、後片付けは要りません
- テストメソッドは
@IsTestを付けたクラスに置きます。API 28.0以降は、テスト以外のクラスにテストメソッドを置けません - Webサービスのコールアウトはテストで呼べません。モックのコールアウトを使います
- テストクラスやテストメソッドで静的変数の値を変えても、その値は他のテストメソッドに引き継がれません
- 履歴系のレコード(項目履歴のAccountHistoryなど)は、親レコードがコミットされていないと作られないため、テストメソッドの中では作れません
組織の75%とは別に、トリガーにも最低限のカバレッジが要る
Testing Best Practicesのページには、すべてのトリガーに何らかのテストカバレッジが必要だと書かれています。組織全体の割合が75%を超えていても、テストが1つも触れていないトリガーがあるとデプロイは通りません。結果JSONでカバレッジが0%のトリガーを見つけたら、クラスより先にそちらを頼んでください。同じページには、条件分岐(三項演算子を含む)は全部の枝を通すこと、有効な入力と無効な入力の両方で呼ぶこと、System.debugの呼び出しとテストクラス自体はカバレッジに数えられないことも載っています。
ガバナ制限とテストデータの作り方
Claudeが書いたテストは、データの準備と実行がごちゃ混ぜになりがちです。Apexのドキュメントは次の分け方を示しています。
Test.startTest()とTest.stopTest()は、1つのテストメソッドで1回ずつだけ呼べます。この2つの間のコードには、新しいガバナ制限が割り当てられます。変数の初期化やデータの投入はstartTestの前に済ませますstopTestが実行されると、非同期処理(バッチやFutureなど)は同期的に実行されます。非同期処理の結果を検証したいときは、stopTestの後に書きます- 複数のテストメソッドで共通のデータは、
@testSetupを付けたメソッドで一度だけ作れます。このメソッドはクラス内のどのテストメソッドよりも先に実行され、あるテストメソッドが変更や削除をしても、次のテストメソッドには元の状態が見えます - テストメソッドは、組織にもともとあるデータ(標準オブジェクト、カスタムオブジェクト、カスタム設定)に既定では触れません。触れるのは自分で作ったデータだけです。
@IsTest(SeeAllData=true)を付ければ制限を外せますが、ドキュメントは、可能な限りテストごとにデータを作るよう勧めています
最後の点は、サンドボックスで試していて「組織にあるはずのレコードが見つからない」と言ってテストが失敗するときの原因になります。SeeAllData=trueで通すより、レコードを作る処理をテストに書かせるほうが、別の組織でも同じ結果になります。プロンプトかCLAUDE.mdに「テストデータはテスト内で作る」と一行足しておくと、そこで迷いません。
特にコールアウトを含むクラスは、モックを書かないとテストが失敗します。Claudeに頼むときは、「コールアウトがあるクラスはモックを使う」と明記しておくと、その場で気づけます。また、トリガーは複数レコードをまとめて処理する前提で動くので、対象がトリガーやその呼び出し先のクラスなら、1件だけでなく複数件のレコードを入れるテストも頼むと確実です。Testing Best Practicesは、バルクの動作を試すのに20件以上のレコードを使うよう示しています。
測り直しまでを回すスクリプト
最後に、書く、デプロイする、測る、をまとめてループにします。デプロイは普段お使いのコマンドに置き換えてください。
#!/usr/bin/env bash
set -euo pipefail
ORG=my-sandbox
mkdir -p .coverage
measure() {
sf apex run test --test-level RunLocalTests \
--code-coverage --result-format json \
--wait 30 --target-org "$ORG" > .coverage/last.json
}
measure
for round in 1 2 3; do
cat .coverage/last.json | claude -p \
"75%未満のApexクラスを1つだけ選び、そのテストを足す。本体は変更しない。" \
--allowedTools "Read,Edit"
# テストクラス以外が変わっていたら止める
if git diff --name-only | grep -v 'Test\.cls'; then
echo "テストクラス以外が変更されました" >&2
exit 1
fi
<普段のデプロイコマンド> # 追加したテストを組織に反映する
measure
grep -o '"testRunCoverage"[^,]*' .coverage/last.json || true
donegit diff --name-only | grep -v 'Test\.cls'は、変更されたファイルのうちテストクラス以外を探す行です。本体クラスが書き換わっていれば、その名前が表示され、ループが止まります。Claudeに「本体を変更しない」と頼むだけでなく、スクリプトの側でも機械的に確かめるための仕掛けです。
testRunCoverageの行は、ヘルプにある通り、評価されたApexクラスの全体の行に対するカバー済みの行の割合です。JSON内の正確な位置や値の書式は、お使いのバージョンの出力で一度確かめてから、判定に使ってください。3周で足りなければ、手作業で止めて差分を読むほうが早いはずです。テストを足したのに数字が動かない場合は、デプロイが失敗しているか、足したテストが別のクラスを見ているのが典型です。
同じ考え方でPythonやGoのテストを足す流れは、Claude Codeでカバレッジの穴を埋めるテスト追加の手順にあります。CIで失敗を直す側の自動化はClaude CodeでCIのテスト失敗を自動修正で扱っています。
まとめ
75%という数字は、テストレベルによって「全体」か「クラスごと」かが変わります。決めたいのは、渡す結果の範囲と、Claudeに触らせるファイルの範囲です。測定はsf apex run test、テスト作成はclaude -p、変更範囲の検査はgit diffと、道具ごとに担当を切ると、数字だけ満たしたテストが混ざっても気づけます。最後は、足されたテストが期待値を検証しているかを、差分で一度読んでください。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- code.claude.com/docs/en/headless
- code.claude.com/docs/en/permissions
- code.claude.com/docs/en/common-workflows
- github.com/salesforcecli/plugin-apex
- developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_testing_unit_…
- developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_testing_best_…
- developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_testing_tools…
- developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_testing_data_…
- developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_testing_tests…