salesforce-apex-quality について
Salesforce Apex のクラス、トリガー、テストをレビューするチェックリストです。明示的なルールに沿って、ガバナー制限やセキュリティ、クエリ、テスト品質を確認できます。
主な機能
- 01ループ内の SOQL と DML の検査
- 02sharing と CRUD/FLS の確認
- 03SOQL インジェクションの防止
- 04最新の Apex イディオムの指針
- 05正常・異常・一括テストの確認
- 06トリガー構成のルール
活用シーン
- 01マージ前の Apex サービスのレビュー
- 02ループ内の SOQL または DML の検出
- 03UI から呼ばれるメソッドの CRUD/FLS 確認
- 04一括処理のテストカバレッジ評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ github/ awesome-copilot --skill salesforce-apex-qualityインストール後、Claude Code で/salesforce-apex-qualityを入力して読み込まれたか確認します。
詳細
Salesforce Apex 品質ガードレール
作成またはレビューするすべての Apex クラス、トリガー、テストファイルに、これらのチェックを適用してください。
ステップ 1 — ガバナ制限の安全性チェック
Apex ファイルを許容可能と判断する前に、次のパターンがないか確認してください。
ループ内の SOQL と DML — 自動的に不合格
// ❌ NEVER — causes LimitException at scale
for (Account a : accounts) {
List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :a.Id]; // SOQL in loop
update a; // DML in loop
}
// ✅ ALWAYS — collect, then query/update once
Set<Id> accountIds = new Map<Id, Account>(accounts).keySet();
Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {
if (!contactsByAccount.containsKey(c.AccountId)) {
contactsByAccount.put(c.AccountId, new List<Contact>());
}
contactsByAccount.get(c.AccountId).add(c);
}
update accounts; // DML once, outside the loop
ルール:for ループ本体内に [SELECT、Database.query、insert、update、delete、upsert、merge がある場合は、そこで作業を止め、先にリファクタリングしてください。
ステップ 2 — 共有モデルの確認
すべてのクラスで、共有の意図を明示的に宣言する必要があります。共有が宣言されていない場合は呼び出し元から継承されるため、動作が予測できません。
| 宣言 | 使用する状況 |
|---|---|
public with sharing class Foo | すべてのサービス、ハンドラー、セレクター、コントローラークラスのデフォルト |
public without sharing class Foo | クラスを昇格された権限で実行する必要がある場合のみ(例:システムレベルのログ記録、トリガーのバイパス)。理由を説明するコードコメントが必要です。 |
public inherited sharing class Foo | 呼び出し元の共有コンテキストを尊重する必要があるフレームワークのエントリーポイント |
クラスにこれら3つの宣言のいずれもない場合は、ほかの作業を始める前に追加してください。
ステップ 3 — CRUD / FLS の適用
ユーザーに代わってレコードを読み書きする Apex コードでは、オブジェクトおよび項目へのアクセスを確認する必要があります。プラットフォームは Apex で FLS や CRUD を自動的に適用しません。
// Check before querying a field
if (!Schema.sObjectType.Contact.fields.Email.isAccessible()) {
throw new System.NoAccessException();
}
// Or use WITH USER_MODE in SOQL (API 56.0+)
List<Contact> contacts = [SELECT Id, Email FROM Contact WHERE AccountId = :accId WITH USER_MODE];
// Or use Database.query with AccessLevel
List<Contact> contacts = Database.query('SELECT Id, Email FROM Contact', AccessLevel.USER_MODE);
ルール:UI コンポーネント、REST エンドポイント、または @InvocableMethod から呼び出し可能な Apex メソッドでは、CRUD/FLS を必ず適用してください。信頼できるコンテキストからのみ呼び出される内部サービスメソッドでは、代わりに with sharing を使用できます。
ステップ 4 — SOQL インジェクションの防止
// ❌ NEVER — concatenates user input into SOQL string
String soql = 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\'';
// ✅ ALWAYS — bind variable
String soql = [SELECT Id FROM Account WHERE Name = :userInput];
// ✅ For dynamic SOQL with user-controlled field names — validate against a whitelist
Set<String> allowedFields = new Set<String>{'Name', 'Industry', 'AnnualRevenue'};
if (!allowedFields.contains(userInput)) {
throw new IllegalArgumentException('Field not permitted: ' + userInput);
}
ステップ 5 — 最新の Apex のイディオム
現在の言語機能(API 62.0 / Winter '25 以降)を優先してください。
| 古いパターン | 最新の代替パターン |
|---|---|
if (obj != null) { x = obj.Field__c; } | x = obj?.Field__c; |
x = (y != null) ? y : defaultVal; | x = y ?? defaultVal; |
System.assertEquals(expected, actual) | Assert.areEqual(expected, actual) |
System.assert(condition) | Assert.isTrue(condition) |
共有コンテキストなしの [SELECT ... WHERE ...] | [SELECT ... WHERE ... WITH USER_MODE] |
ステップ 6 — PNB テストカバレッジのチェックリスト
すべての機能を、次の3つのパスすべてでテストする必要があります。いずれかが欠けている場合は、品質上の不合格です。
正常系
- 期待される入力 → 期待される出力。
- 例外が発生しなかったことだけでなく、正確な項目値、レコード件数、または戻り値をアサートしてください。
異常系
- 無効な入力、null 値、空のコレクション、エラー条件。
- 正しい型とメッセージで例外がスローされることをアサートしてください。
- 操作が正常に失敗すべき場合に、レコードが変更されていないことをアサートしてください。
一括処理
- 1回のテストトランザクションで200~251件のレコードを挿入/更新/削除してください。
- すべてのレコードが正しく処理されたことをアサートしてください。ガバナ制限による部分的な失敗がないことを確認してください。
- 非同期処理のガバナ制限カウンターを分離するには、
Test.startTest()/Test.stopTest()を使用してください。
テストクラスのルール
@isTest(SeeAllData=false) // Required — no exceptions without a documented reason
private class AccountServiceTest {
@TestSetup
static void makeData() {
// Create all test data here — use a factory if one exists in the project
}
@isTest
static void givenValidInput_whenProcessAccounts_thenFieldsUpdated() {
// Positive path
List<Account> accounts = [SELECT Id FROM Account LIMIT 10];
Test.startTest();
AccountService.processAccounts(accounts);
Test.stopTest();
// Assert meaningful outcomes — not just no exception
List<Account> updated = [SELECT Status__c FROM Account WHERE Id IN :accounts];
Assert.areEqual('Processed', updated[0].Status__c, 'Status should be Processed');
}
}
ステップ 7 — トリガーアーキテクチャのチェックリスト
- オブジェクトごとにトリガーは1つにしてください。2つ目のトリガーがある場合は、ハンドラーに統合してください。
- トリガー本体に含めるのは、コンテキストの確認、ハンドラーの呼び出し、ルーティングロジックのみです。
- トリガー本体にビジネスロジック、SOQL、DMLを直接記述しないでください。
- トリガーフレームワーク(Trigger Actions Framework、ff-apex-common、カスタム基底クラス)がすでに使われている場合は、それを拡張してください。並行する別のパターンを作成しないでください。
- トリガーで昇格されたアクセス権が必要でない限り、ハンドラークラスには
with sharingを指定してください。
クイックリファレンス — ハードコードされたアンチパターンの概要
| パターン | 対応 |
|---|---|
for ループ内の SOQL | リファクタリング:ループの前にクエリを実行し、コレクションを操作します |
for ループ内の DML | リファクタリング:変更内容を収集し、ループの後に DML を一度だけ実行します |
| 共有宣言のないクラス | with sharing を追加します(または、without sharing にする理由を記載します) |
ユーザーデータに対する escape="false"(VF) | 削除します — 自動エスケープにより XSS を防止します |
空の catch ブロック | ログ記録と、適切な再スローまたはエラー処理を追加します |
| ユーザー入力を連結した SOQL 文字列 | バインド変数またはホワイトリスト検証に置き換えます |
| アサーションのないテスト | 意味のある Assert.* 呼び出しを追加します |
System.assert / System.assertEquals スタイル | Assert.isTrue / Assert.areEqual に更新します |
ハードコードされたレコード ID('001...') | クエリまたは挿入したテストレコードの ID に置き換えます |