@salesforce/afv-skills 1.5.1 → 1.5.3
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/README.md +16 -416
- package/package.json +5 -3
- package/skills/building-ui-bundle-app/SKILL.md +325 -0
- package/skills/building-ui-bundle-frontend/SKILL.md +122 -0
- package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/component.md +1 -1
- package/skills/creating-b2b-commerce-store/SKILL.md +169 -0
- package/skills/creating-b2b-commerce-store/references/store-vs-storefront.md +169 -0
- package/skills/deploying-ui-bundle/SKILL.md +77 -0
- package/skills/generating-apex/CREDITS.md +30 -0
- package/skills/generating-apex/SKILL.md +342 -189
- package/skills/generating-apex/assets/abstract.cls +12 -9
- package/skills/generating-apex/assets/batch.cls +7 -8
- package/skills/generating-apex/assets/domain.cls +5 -6
- package/skills/generating-apex/assets/dto.cls +11 -12
- package/skills/generating-apex/assets/exception.cls +1 -2
- package/skills/generating-apex/assets/interface.cls +2 -3
- package/skills/generating-apex/assets/invocable.cls +114 -0
- package/skills/generating-apex/assets/queueable.cls +6 -7
- package/skills/generating-apex/assets/rest-resource.cls +300 -0
- package/skills/generating-apex/assets/schedulable.cls +7 -8
- package/skills/generating-apex/assets/selector.cls +7 -8
- package/skills/generating-apex/assets/service.cls +4 -5
- package/skills/generating-apex/assets/trigger.cls +45 -0
- package/skills/generating-apex/assets/utility.cls +5 -6
- package/skills/generating-apex/references/AccountDeduplicationBatch.cls +7 -8
- package/skills/generating-apex/references/AccountSelector.cls +10 -11
- package/skills/generating-apex/references/AccountService.cls +9 -10
- package/skills/generating-apex-test/CREDITS.md +30 -0
- package/skills/generating-apex-test/SKILL.md +165 -74
- package/skills/generating-apex-test/assets/test-class-template.cls +25 -56
- package/skills/generating-apex-test/assets/test-data-factory-template.cls +0 -1
- package/skills/generating-apex-test/references/assertion-patterns.md +38 -95
- package/skills/generating-apex-test/references/async-testing.md +59 -142
- package/skills/generating-apex-test/references/mocking-patterns.md +77 -76
- package/skills/generating-apex-test/references/test-data-factory.md +29 -130
- package/skills/generating-experience-react-site/SKILL.md +9 -9
- package/skills/generating-experience-react-site/docs/configure-metadata-digital-experience.md +1 -1
- package/skills/generating-flexipage/SKILL.md +28 -12
- package/skills/generating-ui-bundle-features/SKILL.md +45 -0
- package/skills/generating-ui-bundle-metadata/SKILL.md +106 -0
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/SKILL.md +5 -5
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/constraints.md +2 -2
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/examples.md +1 -1
- package/skills/{implementing-webapp-file-upload → implementing-ui-bundle-file-upload}/SKILL.md +11 -11
- package/skills/searching-media/SKILL.md +1 -1
- package/skills/uplifting-components-to-slds2/SKILL.md +236 -0
- package/skills/uplifting-components-to-slds2/references/color-hooks-decision-guide.md +438 -0
- package/skills/uplifting-components-to-slds2/references/common-patterns.md +87 -0
- package/skills/uplifting-components-to-slds2/references/examples.md +443 -0
- package/skills/uplifting-components-to-slds2/references/migration-checklist.md +67 -0
- package/skills/uplifting-components-to-slds2/references/non-color-hooks-decision-guide.md +333 -0
- package/skills/uplifting-components-to-slds2/references/rule-lwc-token-to-slds-hook.md +135 -0
- package/skills/uplifting-components-to-slds2/references/rule-no-deprecated-tokens-slds1.md +211 -0
- package/skills/uplifting-components-to-slds2/references/rule-no-hardcoded-values.md +160 -0
- package/skills/uplifting-components-to-slds2/references/rule-no-slds-class-overrides.md +126 -0
- package/skills/{using-webapp-salesforce-data → using-ui-bundle-salesforce-data}/SKILL.md +52 -25
- package/skills/using-ui-bundle-salesforce-data/references/mutation-query-generation.md +140 -0
- package/skills/using-ui-bundle-salesforce-data/references/query-testing.md +78 -0
- package/skills/using-ui-bundle-salesforce-data/references/read-query-generation.md +307 -0
- package/skills/using-ui-bundle-salesforce-data/references/schema-introspection.md +53 -0
- package/skills/using-ui-bundle-salesforce-data/references/ui-bundle-integration.md +221 -0
- package/skills/{using-webapp-salesforce-data → using-ui-bundle-salesforce-data/scripts}/graphql-search.sh +75 -23
- package/skills/building-webapp-data-visualization/SKILL.md +0 -72
- package/skills/building-webapp-data-visualization/implementation/bar-line-chart.md +0 -316
- package/skills/building-webapp-data-visualization/implementation/dashboard-layout.md +0 -189
- package/skills/building-webapp-data-visualization/implementation/donut-chart.md +0 -181
- package/skills/building-webapp-data-visualization/implementation/stat-card.md +0 -150
- package/skills/building-webapp-react-components/SKILL.md +0 -96
- package/skills/configuring-webapp-csp-trusted-sites/SKILL.md +0 -90
- package/skills/configuring-webapp-metadata/SKILL.md +0 -158
- package/skills/creating-webapp/SKILL.md +0 -138
- package/skills/deploying-webapp-to-salesforce/SKILL.md +0 -226
- package/skills/installing-webapp-features/SKILL.md +0 -210
- /package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/header-footer.md +0 -0
- /package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/page.md +0 -0
- /package/skills/{configuring-webapp-csp-trusted-sites/implementation/metadata-format.md → generating-ui-bundle-metadata/implementation/csp-metadata-format.md} +0 -0
- /package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/style-tokens.md +0 -0
- /package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/troubleshooting.md +0 -0
|
@@ -4,26 +4,30 @@
|
|
|
4
4
|
|
|
5
5
|
| Method | Use Case |
|
|
6
6
|
|--------|----------|
|
|
7
|
-
| `
|
|
8
|
-
| `
|
|
9
|
-
| `
|
|
7
|
+
| `Assert.areEqual(expected, actual, msg)` | Exact equality |
|
|
8
|
+
| `Assert.areNotEqual(expected, actual, msg)` | Value should differ |
|
|
9
|
+
| `Assert.isTrue(condition, msg)` | Boolean condition |
|
|
10
|
+
| `Assert.isFalse(condition, msg)` | Negated boolean condition |
|
|
11
|
+
| `Assert.fail(msg)` | Force failure (e.g., expected exception not thrown) |
|
|
12
|
+
| `Assert.isNotNull(value, msg)` | Non-null check |
|
|
13
|
+
| `Assert.isNull(value, msg)` | Null check |
|
|
10
14
|
|
|
11
|
-
**Always include the
|
|
15
|
+
**Always include the message parameter** — makes test failures actionable.
|
|
12
16
|
|
|
13
17
|
## Good vs Bad Assertions
|
|
14
18
|
|
|
15
|
-
###
|
|
19
|
+
### Bad: No message, tests coverage not behavior
|
|
16
20
|
|
|
17
21
|
```apex
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
Assert.isTrue(result); // no message
|
|
23
|
+
Assert.isTrue(accounts.size() > 0); // vague — use areEqual with exact count
|
|
20
24
|
```
|
|
21
25
|
|
|
22
|
-
###
|
|
26
|
+
### Good: Descriptive message, tests specific behavior
|
|
23
27
|
|
|
24
28
|
```apex
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
Assert.isTrue(result, 'Service should return true for valid input');
|
|
30
|
+
Assert.areEqual(200, accounts.size(), 'All 200 accounts should be processed');
|
|
27
31
|
```
|
|
28
32
|
|
|
29
33
|
## Common Assertion Patterns
|
|
@@ -31,25 +35,18 @@ System.assertEquals(200, accounts.size(), 'All 200 accounts should be processed'
|
|
|
31
35
|
### Collection Size
|
|
32
36
|
|
|
33
37
|
```apex
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// Not empty
|
|
38
|
-
System.assert(!results.isEmpty(), 'Results should not be empty');
|
|
39
|
-
|
|
40
|
-
// Empty
|
|
41
|
-
System.assert(results.isEmpty(), 'No results expected for invalid input');
|
|
38
|
+
Assert.areEqual(200, results.size(), 'Should process all 200 records');
|
|
39
|
+
Assert.isTrue(results.isEmpty(), 'No results expected for invalid input');
|
|
40
|
+
Assert.isFalse(results.isEmpty(), 'Results should not be empty');
|
|
42
41
|
```
|
|
43
42
|
|
|
44
43
|
### Field Values
|
|
45
44
|
|
|
46
45
|
```apex
|
|
47
|
-
|
|
48
|
-
System.assertEquals('Processed', acc.Status__c, 'Account status should be updated to Processed');
|
|
46
|
+
Assert.areEqual('Processed', acc.Status__c, 'Account status should be updated to Processed');
|
|
49
47
|
|
|
50
|
-
// All records in collection
|
|
51
48
|
for (Account acc : updatedAccounts) {
|
|
52
|
-
|
|
49
|
+
Assert.areEqual('Active', acc.Status__c,
|
|
53
50
|
'Account ' + acc.Name + ' should have Active status');
|
|
54
51
|
}
|
|
55
52
|
```
|
|
@@ -59,21 +56,15 @@ for (Account acc : updatedAccounts) {
|
|
|
59
56
|
```apex
|
|
60
57
|
@isTest
|
|
61
58
|
static void shouldThrowException_WhenInputInvalid() {
|
|
62
|
-
Boolean exceptionThrown = false;
|
|
63
|
-
String exceptionMessage = '';
|
|
64
|
-
|
|
65
59
|
Test.startTest();
|
|
66
60
|
try {
|
|
67
61
|
MyService.process(null);
|
|
62
|
+
Assert.fail('Expected MyCustomException for null input');
|
|
68
63
|
} catch (MyCustomException e) {
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
Assert.isTrue(e.getMessage().contains('cannot be null'),
|
|
65
|
+
'Exception message should mention null input');
|
|
71
66
|
}
|
|
72
67
|
Test.stopTest();
|
|
73
|
-
|
|
74
|
-
System.assert(exceptionThrown, 'MyCustomException should be thrown for null input');
|
|
75
|
-
System.assert(exceptionMessage.contains('cannot be null'),
|
|
76
|
-
'Exception message should mention null input');
|
|
77
68
|
}
|
|
78
69
|
```
|
|
79
70
|
|
|
@@ -83,83 +74,35 @@ static void shouldThrowException_WhenInputInvalid() {
|
|
|
83
74
|
// Insert success
|
|
84
75
|
Database.SaveResult[] results = Database.insert(accounts, false);
|
|
85
76
|
for (Database.SaveResult sr : results) {
|
|
86
|
-
|
|
77
|
+
Assert.isTrue(sr.isSuccess(), 'Insert should succeed: ' + sr.getErrors());
|
|
87
78
|
}
|
|
88
79
|
|
|
89
|
-
// Expected failures
|
|
90
80
|
Database.SaveResult sr = Database.insert(invalidAccount, false);
|
|
91
|
-
|
|
92
|
-
|
|
81
|
+
Assert.isFalse(sr.isSuccess(), 'Insert should fail for invalid data');
|
|
82
|
+
Assert.isTrue(sr.getErrors()[0].getMessage().contains('REQUIRED_FIELD_MISSING'),
|
|
93
83
|
'Error should indicate missing required field');
|
|
94
84
|
```
|
|
95
85
|
|
|
96
|
-
### Comparing Objects
|
|
97
|
-
|
|
98
|
-
```apex
|
|
99
|
-
// Compare specific fields, not entire objects
|
|
100
|
-
System.assertEquals(expected.Name, actual.Name, 'Names should match');
|
|
101
|
-
System.assertEquals(expected.Status__c, actual.Status__c, 'Status should match');
|
|
102
|
-
|
|
103
|
-
// Or use JSON for deep comparison (use sparingly)
|
|
104
|
-
System.assertEquals(
|
|
105
|
-
JSON.serialize(expected),
|
|
106
|
-
JSON.serialize(actual),
|
|
107
|
-
'Objects should be identical'
|
|
108
|
-
);
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### Date/DateTime Assertions
|
|
112
|
-
|
|
113
|
-
```apex
|
|
114
|
-
// Exact date
|
|
115
|
-
System.assertEquals(Date.today(), record.CreatedDate__c, 'Should be created today');
|
|
116
|
-
|
|
117
|
-
// Date within range
|
|
118
|
-
System.assert(record.DueDate__c >= Date.today(), 'Due date should be in the future');
|
|
119
|
-
System.assert(record.DueDate__c <= Date.today().addDays(30),
|
|
120
|
-
'Due date should be within 30 days');
|
|
121
|
-
```
|
|
122
|
-
|
|
123
86
|
### Null Checks
|
|
124
87
|
|
|
125
88
|
```apex
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// Should not be null
|
|
130
|
-
System.assertNotEquals(null, result.Id, 'Record should have been inserted');
|
|
89
|
+
Assert.isNull(result.ErrorMessage__c, 'No error expected for valid input');
|
|
90
|
+
Assert.isNotNull(result.Id, 'Record should have been inserted');
|
|
131
91
|
```
|
|
132
92
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
### ❌ Testing implementation, not behavior
|
|
93
|
+
### Date/DateTime
|
|
136
94
|
|
|
137
95
|
```apex
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Good: Testing the observable outcome
|
|
142
|
-
System.assertEquals('Expected Value', record.Field__c, 'Field should be updated');
|
|
96
|
+
Assert.areEqual(Date.today(), record.CreatedDate__c, 'Should be created today');
|
|
97
|
+
Assert.isTrue(record.DueDate__c >= Date.today(), 'Due date should be in the future');
|
|
143
98
|
```
|
|
144
99
|
|
|
145
|
-
|
|
100
|
+
## Anti-Patterns
|
|
146
101
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
### ❌ Missing negative test assertions
|
|
156
|
-
|
|
157
|
-
```apex
|
|
158
|
-
// Bad: Only tests that no exception occurred
|
|
159
|
-
MyService.process(data); // Test passes if no exception
|
|
160
|
-
|
|
161
|
-
// Good: Verifies the actual outcome
|
|
162
|
-
Result r = MyService.process(data);
|
|
163
|
-
System.assertEquals('Success', r.status, 'Processing should succeed');
|
|
164
|
-
System.assertEquals(0, r.errorCount, 'No errors should occur');
|
|
165
|
-
```
|
|
102
|
+
| Anti-Pattern | Fix |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `Assert.isTrue(results.size() > 0)` | Use `Assert.areEqual(expectedCount, results.size(), ...)` |
|
|
105
|
+
| `Assert.isTrue(results.size() >= expected)` | Compute exact expected count, use `Assert.areEqual` |
|
|
106
|
+
| Testing implementation not behavior | Assert on observable outcomes (field values, record counts) |
|
|
107
|
+
| Missing negative test assertions | Verify the actual outcome, not just that no exception occurred |
|
|
108
|
+
| `Assert.isTrue(count != 0)` | Use `Assert.areEqual` with deterministic value from test data |
|
|
@@ -8,36 +8,33 @@
|
|
|
8
8
|
|
|
9
9
|
### Basic Batch Test
|
|
10
10
|
|
|
11
|
+
**Critical:** In test context only one `execute()` invocation runs, so always set `batchSize >= testRecordCount` (e.g., `Database.executeBatch(batch, 200)` with 200 records). Never create more records than the batch size.
|
|
12
|
+
|
|
11
13
|
```apex
|
|
12
14
|
@isTest
|
|
13
15
|
static void shouldProcessAllRecords_WhenBatchExecutes() {
|
|
14
|
-
// Given: Create test data
|
|
15
16
|
List<Account> accounts = TestDataFactory.createAccounts(200, true);
|
|
16
|
-
|
|
17
|
-
// When: Execute batch
|
|
17
|
+
|
|
18
18
|
Test.startTest();
|
|
19
19
|
MyBatchClass batch = new MyBatchClass();
|
|
20
20
|
Id batchId = Database.executeBatch(batch, 200);
|
|
21
|
-
Test.stopTest();
|
|
22
|
-
|
|
23
|
-
// Then: Verify results
|
|
21
|
+
Test.stopTest();
|
|
22
|
+
|
|
24
23
|
List<Account> updated = [SELECT Id, Status__c FROM Account];
|
|
25
24
|
for (Account acc : updated) {
|
|
26
|
-
|
|
25
|
+
Assert.areEqual('Processed', acc.Status__c,
|
|
27
26
|
'Batch should update all account statuses');
|
|
28
27
|
}
|
|
29
28
|
}
|
|
30
29
|
```
|
|
31
30
|
|
|
32
|
-
###
|
|
31
|
+
### Batch with Failures
|
|
33
32
|
|
|
34
33
|
```apex
|
|
35
34
|
@isTest
|
|
36
35
|
static void shouldLogErrors_WhenRecordsFail() {
|
|
37
|
-
// Given: Create mix of valid and invalid records
|
|
38
36
|
List<Account> accounts = TestDataFactory.createAccounts(198, true);
|
|
39
|
-
|
|
40
|
-
// Create 2 accounts that will fail processing
|
|
37
|
+
|
|
41
38
|
List<Account> invalidAccounts = new List<Account>();
|
|
42
39
|
for (Integer i = 0; i < 2; i++) {
|
|
43
40
|
invalidAccounts.add(new Account(
|
|
@@ -46,37 +43,20 @@ static void shouldLogErrors_WhenRecordsFail() {
|
|
|
46
43
|
));
|
|
47
44
|
}
|
|
48
45
|
insert invalidAccounts;
|
|
49
|
-
|
|
50
|
-
// When
|
|
46
|
+
|
|
51
47
|
Test.startTest();
|
|
52
48
|
MyBatchClass batch = new MyBatchClass();
|
|
53
|
-
Database.executeBatch(batch,
|
|
49
|
+
Database.executeBatch(batch, 200);
|
|
54
50
|
Test.stopTest();
|
|
55
|
-
|
|
56
|
-
// Then
|
|
51
|
+
|
|
57
52
|
List<Error_Log__c> errors = [SELECT Id, Message__c FROM Error_Log__c];
|
|
58
|
-
|
|
53
|
+
Assert.areEqual(2, errors.size(), 'Should log 2 failed records');
|
|
59
54
|
}
|
|
60
55
|
```
|
|
61
56
|
|
|
62
|
-
###
|
|
57
|
+
### Batch Chaining
|
|
63
58
|
|
|
64
|
-
|
|
65
|
-
@isTest
|
|
66
|
-
static void shouldRespectBatchSize() {
|
|
67
|
-
// Given
|
|
68
|
-
List<Account> accounts = TestDataFactory.createAccounts(250, true);
|
|
69
|
-
|
|
70
|
-
Test.startTest();
|
|
71
|
-
MyBatchClass batch = new MyBatchClass();
|
|
72
|
-
Database.executeBatch(batch, 50); // 5 batches of 50
|
|
73
|
-
Test.stopTest();
|
|
74
|
-
|
|
75
|
-
// Note: In tests, all batches execute but you can verify total processing
|
|
76
|
-
List<Account> processed = [SELECT Id FROM Account WHERE Processed__c = true];
|
|
77
|
-
System.assertEquals(250, processed.size(), 'All records should be processed');
|
|
78
|
-
}
|
|
79
|
-
```
|
|
59
|
+
Test `finish()` chaining in a **separate test method** — calling `Database.executeBatch()` inside `finish()` during a test can throw `UnexpectedException`. Verify the first batch independently, then test that `finish()` enqueues the next batch.
|
|
80
60
|
|
|
81
61
|
## Queueable Testing
|
|
82
62
|
|
|
@@ -85,70 +65,65 @@ static void shouldRespectBatchSize() {
|
|
|
85
65
|
```apex
|
|
86
66
|
@isTest
|
|
87
67
|
static void shouldCompleteProcessing_WhenQueueableEnqueued() {
|
|
88
|
-
// Given
|
|
89
68
|
Account acc = TestDataFactory.createAccount(true);
|
|
90
|
-
|
|
91
|
-
// When
|
|
69
|
+
|
|
92
70
|
Test.startTest();
|
|
93
71
|
MyQueueableClass queueable = new MyQueueableClass(acc.Id);
|
|
94
72
|
System.enqueueJob(queueable);
|
|
95
|
-
Test.stopTest();
|
|
96
|
-
|
|
97
|
-
// Then
|
|
73
|
+
Test.stopTest();
|
|
74
|
+
|
|
98
75
|
Account updated = [SELECT Id, Status__c FROM Account WHERE Id = :acc.Id];
|
|
99
|
-
|
|
76
|
+
Assert.areEqual('Processed', updated.Status__c,
|
|
100
77
|
'Queueable should update account status');
|
|
101
78
|
}
|
|
102
79
|
```
|
|
103
80
|
|
|
104
|
-
###
|
|
81
|
+
### Queueable Chaining
|
|
105
82
|
|
|
106
|
-
|
|
83
|
+
Only the **first** chained queueable executes in tests. Design tests to verify:
|
|
84
|
+
1. First job completes correctly
|
|
85
|
+
2. Chain is properly enqueued (query `AsyncApexJob`)
|
|
86
|
+
3. Each job works independently in its own test method
|
|
107
87
|
|
|
108
88
|
```apex
|
|
109
89
|
@isTest
|
|
110
90
|
static void shouldChainNextJob_WhenMoreRecordsExist() {
|
|
111
|
-
// Given: More records than one queueable can process
|
|
112
91
|
List<Account> accounts = TestDataFactory.createAccounts(500, true);
|
|
113
|
-
|
|
92
|
+
|
|
114
93
|
Test.startTest();
|
|
115
|
-
// First queueable processes batch 1 and chains next
|
|
116
94
|
MyChainedQueueable queueable = new MyChainedQueueable(0, 100);
|
|
117
95
|
System.enqueueJob(queueable);
|
|
118
96
|
Test.stopTest();
|
|
119
|
-
|
|
120
|
-
// Verify first batch processed
|
|
97
|
+
|
|
121
98
|
List<Account> processed = [SELECT Id FROM Account WHERE Processed__c = true];
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// Verify chain was enqueued (check AsyncApexJob)
|
|
99
|
+
Assert.areEqual(100, processed.size(), 'First batch should process 100 records');
|
|
100
|
+
|
|
125
101
|
List<AsyncApexJob> jobs = [
|
|
126
|
-
SELECT Id, Status, JobType
|
|
127
|
-
FROM AsyncApexJob
|
|
102
|
+
SELECT Id, Status, JobType
|
|
103
|
+
FROM AsyncApexJob
|
|
128
104
|
WHERE ApexClass.Name = 'MyChainedQueueable'
|
|
129
105
|
];
|
|
130
|
-
|
|
106
|
+
Assert.isTrue(jobs.size() >= 1, 'Chained job should be enqueued');
|
|
131
107
|
}
|
|
132
108
|
```
|
|
133
109
|
|
|
134
|
-
###
|
|
110
|
+
### Queueable with Callouts
|
|
111
|
+
|
|
112
|
+
Set the callout mock **before** `Test.startTest()`:
|
|
135
113
|
|
|
136
114
|
```apex
|
|
137
115
|
@isTest
|
|
138
116
|
static void shouldMakeCallout_WhenQueueableWithCallout() {
|
|
139
|
-
// Given
|
|
140
117
|
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(200, '{"status":"ok"}'));
|
|
141
118
|
Account acc = TestDataFactory.createAccount(true);
|
|
142
|
-
|
|
143
|
-
// When
|
|
119
|
+
|
|
144
120
|
Test.startTest();
|
|
145
121
|
MyQueueableWithCallout queueable = new MyQueueableWithCallout(acc.Id);
|
|
146
122
|
System.enqueueJob(queueable);
|
|
147
123
|
Test.stopTest();
|
|
148
|
-
|
|
149
|
-
// Then
|
|
124
|
+
|
|
150
125
|
Account updated = [SELECT Id, External_Status__c FROM Account WHERE Id = :acc.Id];
|
|
151
|
-
|
|
126
|
+
Assert.areEqual('Synced', updated.External_Status__c,
|
|
152
127
|
'Should update status after successful callout');
|
|
153
128
|
}
|
|
154
129
|
```
|
|
@@ -158,119 +133,61 @@ static void shouldMakeCallout_WhenQueueableWithCallout() {
|
|
|
158
133
|
```apex
|
|
159
134
|
@isTest
|
|
160
135
|
static void shouldExecuteFutureMethod() {
|
|
161
|
-
// Given
|
|
162
136
|
Account acc = TestDataFactory.createAccount(true);
|
|
163
|
-
|
|
164
|
-
// When
|
|
137
|
+
|
|
165
138
|
Test.startTest();
|
|
166
|
-
MyClass.processFuture(acc.Id);
|
|
167
|
-
Test.stopTest();
|
|
168
|
-
|
|
169
|
-
// Then
|
|
139
|
+
MyClass.processFuture(acc.Id);
|
|
140
|
+
Test.stopTest();
|
|
141
|
+
|
|
170
142
|
Account updated = [SELECT Id, Processed__c FROM Account WHERE Id = :acc.Id];
|
|
171
|
-
|
|
143
|
+
Assert.isTrue(updated.Processed__c, 'Future should process record');
|
|
172
144
|
}
|
|
173
145
|
```
|
|
174
146
|
|
|
175
147
|
## Scheduled Apex Testing
|
|
176
148
|
|
|
177
|
-
###
|
|
149
|
+
### Direct Execution
|
|
178
150
|
|
|
179
151
|
```apex
|
|
180
152
|
@isTest
|
|
181
153
|
static void shouldExecuteScheduledJob() {
|
|
182
|
-
// Given
|
|
183
154
|
List<Account> accounts = TestDataFactory.createAccounts(50, true);
|
|
184
|
-
|
|
185
|
-
// When
|
|
155
|
+
|
|
186
156
|
Test.startTest();
|
|
187
|
-
String cronExp = '0 0 0 1 1 ? 2099'; // Arbitrary future time
|
|
188
|
-
String jobId = System.schedule('Test Job', cronExp, new MyScheduledClass());
|
|
189
|
-
|
|
190
|
-
// Execute the scheduled job immediately
|
|
191
157
|
MyScheduledClass scheduled = new MyScheduledClass();
|
|
192
|
-
scheduled.execute(null);
|
|
158
|
+
scheduled.execute(null);
|
|
193
159
|
Test.stopTest();
|
|
194
|
-
|
|
195
|
-
// Then
|
|
160
|
+
|
|
196
161
|
List<Account> processed = [SELECT Id FROM Account WHERE Processed__c = true];
|
|
197
|
-
|
|
162
|
+
Assert.areEqual(50, processed.size(), 'Scheduled job should process records');
|
|
198
163
|
}
|
|
199
164
|
```
|
|
200
165
|
|
|
201
|
-
###
|
|
166
|
+
### CRON Registration
|
|
202
167
|
|
|
203
168
|
```apex
|
|
204
169
|
@isTest
|
|
205
170
|
static void shouldScheduleJob() {
|
|
206
171
|
Test.startTest();
|
|
207
|
-
String cronExp = '0 0 6 * * ?';
|
|
172
|
+
String cronExp = '0 0 6 * * ?';
|
|
208
173
|
String jobId = System.schedule('Daily Processing', cronExp, new MyScheduledClass());
|
|
209
174
|
Test.stopTest();
|
|
210
|
-
|
|
211
|
-
// Verify job is scheduled
|
|
175
|
+
|
|
212
176
|
CronTrigger ct = [
|
|
213
|
-
SELECT Id, CronExpression, State
|
|
214
|
-
FROM CronTrigger
|
|
177
|
+
SELECT Id, CronExpression, State
|
|
178
|
+
FROM CronTrigger
|
|
215
179
|
WHERE Id = :jobId
|
|
216
180
|
];
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
## Testing Async Limits
|
|
223
|
-
|
|
224
|
-
```apex
|
|
225
|
-
@isTest
|
|
226
|
-
static void shouldNotExceedQueueableLimits() {
|
|
227
|
-
// Given: Setup that might enqueue multiple jobs
|
|
228
|
-
List<Account> accounts = TestDataFactory.createAccounts(100, true);
|
|
229
|
-
|
|
230
|
-
Test.startTest();
|
|
231
|
-
Integer queueablesBefore = Limits.getQueueableJobs();
|
|
232
|
-
|
|
233
|
-
MyService.processWithQueueables(accounts);
|
|
234
|
-
|
|
235
|
-
Integer queueablesUsed = Limits.getQueueableJobs() - queueablesBefore;
|
|
236
|
-
Test.stopTest();
|
|
237
|
-
|
|
238
|
-
// Verify limit not exceeded (50 in synchronous context, 1 in queueable)
|
|
239
|
-
System.assert(queueablesUsed <= 50,
|
|
240
|
-
'Should not exceed queueable limit. Used: ' + queueablesUsed);
|
|
181
|
+
Assert.areEqual('0 0 6 * * ?', ct.CronExpression, 'CRON should match');
|
|
182
|
+
Assert.areEqual('WAITING', ct.State, 'Job should be waiting');
|
|
241
183
|
}
|
|
242
184
|
```
|
|
243
185
|
|
|
244
186
|
## Common Pitfalls
|
|
245
187
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
Test.startTest()
|
|
251
|
-
|
|
252
|
-
// Missing Test.stopTest()!
|
|
253
|
-
|
|
254
|
-
List<Account> results = [SELECT Id FROM Account WHERE Processed__c = true];
|
|
255
|
-
System.assertEquals(100, results.size()); // FAILS - queueable didn't run
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
### ❌ Testing chained jobs without understanding limits
|
|
259
|
-
|
|
260
|
-
```apex
|
|
261
|
-
// Only the FIRST chained queueable runs in tests
|
|
262
|
-
// Design tests to verify:
|
|
263
|
-
// 1. First job completes correctly
|
|
264
|
-
// 2. Chain is properly enqueued (check AsyncApexJob)
|
|
265
|
-
// 3. Each job works independently
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
### ❌ Not mocking callouts in async
|
|
269
|
-
|
|
270
|
-
```apex
|
|
271
|
-
// Async with callouts MUST have mock set BEFORE Test.startTest()
|
|
272
|
-
Test.setMock(HttpCalloutMock.class, new MockResponse()); // Before startTest!
|
|
273
|
-
Test.startTest();
|
|
274
|
-
System.enqueueJob(new QueueableWithCallout());
|
|
275
|
-
Test.stopTest();
|
|
276
|
-
```
|
|
188
|
+
| Pitfall | Impact |
|
|
189
|
+
|---|---|
|
|
190
|
+
| Missing `Test.stopTest()` | Async never executes, assertions fail silently |
|
|
191
|
+
| Expecting all chained queueables to run | Only the first runs; test each independently |
|
|
192
|
+
| Mock set after `Test.startTest()` | Callout mock must be set **before** `Test.startTest()` |
|
|
193
|
+
| Batch size < record count in tests | Only `batchSize` records processed; set `batchSize >= recordCount` |
|