sf-forcekit 1.0.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/License +21 -0
- package/README.md +106 -0
- package/bin/cli.js +85 -0
- package/package.json +44 -0
- package/templates/ai-dir/README.md +155 -0
- package/templates/ai-dir/agentforce.md +213 -0
- package/templates/ai-dir/architecture.md +123 -0
- package/templates/ai-dir/commands.md +276 -0
- package/templates/ai-dir/context-snapshots/TEMPLATE.md +64 -0
- package/templates/ai-dir/conventions.md +242 -0
- package/templates/ai-dir/current-state.md +113 -0
- package/templates/ai-dir/debugging-notes.md +165 -0
- package/templates/ai-dir/deployment.md +161 -0
- package/templates/ai-dir/integrations.md +199 -0
- package/templates/ai-dir/inventory.md +209 -0
- package/templates/ai-dir/known-issues.md +124 -0
- package/templates/ai-dir/org-context.md +110 -0
- package/templates/ai-dir/performance.md +312 -0
- package/templates/ai-dir/prompts/agentforce.md +163 -0
- package/templates/ai-dir/prompts/apex.md +165 -0
- package/templates/ai-dir/prompts/flows.md +125 -0
- package/templates/ai-dir/prompts/lwc.md +230 -0
- package/templates/ai-dir/prompts/security.md +181 -0
- package/templates/ai-dir/prompts/testing.md +269 -0
- package/templates/ai-dir/rules.md +238 -0
- package/templates/ai-dir/scripts/update_state.py +1406 -0
- package/templates/ai-dir/source-of-truth.md +180 -0
- package/templates/ai-dir/templates/Selector.cls +113 -0
- package/templates/ai-dir/templates/Service.cls +132 -0
- package/templates/ai-dir/templates/TestClass.cls +143 -0
- package/templates/ai-dir/templates/TriggerHandler.cls +67 -0
- package/templates/ai-dir/testing-strategy.md +342 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# Testing Strategy
|
|
2
|
+
|
|
3
|
+
> How we test, what we test, and why. Agents MUST follow these patterns.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Testing Philosophy
|
|
8
|
+
|
|
9
|
+
1. **Test behavior, not implementation** — Assert what the code *does*, not how it does it.
|
|
10
|
+
2. **Bulk by default** — Every test uses 200+ records unless explicitly testing a single-record path.
|
|
11
|
+
3. **Negative paths are mandatory** — If a method can fail, test the failure.
|
|
12
|
+
4. **Permissions matter** — Test with restricted users, not just System Administrator.
|
|
13
|
+
5. **No org data** — `SeeAllData=true` is permanently banned.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Coverage Requirements
|
|
18
|
+
|
|
19
|
+
| Target | Minimum | Standard |
|
|
20
|
+
|--------|---------|----------|
|
|
21
|
+
| Per class | 75% | **90%+** |
|
|
22
|
+
| Org-wide | 75% | **85%+** |
|
|
23
|
+
| New code (PRs) | 90% | **95%+** |
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Test Categories
|
|
28
|
+
|
|
29
|
+
### Unit Tests (Required for every class)
|
|
30
|
+
|
|
31
|
+
Tests a single method in isolation. Mock all dependencies.
|
|
32
|
+
|
|
33
|
+
```apex
|
|
34
|
+
@IsTest
|
|
35
|
+
private class AccountServiceTest {
|
|
36
|
+
|
|
37
|
+
@TestSetup
|
|
38
|
+
static void setupTestData() {
|
|
39
|
+
// Use TestDataFactory — never hardcode test data inline
|
|
40
|
+
List<Account> accounts = TestDataFactory.createAccounts(200);
|
|
41
|
+
insert accounts;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@IsTest
|
|
45
|
+
static void testProcessAccounts_BulkPositive() {
|
|
46
|
+
// Arrange
|
|
47
|
+
List<Account> accounts = [SELECT Id, Name FROM Account];
|
|
48
|
+
System.assertEquals(200, accounts.size(), 'Setup should create 200 accounts');
|
|
49
|
+
|
|
50
|
+
// Act
|
|
51
|
+
Test.startTest();
|
|
52
|
+
AccountService.processAccounts(accounts);
|
|
53
|
+
Test.stopTest();
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
List<Account> updated = [SELECT Id, Status__c FROM Account WHERE Id IN :accounts];
|
|
57
|
+
for (Account a : updated) {
|
|
58
|
+
System.assertNotEquals(null, a.Status__c, 'Status should be set after processing');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@IsTest
|
|
63
|
+
static void testProcessAccounts_EmptyList() {
|
|
64
|
+
// Negative: empty input should not throw
|
|
65
|
+
Test.startTest();
|
|
66
|
+
AccountService.processAccounts(new List<Account>());
|
|
67
|
+
Test.stopTest();
|
|
68
|
+
// No exception = pass
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@IsTest
|
|
72
|
+
static void testProcessAccounts_NullInput() {
|
|
73
|
+
// Negative: null input should throw
|
|
74
|
+
try {
|
|
75
|
+
AccountService.processAccounts(null);
|
|
76
|
+
System.assert(false, 'Should have thrown exception for null input');
|
|
77
|
+
} catch (AccountServiceException e) {
|
|
78
|
+
System.assert(e.getMessage().contains('cannot be null'),
|
|
79
|
+
'Exception message should mention null: ' + e.getMessage());
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Integration Tests (Required for callouts & triggers)
|
|
86
|
+
|
|
87
|
+
Tests end-to-end flow including DML and trigger execution.
|
|
88
|
+
|
|
89
|
+
```apex
|
|
90
|
+
@IsTest
|
|
91
|
+
private class AccountTriggerTest {
|
|
92
|
+
|
|
93
|
+
@TestSetup
|
|
94
|
+
static void setupTestData() {
|
|
95
|
+
List<Account> accounts = TestDataFactory.createAccounts(200);
|
|
96
|
+
insert accounts;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@IsTest
|
|
100
|
+
static void testAfterUpdate_StatusChange_FiresRelatedUpdate() {
|
|
101
|
+
// Arrange
|
|
102
|
+
List<Account> accounts = [SELECT Id, Status__c FROM Account];
|
|
103
|
+
for (Account a : accounts) {
|
|
104
|
+
a.Status__c = 'Active';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Act — trigger fires on update
|
|
108
|
+
Test.startTest();
|
|
109
|
+
update accounts;
|
|
110
|
+
Test.stopTest();
|
|
111
|
+
|
|
112
|
+
// Assert — check related records were updated
|
|
113
|
+
List<Contact> contacts = [SELECT Id, Account_Status__c FROM Contact WHERE AccountId IN :accounts];
|
|
114
|
+
for (Contact c : contacts) {
|
|
115
|
+
System.assertEquals('Active', c.Account_Status__c,
|
|
116
|
+
'Contact should reflect parent Account status');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Callout Mock Tests (Required for all integrations)
|
|
123
|
+
|
|
124
|
+
```apex
|
|
125
|
+
@IsTest
|
|
126
|
+
private class ExternalApiServiceTest {
|
|
127
|
+
|
|
128
|
+
private class MockSuccess implements HttpCalloutMock {
|
|
129
|
+
public HttpResponse respond(HttpRequest req) {
|
|
130
|
+
HttpResponse res = new HttpResponse();
|
|
131
|
+
res.setStatusCode(200);
|
|
132
|
+
res.setBody('{"status":"success","id":"ext-123"}');
|
|
133
|
+
res.setHeader('Content-Type', 'application/json');
|
|
134
|
+
return res;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private class MockFailure implements HttpCalloutMock {
|
|
139
|
+
public HttpResponse respond(HttpRequest req) {
|
|
140
|
+
HttpResponse res = new HttpResponse();
|
|
141
|
+
res.setStatusCode(500);
|
|
142
|
+
res.setBody('{"error":"Internal Server Error"}');
|
|
143
|
+
return res;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@IsTest
|
|
148
|
+
static void testCallExternalApi_Success() {
|
|
149
|
+
Test.setMock(HttpCalloutMock.class, new MockSuccess());
|
|
150
|
+
|
|
151
|
+
Test.startTest();
|
|
152
|
+
ApiResponseDTO result = ExternalApiService.callExternalApi('/orders', 'GET', null);
|
|
153
|
+
Test.stopTest();
|
|
154
|
+
|
|
155
|
+
System.assertEquals('success', result.status, 'Should parse success response');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@IsTest
|
|
159
|
+
static void testCallExternalApi_ServerError() {
|
|
160
|
+
Test.setMock(HttpCalloutMock.class, new MockFailure());
|
|
161
|
+
|
|
162
|
+
Test.startTest();
|
|
163
|
+
try {
|
|
164
|
+
ExternalApiService.callExternalApi('/orders', 'GET', null);
|
|
165
|
+
System.assert(false, 'Should throw IntegrationException on 500');
|
|
166
|
+
} catch (IntegrationException e) {
|
|
167
|
+
System.assert(e.getMessage().contains('500'),
|
|
168
|
+
'Exception should contain status code');
|
|
169
|
+
}
|
|
170
|
+
Test.stopTest();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Permission Tests (Required for customer-facing features)
|
|
176
|
+
|
|
177
|
+
```apex
|
|
178
|
+
@IsTest
|
|
179
|
+
private class AccountService_PermissionTest {
|
|
180
|
+
|
|
181
|
+
@IsTest
|
|
182
|
+
static void testReadAccess_RestrictedUser() {
|
|
183
|
+
// Create a user with minimal permissions
|
|
184
|
+
User restrictedUser = TestDataFactory.createUser('Standard User');
|
|
185
|
+
|
|
186
|
+
System.runAs(restrictedUser) {
|
|
187
|
+
Test.startTest();
|
|
188
|
+
try {
|
|
189
|
+
List<Account> results = AccountService.getAccounts(new Set<Id>());
|
|
190
|
+
// Should return empty, not throw
|
|
191
|
+
System.assertEquals(0, results.size(), 'Restricted user should see no accounts');
|
|
192
|
+
} catch (Exception e) {
|
|
193
|
+
System.assert(false,
|
|
194
|
+
'Should not throw for restricted user, should return empty: ' + e.getMessage());
|
|
195
|
+
}
|
|
196
|
+
Test.stopTest();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## TestDataFactory Pattern
|
|
205
|
+
|
|
206
|
+
Every project MUST have a `TestDataFactory` class. Never create test data inline.
|
|
207
|
+
|
|
208
|
+
```apex
|
|
209
|
+
@IsTest
|
|
210
|
+
public class TestDataFactory {
|
|
211
|
+
|
|
212
|
+
public static List<Account> createAccounts(Integer count) {
|
|
213
|
+
List<Account> accounts = new List<Account>();
|
|
214
|
+
for (Integer i = 0; i < count; i++) {
|
|
215
|
+
accounts.add(new Account(
|
|
216
|
+
Name = 'Test Account ' + i,
|
|
217
|
+
Industry = 'Technology'
|
|
218
|
+
));
|
|
219
|
+
}
|
|
220
|
+
return accounts;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
public static List<Contact> createContacts(Integer count, Id accountId) {
|
|
224
|
+
List<Contact> contacts = new List<Contact>();
|
|
225
|
+
for (Integer i = 0; i < count; i++) {
|
|
226
|
+
contacts.add(new Contact(
|
|
227
|
+
FirstName = 'Test',
|
|
228
|
+
LastName = 'Contact ' + i,
|
|
229
|
+
AccountId = accountId,
|
|
230
|
+
Email = 'test' + i + '@example.com'
|
|
231
|
+
));
|
|
232
|
+
}
|
|
233
|
+
return contacts;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
public static User createUser(String profileName) {
|
|
237
|
+
Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
|
|
238
|
+
return new User(
|
|
239
|
+
FirstName = 'Test',
|
|
240
|
+
LastName = 'User',
|
|
241
|
+
Email = 'testuser@example.com',
|
|
242
|
+
Username = 'testuser' + DateTime.now().getTime() + '@example.com',
|
|
243
|
+
Alias = 'tuser',
|
|
244
|
+
ProfileId = p.Id,
|
|
245
|
+
TimeZoneSidKey = 'America/New_York',
|
|
246
|
+
LocaleSidKey = 'en_US',
|
|
247
|
+
EmailEncodingKey = 'UTF-8',
|
|
248
|
+
LanguageLocaleKey = 'en_US'
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Test Naming Convention
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
{MethodName}_{Scenario}_{ExpectedResult}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
| Example | Meaning |
|
|
263
|
+
|---------|---------|
|
|
264
|
+
| `testGetAccounts_ValidIds_ReturnsAccounts` | Happy path |
|
|
265
|
+
| `testGetAccounts_EmptySet_ReturnsEmpty` | Empty input |
|
|
266
|
+
| `testGetAccounts_NullInput_ThrowsException` | Null input |
|
|
267
|
+
| `testGetAccounts_200Records_NoLimitErrors` | Bulk test |
|
|
268
|
+
| `testGetAccounts_RestrictedUser_ReturnsEmpty` | Permission |
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Governor Limit Testing
|
|
273
|
+
|
|
274
|
+
```apex
|
|
275
|
+
@IsTest
|
|
276
|
+
static void testBulkProcessing_200Records_NoGovernorViolations() {
|
|
277
|
+
List<Account> accounts = TestDataFactory.createAccounts(200);
|
|
278
|
+
insert accounts;
|
|
279
|
+
|
|
280
|
+
Test.startTest();
|
|
281
|
+
|
|
282
|
+
Integer queriesBefore = Limits.getQueries();
|
|
283
|
+
Integer dmlBefore = Limits.getDmlStatements();
|
|
284
|
+
|
|
285
|
+
AccountService.processAccounts(accounts);
|
|
286
|
+
|
|
287
|
+
Integer queriesUsed = Limits.getQueries() - queriesBefore;
|
|
288
|
+
Integer dmlUsed = Limits.getDmlStatements() - dmlBefore;
|
|
289
|
+
|
|
290
|
+
Test.stopTest();
|
|
291
|
+
|
|
292
|
+
// Assert governor-friendly behavior
|
|
293
|
+
System.assert(queriesUsed <= 5,
|
|
294
|
+
'Should use ≤5 SOQL queries for 200 records, used: ' + queriesUsed);
|
|
295
|
+
System.assert(dmlUsed <= 3,
|
|
296
|
+
'Should use ≤3 DML statements for 200 records, used: ' + dmlUsed);
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Test Execution Commands
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
# Run all local tests
|
|
306
|
+
sf apex run test --target-org <target_org> --code-coverage --result-format human --test-level RunLocalTests
|
|
307
|
+
|
|
308
|
+
# Run one test class
|
|
309
|
+
sf apex run test --target-org <target_org> --tests AccountServiceTest --code-coverage --result-format human
|
|
310
|
+
|
|
311
|
+
# Run one test method
|
|
312
|
+
sf apex run test --target-org <target_org> --tests "AccountServiceTest.testBulkInsert" --code-coverage --result-format human
|
|
313
|
+
|
|
314
|
+
# Run tests with JSON output (for CI/CD parsing)
|
|
315
|
+
sf apex run test --target-org <target_org> --code-coverage --result-format json --output-dir test-results --json
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## What NOT to Test
|
|
321
|
+
|
|
322
|
+
| Don't Test | Why |
|
|
323
|
+
|-----------|-----|
|
|
324
|
+
| Standard Salesforce behavior (e.g., required field validation) | Platform handles it |
|
|
325
|
+
| Getter/setter methods with no logic | Trivial, adds no value |
|
|
326
|
+
| Private methods directly | Test through public interface |
|
|
327
|
+
| Platform UI behavior | Not testable via Apex |
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Test Review Checklist
|
|
332
|
+
|
|
333
|
+
- [ ] Uses `@TestSetup` for shared data
|
|
334
|
+
- [ ] `SeeAllData=true` is NOT used
|
|
335
|
+
- [ ] Tests 200+ records (bulk)
|
|
336
|
+
- [ ] Tests negative paths (null, empty, invalid)
|
|
337
|
+
- [ ] Tests permission scenarios (`System.runAs`)
|
|
338
|
+
- [ ] Tests callout mocks (`HttpCalloutMock`)
|
|
339
|
+
- [ ] Every assertion has a meaningful message
|
|
340
|
+
- [ ] `Test.startTest()` / `Test.stopTest()` bracket the action
|
|
341
|
+
- [ ] No hardcoded IDs in test data
|
|
342
|
+
- [ ] Coverage ≥ 90%
|