scc-universal 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +44 -0
- package/.cursor/agents/deep-researcher.md +142 -0
- package/.cursor/agents/doc-updater.md +219 -0
- package/.cursor/agents/eval-runner.md +335 -0
- package/.cursor/agents/learning-engine.md +210 -0
- package/.cursor/agents/loop-operator.md +245 -0
- package/.cursor/agents/refactor-cleaner.md +119 -0
- package/.cursor/agents/sf-admin-agent.md +127 -0
- package/.cursor/agents/sf-agentforce-agent.md +126 -0
- package/.cursor/agents/sf-apex-agent.md +117 -0
- package/.cursor/agents/sf-architect.md +426 -0
- package/.cursor/agents/sf-aura-reviewer.md +369 -0
- package/.cursor/agents/sf-bugfix-agent.md +101 -0
- package/.cursor/agents/sf-flow-agent.md +155 -0
- package/.cursor/agents/sf-integration-agent.md +141 -0
- package/.cursor/agents/sf-lwc-agent.md +123 -0
- package/.cursor/agents/sf-review-agent.md +357 -0
- package/.cursor/agents/sf-visualforce-reviewer.md +465 -0
- package/.cursor/hooks/adapter.js +81 -0
- package/.cursor/hooks/after-file-edit.js +26 -0
- package/.cursor/hooks/after-mcp-execution.js +12 -0
- package/.cursor/hooks/after-shell-execution.js +30 -0
- package/.cursor/hooks/after-tab-file-edit.js +12 -0
- package/.cursor/hooks/before-mcp-execution.js +11 -0
- package/.cursor/hooks/before-read-file.js +13 -0
- package/.cursor/hooks/before-shell-execution.js +29 -0
- package/.cursor/hooks/before-submit-prompt.js +23 -0
- package/.cursor/hooks/pre-compact.js +7 -0
- package/.cursor/hooks/session-end.js +10 -0
- package/.cursor/hooks/session-start.js +10 -0
- package/.cursor/hooks/stop.js +18 -0
- package/.cursor/hooks/subagent-start.js +10 -0
- package/.cursor/hooks/subagent-stop.js +10 -0
- package/.cursor/hooks.json +107 -0
- package/.cursor/skills/aside/SKILL.md +115 -0
- package/.cursor/skills/checkpoint/SKILL.md +50 -0
- package/.cursor/skills/configure-scc/SKILL.md +160 -0
- package/.cursor/skills/continuous-agent-loop/SKILL.md +260 -0
- package/.cursor/skills/mcp-server-patterns/SKILL.md +142 -0
- package/.cursor/skills/model-route/SKILL.md +81 -0
- package/.cursor/skills/prompt-optimizer/SKILL.md +366 -0
- package/.cursor/skills/refactor-clean/SKILL.md +133 -0
- package/.cursor/skills/resume-session/SKILL.md +111 -0
- package/.cursor/skills/save-session/SKILL.md +183 -0
- package/.cursor/skills/search-first/SKILL.md +140 -0
- package/.cursor/skills/security-scan/SKILL.md +142 -0
- package/.cursor/skills/sessions/SKILL.md +124 -0
- package/.cursor/skills/sf-agentforce-development/SKILL.md +449 -0
- package/.cursor/skills/sf-apex-async-patterns/SKILL.md +324 -0
- package/.cursor/skills/sf-apex-best-practices/SKILL.md +421 -0
- package/.cursor/skills/sf-apex-constraints/SKILL.md +79 -0
- package/.cursor/skills/sf-apex-cursor/SKILL.md +336 -0
- package/.cursor/skills/sf-apex-enterprise-patterns/SKILL.md +344 -0
- package/.cursor/skills/sf-apex-testing/SKILL.md +407 -0
- package/.cursor/skills/sf-api-design/SKILL.md +237 -0
- package/.cursor/skills/sf-approval-processes/SKILL.md +312 -0
- package/.cursor/skills/sf-aura-development/SKILL.md +260 -0
- package/.cursor/skills/sf-build-fix/SKILL.md +120 -0
- package/.cursor/skills/sf-data-modeling/SKILL.md +274 -0
- package/.cursor/skills/sf-debugging/SKILL.md +362 -0
- package/.cursor/skills/sf-deployment/SKILL.md +291 -0
- package/.cursor/skills/sf-deployment-constraints/SKILL.md +153 -0
- package/.cursor/skills/sf-devops-ci-cd/SKILL.md +322 -0
- package/.cursor/skills/sf-docs-lookup/SKILL.md +100 -0
- package/.cursor/skills/sf-e2e-testing/SKILL.md +321 -0
- package/.cursor/skills/sf-experience-cloud/SKILL.md +248 -0
- package/.cursor/skills/sf-flow-development/SKILL.md +376 -0
- package/.cursor/skills/sf-governor-limits/SKILL.md +319 -0
- package/.cursor/skills/sf-harness-audit/SKILL.md +139 -0
- package/.cursor/skills/sf-help/SKILL.md +156 -0
- package/.cursor/skills/sf-integration/SKILL.md +479 -0
- package/.cursor/skills/sf-lwc-constraints/SKILL.md +128 -0
- package/.cursor/skills/sf-lwc-development/SKILL.md +302 -0
- package/.cursor/skills/sf-lwc-testing/SKILL.md +387 -0
- package/.cursor/skills/sf-metadata-management/SKILL.md +285 -0
- package/.cursor/skills/sf-platform-events-cdc/SKILL.md +372 -0
- package/.cursor/skills/sf-quickstart/SKILL.md +170 -0
- package/.cursor/skills/sf-security/SKILL.md +330 -0
- package/.cursor/skills/sf-security-constraints/SKILL.md +125 -0
- package/.cursor/skills/sf-soql-constraints/SKILL.md +129 -0
- package/.cursor/skills/sf-soql-optimization/SKILL.md +353 -0
- package/.cursor/skills/sf-tdd-workflow/SKILL.md +332 -0
- package/.cursor/skills/sf-testing-constraints/SKILL.md +198 -0
- package/.cursor/skills/sf-trigger-constraints/SKILL.md +88 -0
- package/.cursor/skills/sf-trigger-frameworks/SKILL.md +343 -0
- package/.cursor/skills/sf-visualforce-development/SKILL.md +259 -0
- package/.cursor/skills/strategic-compact/SKILL.md +205 -0
- package/.cursor/skills/update-docs/SKILL.md +162 -0
- package/.cursor/skills/update-platform-docs/SKILL.md +86 -0
- package/.cursor-plugin/plugin.json +26 -0
- package/LICENSE +21 -0
- package/README.md +522 -0
- package/agents/deep-researcher.md +145 -0
- package/agents/doc-updater.md +222 -0
- package/agents/eval-runner.md +340 -0
- package/agents/learning-engine.md +211 -0
- package/agents/loop-operator.md +247 -0
- package/agents/refactor-cleaner.md +122 -0
- package/agents/sf-admin-agent.md +131 -0
- package/agents/sf-agentforce-agent.md +132 -0
- package/agents/sf-apex-agent.md +124 -0
- package/agents/sf-architect.md +435 -0
- package/agents/sf-aura-reviewer.md +372 -0
- package/agents/sf-bugfix-agent.md +105 -0
- package/agents/sf-flow-agent.md +159 -0
- package/agents/sf-integration-agent.md +146 -0
- package/agents/sf-lwc-agent.md +127 -0
- package/agents/sf-review-agent.md +366 -0
- package/agents/sf-visualforce-reviewer.md +468 -0
- package/assets/logo.svg +18 -0
- package/docs/ARCHITECTURE.md +133 -0
- package/docs/authoring-guide.md +373 -0
- package/docs/hook-development.md +578 -0
- package/docs/token-optimization.md +139 -0
- package/docs/workflow-examples.md +645 -0
- package/examples/agentforce-action/README.md +227 -0
- package/examples/apex-trigger-handler/README.md +114 -0
- package/examples/devops-pipeline/README.md +325 -0
- package/examples/flow-automation/README.md +188 -0
- package/examples/integration-pattern/README.md +416 -0
- package/examples/lwc-component/README.md +180 -0
- package/examples/platform-events/README.md +492 -0
- package/examples/scratch-org-setup/README.md +138 -0
- package/examples/security-audit/README.md +244 -0
- package/examples/visualforce-migration/README.md +314 -0
- package/hooks/hooks.json +338 -0
- package/hooks/memory-persistence/README.md +73 -0
- package/manifests/install-modules.json +217 -0
- package/manifests/install-profiles.json +17 -0
- package/mcp-configs/mcp-servers.json +19 -0
- package/package.json +89 -0
- package/schemas/hooks.schema.json +123 -0
- package/schemas/install-modules.schema.json +76 -0
- package/schemas/install-profiles.schema.json +28 -0
- package/schemas/install-state.schema.json +73 -0
- package/schemas/package-manager.schema.json +18 -0
- package/schemas/plugin.schema.json +112 -0
- package/schemas/scc-install-config.schema.json +29 -0
- package/schemas/state-store.schema.json +111 -0
- package/scripts/cli/install-apply.js +170 -0
- package/scripts/cli/uninstall.js +193 -0
- package/scripts/hooks/check-console-log.js +101 -0
- package/scripts/hooks/check-hook-enabled.js +17 -0
- package/scripts/hooks/check-platform-docs-age.js +48 -0
- package/scripts/hooks/cost-tracker.js +78 -0
- package/scripts/hooks/doc-file-warning.js +63 -0
- package/scripts/hooks/evaluate-session.js +98 -0
- package/scripts/hooks/governor-check.js +220 -0
- package/scripts/hooks/learning-observe.sh +206 -0
- package/scripts/hooks/mcp-health-check.js +588 -0
- package/scripts/hooks/post-bash-build-complete.js +34 -0
- package/scripts/hooks/post-bash-pr-created.js +43 -0
- package/scripts/hooks/post-edit-console-warn.js +61 -0
- package/scripts/hooks/post-edit-format.js +79 -0
- package/scripts/hooks/post-edit-typecheck.js +98 -0
- package/scripts/hooks/post-write.js +168 -0
- package/scripts/hooks/pre-bash-git-push-reminder.js +35 -0
- package/scripts/hooks/pre-bash-tmux-reminder.js +47 -0
- package/scripts/hooks/pre-compact.js +51 -0
- package/scripts/hooks/pre-tool-use.js +163 -0
- package/scripts/hooks/pre-write-doc-warn.js +9 -0
- package/scripts/hooks/quality-gate.js +251 -0
- package/scripts/hooks/run-with-flags-shell.sh +32 -0
- package/scripts/hooks/run-with-flags.js +135 -0
- package/scripts/hooks/session-end-marker.js +29 -0
- package/scripts/hooks/session-end.js +311 -0
- package/scripts/hooks/session-start.js +202 -0
- package/scripts/hooks/sfdx-scanner-check.js +142 -0
- package/scripts/hooks/sfdx-validate.js +119 -0
- package/scripts/hooks/stop-hook.js +170 -0
- package/scripts/hooks/suggest-compact.js +67 -0
- package/scripts/lib/agent-adapter.js +82 -0
- package/scripts/lib/apex-analysis.js +194 -0
- package/scripts/lib/hook-flags.js +74 -0
- package/scripts/lib/install-config.js +73 -0
- package/scripts/lib/install-executor.js +363 -0
- package/scripts/lib/install-state.js +121 -0
- package/scripts/lib/orchestration-session.js +299 -0
- package/scripts/lib/package-manager.js +124 -0
- package/scripts/lib/project-detect.js +228 -0
- package/scripts/lib/schema-validator.js +190 -0
- package/scripts/lib/skill-adapter.js +100 -0
- package/scripts/lib/state-store.js +376 -0
- package/scripts/lib/tmux-worktree-orchestrator.js +598 -0
- package/scripts/lib/utils.js +313 -0
- package/scripts/scc.js +164 -0
- package/skills/_reference/AGENTFORCE_PATTERNS.md +112 -0
- package/skills/_reference/APEX_CURSOR.md +159 -0
- package/skills/_reference/API_VERSIONS.md +78 -0
- package/skills/_reference/APPROVAL_PROCESSES.md +105 -0
- package/skills/_reference/ASYNC_PATTERNS.md +163 -0
- package/skills/_reference/AURA_COMPONENTS.md +146 -0
- package/skills/_reference/DATA_MIGRATION_PATTERNS.md +151 -0
- package/skills/_reference/DATA_MODELING.md +124 -0
- package/skills/_reference/DEBUGGING_TOOLS.md +140 -0
- package/skills/_reference/DEPLOYMENT_CHECKLIST.md +87 -0
- package/skills/_reference/DEPRECATIONS.md +79 -0
- package/skills/_reference/DOCKER_CI_PATTERNS.md +138 -0
- package/skills/_reference/ENTERPRISE_PATTERNS.md +122 -0
- package/skills/_reference/EXPERIENCE_CLOUD.md +143 -0
- package/skills/_reference/FLOW_PATTERNS.md +113 -0
- package/skills/_reference/GOVERNOR_LIMITS.md +77 -0
- package/skills/_reference/INTEGRATION_PATTERNS.md +105 -0
- package/skills/_reference/LWC_PATTERNS.md +79 -0
- package/skills/_reference/METADATA_TYPES.md +115 -0
- package/skills/_reference/NAMING_CONVENTIONS.md +84 -0
- package/skills/_reference/PACKAGE_DEVELOPMENT.md +150 -0
- package/skills/_reference/PLATFORM_EVENTS.md +121 -0
- package/skills/_reference/REPORTING_API.md +143 -0
- package/skills/_reference/SCRATCH_ORG_PATTERNS.md +126 -0
- package/skills/_reference/SECURITY_PATTERNS.md +127 -0
- package/skills/_reference/SHARING_MODEL.md +120 -0
- package/skills/_reference/SOQL_PATTERNS.md +119 -0
- package/skills/_reference/TESTING_STANDARDS.md +96 -0
- package/skills/_reference/TRIGGER_PATTERNS.md +114 -0
- package/skills/_reference/VISUALFORCE_PATTERNS.md +121 -0
- package/skills/aside/SKILL.md +118 -0
- package/skills/checkpoint/SKILL.md +53 -0
- package/skills/configure-scc/SKILL.md +163 -0
- package/skills/continuous-agent-loop/SKILL.md +264 -0
- package/skills/mcp-server-patterns/SKILL.md +146 -0
- package/skills/model-route/SKILL.md +84 -0
- package/skills/prompt-optimizer/SKILL.md +369 -0
- package/skills/refactor-clean/SKILL.md +136 -0
- package/skills/resume-session/SKILL.md +114 -0
- package/skills/save-session/SKILL.md +186 -0
- package/skills/search-first/SKILL.md +144 -0
- package/skills/security-scan/SKILL.md +146 -0
- package/skills/sessions/SKILL.md +127 -0
- package/skills/sf-agentforce-development/SKILL.md +450 -0
- package/skills/sf-apex-async-patterns/SKILL.md +326 -0
- package/skills/sf-apex-best-practices/SKILL.md +425 -0
- package/skills/sf-apex-constraints/SKILL.md +81 -0
- package/skills/sf-apex-cursor/SKILL.md +338 -0
- package/skills/sf-apex-enterprise-patterns/SKILL.md +348 -0
- package/skills/sf-apex-testing/SKILL.md +409 -0
- package/skills/sf-api-design/SKILL.md +238 -0
- package/skills/sf-approval-processes/SKILL.md +315 -0
- package/skills/sf-aura-development/SKILL.md +263 -0
- package/skills/sf-build-fix/SKILL.md +121 -0
- package/skills/sf-data-modeling/SKILL.md +278 -0
- package/skills/sf-debugging/SKILL.md +363 -0
- package/skills/sf-deployment/SKILL.md +295 -0
- package/skills/sf-deployment-constraints/SKILL.md +155 -0
- package/skills/sf-devops-ci-cd/SKILL.md +325 -0
- package/skills/sf-docs-lookup/SKILL.md +103 -0
- package/skills/sf-e2e-testing/SKILL.md +324 -0
- package/skills/sf-experience-cloud/SKILL.md +249 -0
- package/skills/sf-flow-development/SKILL.md +377 -0
- package/skills/sf-governor-limits/SKILL.md +323 -0
- package/skills/sf-harness-audit/SKILL.md +142 -0
- package/skills/sf-help/SKILL.md +159 -0
- package/skills/sf-integration/SKILL.md +483 -0
- package/skills/sf-lwc-constraints/SKILL.md +130 -0
- package/skills/sf-lwc-development/SKILL.md +303 -0
- package/skills/sf-lwc-testing/SKILL.md +388 -0
- package/skills/sf-metadata-management/SKILL.md +288 -0
- package/skills/sf-platform-events-cdc/SKILL.md +375 -0
- package/skills/sf-quickstart/SKILL.md +173 -0
- package/skills/sf-security/SKILL.md +334 -0
- package/skills/sf-security-constraints/SKILL.md +127 -0
- package/skills/sf-soql-constraints/SKILL.md +131 -0
- package/skills/sf-soql-optimization/SKILL.md +354 -0
- package/skills/sf-tdd-workflow/SKILL.md +336 -0
- package/skills/sf-testing-constraints/SKILL.md +200 -0
- package/skills/sf-trigger-constraints/SKILL.md +90 -0
- package/skills/sf-trigger-frameworks/SKILL.md +347 -0
- package/skills/sf-visualforce-development/SKILL.md +260 -0
- package/skills/strategic-compact/SKILL.md +208 -0
- package/skills/update-docs/SKILL.md +165 -0
- package/skills/update-platform-docs/SKILL.md +90 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sf-tdd-workflow
|
|
3
|
+
description: >-
|
|
4
|
+
Use when doing test-driven Salesforce Apex development — RED-GREEN-REFACTOR
|
|
5
|
+
cycle for classes, triggers, and LWC Jest. Do NOT use for test patterns only.
|
|
6
|
+
origin: SCC
|
|
7
|
+
user-invocable: false
|
|
8
|
+
allowed-tools: Read, Grep, Glob, Edit, Write, Bash
|
|
9
|
+
disable-model-invocation: true
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Salesforce TDD Workflow
|
|
13
|
+
|
|
14
|
+
The test-driven development process adapted for the Salesforce platform. Test implementation patterns (mocks, factories, coverage strategies) live in `sf-apex-testing`. This skill covers the TDD _process_ — RED-GREEN-REFACTOR cycle and how to apply it to Apex, LWC, and Flows.
|
|
15
|
+
|
|
16
|
+
Reference: @../_reference/TESTING_STANDARDS.md
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## When to Use
|
|
21
|
+
|
|
22
|
+
- When starting new Apex class or trigger development using the RED-GREEN-REFACTOR cycle
|
|
23
|
+
- When writing LWC Jest tests before building component logic
|
|
24
|
+
- When refactoring existing untested Apex code and needing a safety net
|
|
25
|
+
- When establishing TDD practices and coverage targets for a team
|
|
26
|
+
- When adding tests for a class below the 75% coverage deployment requirement
|
|
27
|
+
|
|
28
|
+
> **Related:** For test implementation details (@TestSetup, mocks, bulk testing, coverage), see `sf-apex-testing`.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Core Workflow: RED-GREEN-REFACTOR
|
|
33
|
+
|
|
34
|
+
### Step 1: RED — Write Failing Tests First
|
|
35
|
+
|
|
36
|
+
Write the test before the production code exists. The test should compile but fail.
|
|
37
|
+
|
|
38
|
+
**Apex:**
|
|
39
|
+
|
|
40
|
+
```apex
|
|
41
|
+
@IsTest
|
|
42
|
+
private class AccountServiceTest {
|
|
43
|
+
|
|
44
|
+
@TestSetup
|
|
45
|
+
static void makeData() {
|
|
46
|
+
Account acc = new Account(Name = 'Test Account', Industry = 'Technology');
|
|
47
|
+
insert acc;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@IsTest
|
|
51
|
+
static void shouldCalculateAnnualRevenue() {
|
|
52
|
+
Account acc = [SELECT Id FROM Account LIMIT 1];
|
|
53
|
+
|
|
54
|
+
Test.startTest();
|
|
55
|
+
Decimal revenue = AccountService.calculateAnnualRevenue(acc.Id);
|
|
56
|
+
Test.stopTest();
|
|
57
|
+
|
|
58
|
+
Assert.isNotNull(revenue, 'Revenue should not be null');
|
|
59
|
+
Assert.isTrue(revenue >= 0, 'Revenue should be non-negative');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@IsTest
|
|
63
|
+
static void shouldHandleNullInput() {
|
|
64
|
+
Test.startTest();
|
|
65
|
+
try {
|
|
66
|
+
AccountService.calculateAnnualRevenue(null);
|
|
67
|
+
Assert.fail('Should have thrown exception');
|
|
68
|
+
} catch (AccountService.AccountServiceException e) {
|
|
69
|
+
Assert.isTrue(e.getMessage().contains('Account Id'),
|
|
70
|
+
'Error should mention Account Id');
|
|
71
|
+
}
|
|
72
|
+
Test.stopTest();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@IsTest
|
|
76
|
+
static void shouldHandleBulkRecords() {
|
|
77
|
+
List<Account> accounts = new List<Account>();
|
|
78
|
+
for (Integer i = 0; i < 200; i++) {
|
|
79
|
+
accounts.add(new Account(Name = 'Bulk Test ' + i));
|
|
80
|
+
}
|
|
81
|
+
insert accounts;
|
|
82
|
+
|
|
83
|
+
Test.startTest();
|
|
84
|
+
List<Decimal> revenues = AccountService.calculateAnnualRevenueBulk(
|
|
85
|
+
new Map<Id, Account>(accounts).keySet()
|
|
86
|
+
);
|
|
87
|
+
Test.stopTest();
|
|
88
|
+
|
|
89
|
+
System.assertEquals(200, revenues.size(), 'Should process all 200 records');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**LWC Jest:**
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
import { createElement } from 'lwc';
|
|
98
|
+
import AccountCard from 'c/accountCard';
|
|
99
|
+
import getAccount from '@salesforce/apex/AccountController.getAccount';
|
|
100
|
+
|
|
101
|
+
jest.mock('@salesforce/apex/AccountController.getAccount',
|
|
102
|
+
() => ({ default: jest.fn() }), { virtual: true });
|
|
103
|
+
|
|
104
|
+
describe('c-account-card', () => {
|
|
105
|
+
afterEach(() => {
|
|
106
|
+
while (document.body.firstChild) document.body.removeChild(document.body.firstChild);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('displays account name when data is loaded', async () => {
|
|
110
|
+
getAccount.mockResolvedValue({ Name: 'Acme Corp', Industry: 'Technology' });
|
|
111
|
+
const element = createElement('c-account-card', { is: AccountCard });
|
|
112
|
+
element.recordId = '001xx000003ABCDEF';
|
|
113
|
+
document.body.appendChild(element);
|
|
114
|
+
|
|
115
|
+
await Promise.resolve();
|
|
116
|
+
const nameEl = element.shadowRoot.querySelector('.account-name');
|
|
117
|
+
expect(nameEl.textContent).toBe('Acme Corp');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Step 2: GREEN — Implement Minimum Code
|
|
123
|
+
|
|
124
|
+
Write only enough code to make the tests pass. No premature optimization.
|
|
125
|
+
|
|
126
|
+
### Step 3: REFACTOR — Clean Up
|
|
127
|
+
|
|
128
|
+
- Extract common patterns into utility classes
|
|
129
|
+
- Apply enterprise patterns (Service Layer, Selector Layer) if warranted
|
|
130
|
+
- Ensure bulkification
|
|
131
|
+
- Run full test suite to verify no regressions
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Trigger TDD
|
|
136
|
+
|
|
137
|
+
Create the handler class stub first (empty method signatures so the test compiles), then write the test, then implement the handler, then wire the trigger.
|
|
138
|
+
|
|
139
|
+
```apex
|
|
140
|
+
// RED: Write test for trigger handler
|
|
141
|
+
@IsTest
|
|
142
|
+
private class AccountTriggerHandlerTest {
|
|
143
|
+
|
|
144
|
+
@IsTest
|
|
145
|
+
static void shouldSetDefaultIndustryOnInsert() {
|
|
146
|
+
Account acc = new Account(Name = 'TDD Account');
|
|
147
|
+
|
|
148
|
+
Test.startTest();
|
|
149
|
+
insert acc;
|
|
150
|
+
Test.stopTest();
|
|
151
|
+
|
|
152
|
+
Account result = [SELECT Industry FROM Account WHERE Id = :acc.Id];
|
|
153
|
+
System.assertEquals('Other', result.Industry, 'Should default Industry to Other');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@IsTest
|
|
157
|
+
static void shouldNotOverrideExistingIndustry() {
|
|
158
|
+
Account acc = new Account(Name = 'TDD Account', Industry = 'Technology');
|
|
159
|
+
|
|
160
|
+
Test.startTest();
|
|
161
|
+
insert acc;
|
|
162
|
+
Test.stopTest();
|
|
163
|
+
|
|
164
|
+
Account result = [SELECT Industry FROM Account WHERE Id = :acc.Id];
|
|
165
|
+
System.assertEquals('Technology', result.Industry, 'Should keep existing Industry');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// GREEN: Implement handler
|
|
170
|
+
public with sharing class AccountTriggerHandler {
|
|
171
|
+
public static void onBeforeInsert(List<Account> newAccounts) {
|
|
172
|
+
for (Account acc : newAccounts) {
|
|
173
|
+
if (acc.Industry == null) acc.Industry = 'Other';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Wire trigger (last step)
|
|
179
|
+
trigger AccountTrigger on Account (before insert) {
|
|
180
|
+
AccountTriggerHandler.onBeforeInsert(Trigger.new);
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Async TDD Patterns
|
|
187
|
+
|
|
188
|
+
### Queueable TDD
|
|
189
|
+
|
|
190
|
+
```apex
|
|
191
|
+
@IsTest
|
|
192
|
+
private class AccountEnrichmentJobTest {
|
|
193
|
+
|
|
194
|
+
@IsTest
|
|
195
|
+
static void shouldEnrichAccountsWithExternalData() {
|
|
196
|
+
Account acc = new Account(Name = 'Enrich Me', BillingCity = 'San Francisco');
|
|
197
|
+
insert acc;
|
|
198
|
+
|
|
199
|
+
Test.startTest();
|
|
200
|
+
System.enqueueJob(new AccountEnrichmentJob(new Set<Id>{ acc.Id }));
|
|
201
|
+
Test.stopTest(); // Forces Queueable to execute synchronously
|
|
202
|
+
|
|
203
|
+
Account result = [SELECT Description FROM Account WHERE Id = :acc.Id];
|
|
204
|
+
System.assertNotEquals(null, result.Description, 'Should have enrichment data');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Batch TDD
|
|
210
|
+
|
|
211
|
+
```apex
|
|
212
|
+
@IsTest
|
|
213
|
+
private class DataCleanupBatchTest {
|
|
214
|
+
|
|
215
|
+
@IsTest
|
|
216
|
+
static void shouldDeactivateStaleAccounts() {
|
|
217
|
+
List<Account> accounts = new List<Account>();
|
|
218
|
+
for (Integer i = 0; i < 200; i++) {
|
|
219
|
+
accounts.add(new Account(Name = 'Stale ' + i));
|
|
220
|
+
}
|
|
221
|
+
insert accounts;
|
|
222
|
+
|
|
223
|
+
// Create old Tasks so LastActivityDate is set automatically
|
|
224
|
+
List<Task> oldTasks = new List<Task>();
|
|
225
|
+
for (Account acc : accounts) {
|
|
226
|
+
oldTasks.add(new Task(
|
|
227
|
+
WhatId = acc.Id, Subject = 'Old Activity',
|
|
228
|
+
ActivityDate = Date.today().addDays(-365), Status = 'Completed'
|
|
229
|
+
));
|
|
230
|
+
}
|
|
231
|
+
insert oldTasks;
|
|
232
|
+
|
|
233
|
+
Test.startTest();
|
|
234
|
+
Database.executeBatch(new DataCleanupBatch(), 200);
|
|
235
|
+
Test.stopTest();
|
|
236
|
+
|
|
237
|
+
Integer activeCount = [SELECT COUNT() FROM Account
|
|
238
|
+
WHERE Active__c = 'Yes' AND Name LIKE 'Stale%'];
|
|
239
|
+
Assert.areEqual(0, activeCount, 'All stale accounts should be deactivated');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Flow Testing in TDD
|
|
247
|
+
|
|
248
|
+
Test Flows by triggering them through DML and verifying outcomes.
|
|
249
|
+
|
|
250
|
+
```apex
|
|
251
|
+
@IsTest
|
|
252
|
+
private class OpportunityFlowTest {
|
|
253
|
+
|
|
254
|
+
@IsTest
|
|
255
|
+
static void shouldCreateFollowUpTaskWhenOppClosedWon() {
|
|
256
|
+
Account acc = new Account(Name = 'Flow Test');
|
|
257
|
+
insert acc;
|
|
258
|
+
|
|
259
|
+
Opportunity opp = new Opportunity(
|
|
260
|
+
AccountId = acc.Id, Name = 'Flow Test Opp',
|
|
261
|
+
StageName = 'Prospecting', CloseDate = Date.today().addDays(30)
|
|
262
|
+
);
|
|
263
|
+
insert opp;
|
|
264
|
+
|
|
265
|
+
Test.startTest();
|
|
266
|
+
opp.StageName = 'Closed Won';
|
|
267
|
+
update opp;
|
|
268
|
+
Test.stopTest();
|
|
269
|
+
|
|
270
|
+
List<Task> tasks = [SELECT Subject FROM Task WHERE WhatId = :opp.Id];
|
|
271
|
+
System.assert(!tasks.isEmpty(), 'Flow should have created a follow-up task');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## TDD in CI/CD Pipeline
|
|
279
|
+
|
|
280
|
+
```yaml
|
|
281
|
+
# GitHub Actions: Run TDD suite on every PR
|
|
282
|
+
- name: Run Apex Tests and Check Coverage
|
|
283
|
+
run: |
|
|
284
|
+
sf apex run test \
|
|
285
|
+
--test-level RunLocalTests \
|
|
286
|
+
--code-coverage \
|
|
287
|
+
--result-format json \
|
|
288
|
+
--wait 15 \
|
|
289
|
+
--output-dir test-results \
|
|
290
|
+
--target-org ci-scratch
|
|
291
|
+
|
|
292
|
+
RESULT_FILE=$(ls test-results/test-run-*.json 2>/dev/null | head -1)
|
|
293
|
+
if [ -z "$RESULT_FILE" ]; then
|
|
294
|
+
echo "No test result file found"
|
|
295
|
+
exit 1
|
|
296
|
+
fi
|
|
297
|
+
COVERAGE=$(node -e "const r=JSON.parse(require('fs').readFileSync('$RESULT_FILE','utf8')); \
|
|
298
|
+
console.log(r.result?.summary?.orgWideCoverage?.replace('%',''))")
|
|
299
|
+
echo "Org-wide coverage: ${COVERAGE}%"
|
|
300
|
+
node -e "if (parseFloat('$COVERAGE') < 75) { process.exit(1); }"
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Coverage Targets
|
|
306
|
+
|
|
307
|
+
| Type | Minimum | Target |
|
|
308
|
+
|------|---------|--------|
|
|
309
|
+
| Apex Classes | 75% | 85%+ |
|
|
310
|
+
| Apex Triggers | 75% | 90%+ |
|
|
311
|
+
| LWC Components | N/A | 80%+ |
|
|
312
|
+
| Integration Tests | N/A | Key paths |
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Common TDD Mistakes
|
|
317
|
+
|
|
318
|
+
| Mistake | Problem | Fix |
|
|
319
|
+
|---------|---------|-----|
|
|
320
|
+
| Testing platform behavior | Tests Salesforce, not your code | Test business logic outcomes only |
|
|
321
|
+
| Testing getters/setters | No business value | Skip trivial accessors |
|
|
322
|
+
| No bulk test | Passes with 1 record, fails with 200 | Always include 200+ record test |
|
|
323
|
+
| Hardcoded record IDs | Breaks across orgs | Use @TestSetup or TestDataFactory |
|
|
324
|
+
| Skipping negative tests | Misses error handling gaps | Test invalid input, missing permissions |
|
|
325
|
+
| Testing inside try/catch without re-throw | Test passes even when assertion fails | Use `Assert.fail('Should have thrown')` pattern |
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Related
|
|
330
|
+
|
|
331
|
+
- **Agent**: `sf-apex-agent` — For interactive, in-depth guidance
|
|
332
|
+
- **Skills**: `sf-apex-testing` — Test implementation patterns
|
|
333
|
+
|
|
334
|
+
### Guardrails
|
|
335
|
+
|
|
336
|
+
- `sf-testing-constraints` — Enforces test isolation, assertion requirements, SeeAllData prohibition, and coverage thresholds
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sf-testing-constraints
|
|
3
|
+
description: "Enforce Apex testing standards — 75% coverage minimum, test isolation, assertions, TestDataFactory. Use when writing or reviewing ANY Apex test class or method. Do NOT use for LWC Jest or Flow tests."
|
|
4
|
+
origin: SCC
|
|
5
|
+
user-invocable: false
|
|
6
|
+
allowed-tools: Read, Grep, Glob
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Apex Testing Constraints
|
|
10
|
+
|
|
11
|
+
## When to Use
|
|
12
|
+
|
|
13
|
+
This skill auto-activates when writing, reviewing, or modifying any Apex test class or method. It enforces coverage minimums, test isolation, assertion requirements, and TestDataFactory patterns for all test artifacts.
|
|
14
|
+
|
|
15
|
+
Hard rules for every Apex test class and method. Violating any NEVER rule is a blocking defect. Violating any ALWAYS rule requires justification in a code comment.
|
|
16
|
+
|
|
17
|
+
Reference: @../_reference/TESTING_STANDARDS.md
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## NEVER Rules
|
|
22
|
+
|
|
23
|
+
These are non-negotiable. Flag violations immediately.
|
|
24
|
+
|
|
25
|
+
### N1 — Never use `@isTest(SeeAllData=true)`
|
|
26
|
+
|
|
27
|
+
Tests must create their own data. `SeeAllData=true` couples tests to org state and breaks on every sandbox refresh. **Sole exception:** Standard Pricebook tests (prefer `Test.getStandardPricebookId()` even then).
|
|
28
|
+
|
|
29
|
+
```apex
|
|
30
|
+
// VIOLATION
|
|
31
|
+
@isTest(SeeAllData=true)
|
|
32
|
+
private class BadTest { ... }
|
|
33
|
+
|
|
34
|
+
// CORRECT
|
|
35
|
+
@isTest
|
|
36
|
+
private class GoodTest { ... }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### N2 — Never write a test without assertions
|
|
40
|
+
|
|
41
|
+
A test method with zero assertions always passes and catches nothing. Every `@isTest` method must contain at least one meaningful assertion that verifies business logic, not just that DML succeeded.
|
|
42
|
+
|
|
43
|
+
```apex
|
|
44
|
+
// VIOLATION — no assertion
|
|
45
|
+
@isTest
|
|
46
|
+
static void testCreate() {
|
|
47
|
+
Account a = new Account(Name = 'X');
|
|
48
|
+
insert a;
|
|
49
|
+
// method ends without any assert
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// CORRECT — asserts business outcome
|
|
53
|
+
@isTest
|
|
54
|
+
static void testCreate_setsDefaultTier() {
|
|
55
|
+
Account a = new Account(Name = 'X');
|
|
56
|
+
insert a;
|
|
57
|
+
Account result = [SELECT Customer_Tier__c FROM Account WHERE Id = :a.Id];
|
|
58
|
+
Assert.areEqual('Standard', result.Customer_Tier__c,
|
|
59
|
+
'Trigger should set default tier on insert');
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### N3 — Never hardcode Record IDs
|
|
64
|
+
|
|
65
|
+
IDs differ between orgs, sandboxes, and scratch orgs. Hardcoded IDs cause silent test failures after a refresh.
|
|
66
|
+
|
|
67
|
+
```apex
|
|
68
|
+
// VIOLATION
|
|
69
|
+
Id accountId = '0015g00000ABC12AAA';
|
|
70
|
+
|
|
71
|
+
// CORRECT
|
|
72
|
+
Account acc = TestDataFactory.createAccount();
|
|
73
|
+
Id accountId = acc.Id;
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### N4 — Never test only SOQL
|
|
77
|
+
|
|
78
|
+
A test that queries records without exercising any service, trigger, or business logic method is not a test. It verifies that Salesforce can read its own database.
|
|
79
|
+
|
|
80
|
+
```apex
|
|
81
|
+
// VIOLATION — tests the platform, not your code
|
|
82
|
+
@isTest
|
|
83
|
+
static void testQuery() {
|
|
84
|
+
insert new Account(Name = 'X');
|
|
85
|
+
List<Account> accs = [SELECT Id FROM Account];
|
|
86
|
+
Assert.areEqual(1, accs.size());
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### N5 — Never use `System.debug` as a substitute for assertions
|
|
91
|
+
|
|
92
|
+
Debug statements produce no test signal. They cannot fail and cannot catch regressions.
|
|
93
|
+
|
|
94
|
+
### N6 — Never test multiple unrelated scenarios in one method
|
|
95
|
+
|
|
96
|
+
One scenario per test method. Multi-scenario methods mask which scenario broke.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## ALWAYS Rules
|
|
101
|
+
|
|
102
|
+
Required in every test class unless explicitly justified.
|
|
103
|
+
|
|
104
|
+
### A1 — Always use `@TestSetup` for shared test data
|
|
105
|
+
|
|
106
|
+
Runs once per class; each test method gets its own rollback. Omitting it duplicates DML and wastes governor limits.
|
|
107
|
+
|
|
108
|
+
```apex
|
|
109
|
+
@TestSetup
|
|
110
|
+
static void makeData() {
|
|
111
|
+
Account acc = TestDataFactory.createAccount();
|
|
112
|
+
TestDataFactory.createOpportunity(acc.Id);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### A2 — Always use TestDataFactory
|
|
117
|
+
|
|
118
|
+
All test record creation goes through a centralized `TestDataFactory` class. Inline `new SObject(...)` / `insert` is acceptable only for one-off scenario overrides the factory cannot express. Single maintenance point, override maps, consistent defaults.
|
|
119
|
+
|
|
120
|
+
### A3 — Always assert positive AND negative cases
|
|
121
|
+
|
|
122
|
+
Every service method needs at least:
|
|
123
|
+
|
|
124
|
+
- One positive test (valid input produces expected output)
|
|
125
|
+
- One negative test (invalid input throws expected exception or returns expected error)
|
|
126
|
+
|
|
127
|
+
```apex
|
|
128
|
+
// Positive
|
|
129
|
+
@isTest
|
|
130
|
+
static void testUpgrade_validAccount_succeeds() { ... }
|
|
131
|
+
|
|
132
|
+
// Negative
|
|
133
|
+
@isTest
|
|
134
|
+
static void testUpgrade_insufficientRevenue_throwsException() { ... }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### A4 — Always include a bulk test with 200+ records
|
|
138
|
+
|
|
139
|
+
200 is the standard trigger batch size. Code that passes with 1 record can hit governor limits at 200. Every trigger handler and service method operating on collections must have a 200-record test.
|
|
140
|
+
|
|
141
|
+
```apex
|
|
142
|
+
@isTest
|
|
143
|
+
static void testHandler_bulkInsert_200Records_noLimitException() {
|
|
144
|
+
List<Account> accounts = TestDataFactory.createAccounts(200);
|
|
145
|
+
// Trigger already fired on insert; verify outcomes
|
|
146
|
+
Assert.areEqual(200,
|
|
147
|
+
[SELECT COUNT() FROM Account WHERE Customer_Tier__c = 'Standard'],
|
|
148
|
+
'All 200 accounts should have default tier set');
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### A5 — Always use `Test.startTest()` / `Test.stopTest()`
|
|
153
|
+
|
|
154
|
+
Resets governor limit counters and forces async work (@future, Queueable, Batch) to execute synchronously. One pair per test method.
|
|
155
|
+
|
|
156
|
+
### A6 — Always use descriptive test method names
|
|
157
|
+
|
|
158
|
+
Format: `test{MethodName}_{scenario}_{expectedResult}` (e.g., `testCalculateDiscount_premiumTier_returns20Percent`).
|
|
159
|
+
|
|
160
|
+
### A7 — Always add `@testFor` on test classes (see @../_reference/API_VERSIONS.md for minimum version)
|
|
161
|
+
|
|
162
|
+
Maps test classes to production classes for `RunRelevantTests`. Missing `@testFor` means the test may be skipped on relevant changes.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Anti-Pattern Quick Reference
|
|
167
|
+
|
|
168
|
+
| # | Anti-Pattern | Why It Breaks | Required Fix |
|
|
169
|
+
|---|---|---|---|
|
|
170
|
+
| N1 | `SeeAllData=true` | Fails on sandbox refresh; couples test to org data | Create test data via TestDataFactory |
|
|
171
|
+
| N2 | No assertions | Test always passes; catches zero regressions | Add `Assert.areEqual` / `Assert.isTrue` for business logic |
|
|
172
|
+
| N3 | Hardcoded Record IDs | IDs differ between orgs | Query or create records in test |
|
|
173
|
+
| N4 | SOQL-only test | Tests the platform, not your code | Exercise a service/trigger method, then assert outcomes |
|
|
174
|
+
| N5 | `System.debug` instead of assert | No test signal; cannot fail | Replace with assertions |
|
|
175
|
+
| N6 | One method, 10 scenarios | Masks which scenario fails | One scenario per test method |
|
|
176
|
+
| A1 | No `@TestSetup` | Duplicated DML, wasted governor limits | Add `@TestSetup` with TestDataFactory calls |
|
|
177
|
+
| A2 | Inline record creation | Maintenance burden when fields change | Use TestDataFactory with override maps |
|
|
178
|
+
| A3 | Only happy-path tests | Error handling never verified | Add negative-case tests |
|
|
179
|
+
| A4 | No bulk test | Governor limit violations hidden | Test with 200 records |
|
|
180
|
+
| A5 | Missing `startTest/stopTest` | Governor limits not reset; async not executed | Wrap code under test in the pair |
|
|
181
|
+
| A6 | Vague method names | Cannot identify failing scenario from name | Use `test{Method}_{scenario}_{expected}` format |
|
|
182
|
+
| A7 | Missing `@testFor` | `RunRelevantTests` may skip the test | Add `@testFor(ClassName)` annotation |
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Coverage Targets
|
|
187
|
+
|
|
188
|
+
| Context | Minimum | Recommended |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| Production deployment (org-wide) | 75% | 85%+ per class |
|
|
191
|
+
| Managed package (AppExchange) | 75% | 90%+ per class |
|
|
192
|
+
| Triggers | 75% | 90%+ |
|
|
193
|
+
|
|
194
|
+
Coverage measures lines executed, not branches. Test every branch, not just every line.
|
|
195
|
+
|
|
196
|
+
## Related
|
|
197
|
+
|
|
198
|
+
- `sf-apex-testing` — Full test implementation patterns (mocks, async, permissions)
|
|
199
|
+
- `sf-tdd-workflow` — RED-GREEN-REFACTOR process and TDD workflow
|
|
200
|
+
- @../_reference/TESTING_STANDARDS.md — Platform testing standards (see @../_reference/API_VERSIONS.md)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sf-trigger-constraints
|
|
3
|
+
description: "Enforce one-trigger-per-object, handler delegation, bulkification, and recursion prevention. Use when writing or reviewing ANY Apex trigger or handler. Do NOT use for non-trigger Apex, LWC, or Flow."
|
|
4
|
+
origin: SCC
|
|
5
|
+
user-invocable: false
|
|
6
|
+
allowed-tools: Read, Grep, Glob
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Trigger Constraints
|
|
10
|
+
|
|
11
|
+
## When to Use
|
|
12
|
+
|
|
13
|
+
This skill auto-activates when writing, reviewing, or modifying any Apex trigger or trigger handler. It enforces one-trigger-per-object, handler delegation, bulkification, and recursion prevention rules for all trigger artifacts.
|
|
14
|
+
|
|
15
|
+
Hard rules that every Apex trigger and trigger handler must satisfy. Violations are blockers -- flag them before any other review feedback.
|
|
16
|
+
|
|
17
|
+
Reference: @../_reference/TRIGGER_PATTERNS.md (order of execution, context variables, framework comparison).
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Never Rules
|
|
22
|
+
|
|
23
|
+
These are absolute prohibitions. Any occurrence is a defect.
|
|
24
|
+
|
|
25
|
+
| ID | Rule | Why |
|
|
26
|
+
|----|------|-----|
|
|
27
|
+
| N1 | **No logic in the trigger body** | Trigger files contain only the handler invocation (`new Handler().run()` or `fflib_SObjectDomain.triggerHandler(Domain.class)`). Zero conditionals, zero loops, zero DML. |
|
|
28
|
+
| N2 | **No multiple triggers per object** | Multiple triggers on the same sObject have no guaranteed execution order (see @../_reference/TRIGGER_PATTERNS.md, Step 5/9). Consolidate into one trigger file per object. |
|
|
29
|
+
| N3 | **No DML inside loops** | `insert`/`update`/`delete`/`upsert`/`Database.*` calls inside `for` loops hit governor limits. Collect records first, DML once outside the loop. |
|
|
30
|
+
| N4 | **No SOQL inside loops** | Queries inside `for` loops risk the per-transaction SOQL limit (see @../_reference/GOVERNOR_LIMITS.md). Query before the loop, store results in a `Map<Id, SObject>`. |
|
|
31
|
+
| N5 | **No hardcoded IDs** | Record IDs, profile IDs, or record-type IDs must never appear as string literals. Use `Schema.SObjectType.*.getRecordTypeInfosByDeveloperName()`, Custom Metadata, or Custom Labels. |
|
|
32
|
+
| N6 | **No direct callouts** | Apex triggers cannot make HTTP callouts synchronously. Use `@future(callout=true)` or `Queueable` with `Database.AllowsCallouts`. |
|
|
33
|
+
| N7 | **No `Trigger.new` modification in after triggers** | `Trigger.new` is read-only in after contexts. Field updates in after triggers must go through a separate DML statement on queried/cloned records. |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Always Rules
|
|
38
|
+
|
|
39
|
+
Every trigger implementation must include these elements.
|
|
40
|
+
|
|
41
|
+
| ID | Rule | How |
|
|
42
|
+
|----|------|-----|
|
|
43
|
+
| A1 | **Delegate to a handler class** | Trigger body calls handler: `new AccountTriggerHandler().run();`. All logic lives in the handler or in service classes the handler calls. |
|
|
44
|
+
| A2 | **Bulkify all logic** | Every method must handle `List<SObject>` (up to 200 records per chunk). No assumption of single-record input. Iterate `Trigger.new` / `Trigger.old`, never index `[0]` alone. |
|
|
45
|
+
| A3 | **Use a recursion guard** | Prevent infinite re-entry. Recommended: static `Set<Id>` of processed IDs (allows workflow re-fire for unprocessed records while blocking true recursion). Alternatives: depth counter in base class, `setMaxLoopCount()`. See @../_reference/TRIGGER_PATTERNS.md recursion patterns. |
|
|
46
|
+
| A4 | **Use `Trigger.newMap` / `Trigger.oldMap` for comparisons** | When detecting field changes in update triggers, compare `Trigger.newMap.get(id).Field__c` against `Trigger.oldMap.get(id).Field__c`. Never rely on list index alignment. |
|
|
47
|
+
| A5 | **Register all events in one trigger** | The single trigger file should subscribe to all seven events (`before insert, before update, before delete, after insert, after update, after delete, after undelete`) even if the handler only overrides a subset today. This prevents needing a trigger file redeploy when new events are handled later. |
|
|
48
|
+
| A6 | **Include a bypass mechanism** | Support disabling the handler without a code deploy. Use `TriggerHandler.bypass()` / `.clearBypass()`, Custom Metadata (`Trigger_Setting__mdt`), or Hierarchy Custom Settings. Always reset bypass state in a `finally` block. |
|
|
49
|
+
| A7 | **Keep handler methods focused** | Each `onBeforeInsert()`, `onAfterUpdate()`, etc. should call named service methods. If a handler method exceeds ~30 lines, extract to a service class. |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Anti-Pattern Table
|
|
54
|
+
|
|
55
|
+
| Anti-Pattern | Example | Correct Alternative |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| Logic in trigger body | `trigger T on Account (before insert) { for (Account a : Trigger.new) { a.Name = 'X'; } }` | `trigger T on Account (...) { new AccountTriggerHandler().run(); }` with logic in handler |
|
|
58
|
+
| Two triggers on same object | `AccountTrigger.trigger` + `AccountOwnerTrigger.trigger` | Single `AccountTrigger.trigger` delegating to one handler |
|
|
59
|
+
| DML in loop | `for (Account a : accts) { update a; }` | `update accts;` outside loop |
|
|
60
|
+
| SOQL in loop | `for (Account a : accts) { Contact c = [SELECT ...]; }` | `Map<Id, Contact> cMap = new Map<Id, Contact>([SELECT ...]); // before loop` |
|
|
61
|
+
| Hardcoded ID | `if (acc.RecordTypeId == '012000000000001')` | `Schema.SObjectType.Account.getRecordTypeInfosByDeveloperName().get('Customer').getRecordTypeId()` |
|
|
62
|
+
| No recursion guard | After-update handler updates same records with no static check | `private static Set<Id> processedIds = new Set<Id>();` -- skip IDs already in set |
|
|
63
|
+
| Boolean recursion flag | `static Boolean hasRun = false; if (hasRun) return;` | `Set<Id>` -- boolean flag blocks legitimate workflow re-fire for unprocessed records |
|
|
64
|
+
| Modifying `Trigger.new` in after context | `for (Account a : Trigger.new) { a.Status__c = 'Done'; }` in `onAfterInsert` | Query records, update separately: `update [SELECT Id FROM Account WHERE Id IN :newMap.keySet()]` |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Quick Checklist
|
|
69
|
+
|
|
70
|
+
Use when writing or reviewing a trigger PR:
|
|
71
|
+
|
|
72
|
+
- [ ] Exactly one `.trigger` file per sObject
|
|
73
|
+
- [ ] Trigger body is a single handler call (no logic)
|
|
74
|
+
- [ ] Handler extends `TriggerHandler` (or FFLIB `fflib_SObjectDomain`)
|
|
75
|
+
- [ ] All seven events registered in trigger definition
|
|
76
|
+
- [ ] Every loop processes `List<SObject>`, not a single record
|
|
77
|
+
- [ ] Zero SOQL or DML inside any loop
|
|
78
|
+
- [ ] No hardcoded IDs anywhere
|
|
79
|
+
- [ ] Recursion guard present (prefer `Set<Id>` pattern)
|
|
80
|
+
- [ ] Bypass mechanism available
|
|
81
|
+
- [ ] No `Trigger.new` mutation in after-trigger methods
|
|
82
|
+
- [ ] No synchronous callouts
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Related
|
|
87
|
+
|
|
88
|
+
- **Skill**: `sf-trigger-frameworks` -- Framework patterns, base class code, migration guide
|
|
89
|
+
- **Reference**: @../_reference/TRIGGER_PATTERNS.md -- Order of execution, context variables, framework comparison
|
|
90
|
+
- **Agent**: `sf-architect` -- Interactive trigger design guidance
|