@salesforce/afv-skills 1.4.0 → 1.5.1

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.
Files changed (31) hide show
  1. package/package.json +6 -5
  2. package/skills/creating-webapp/SKILL.md +0 -2
  3. package/skills/generating-apex/SKILL.md +253 -0
  4. package/skills/generating-apex/assets/abstract.cls +128 -0
  5. package/skills/generating-apex/assets/batch.cls +125 -0
  6. package/skills/generating-apex/assets/domain.cls +102 -0
  7. package/skills/generating-apex/assets/dto.cls +108 -0
  8. package/skills/generating-apex/assets/exception.cls +51 -0
  9. package/skills/generating-apex/assets/interface.cls +25 -0
  10. package/skills/generating-apex/assets/queueable.cls +92 -0
  11. package/skills/generating-apex/assets/schedulable.cls +75 -0
  12. package/skills/generating-apex/assets/selector.cls +92 -0
  13. package/skills/generating-apex/assets/service.cls +69 -0
  14. package/skills/generating-apex/assets/utility.cls +97 -0
  15. package/skills/generating-apex/references/AccountDeduplicationBatch.cls +148 -0
  16. package/skills/generating-apex/references/AccountSelector.cls +193 -0
  17. package/skills/generating-apex/references/AccountService.cls +201 -0
  18. package/skills/generating-apex-test/SKILL.md +108 -0
  19. package/skills/generating-apex-test/assets/test-class-template.cls +124 -0
  20. package/skills/generating-apex-test/assets/test-data-factory-template.cls +112 -0
  21. package/skills/generating-apex-test/references/assertion-patterns.md +165 -0
  22. package/skills/generating-apex-test/references/async-testing.md +276 -0
  23. package/skills/generating-apex-test/references/mocking-patterns.md +219 -0
  24. package/skills/generating-apex-test/references/test-data-factory.md +176 -0
  25. package/skills/generating-experience-lwr-site/SKILL.md +42 -16
  26. package/skills/generating-experience-lwr-site/docs/configure-content-brandingSet.md +17 -7
  27. package/skills/generating-experience-lwr-site/docs/configure-content-themeLayout.md +2 -1
  28. package/skills/generating-experience-lwr-site/docs/configure-content-view.md +3 -3
  29. package/skills/generating-experience-react-site/SKILL.md +11 -0
  30. package/skills/generating-flexipage/SKILL.md +39 -57
  31. package/skills/searching-media/SKILL.md +342 -0
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @description Test class for {ClassUnderTest}.
3
+ * Tests bulk operations (200+ records), positive/negative paths,
4
+ * and exception handling.
5
+ * @author Generated by Apex Test Writer Skill
6
+ */
7
+ @isTest
8
+ private class {ClassUnderTest}Test {
9
+
10
+ // ─── Test Setup ───────────────────────────────────────────────────────
11
+
12
+ @TestSetup
13
+ static void setupTestData() {
14
+ // Create shared test data using TestDataFactory
15
+ // List<Account> accounts = TestDataFactory.createAccounts(200, true);
16
+ }
17
+
18
+ // ─── Positive Tests ───────────────────────────────────────────────────
19
+
20
+ @isTest
21
+ static void shouldPerformExpectedBehavior_WhenValidInput() {
22
+ // Given: Setup specific test state
23
+ // List<Account> accounts = [SELECT Id, Name FROM Account];
24
+
25
+ // When: Execute the code under test
26
+ Test.startTest();
27
+ // {ClassUnderTest}.methodUnderTest(params);
28
+ Test.stopTest();
29
+
30
+ // Then: Assert expected outcomes
31
+ // System.assertEquals(expected, actual, 'Descriptive failure message');
32
+ }
33
+
34
+ @isTest
35
+ static void shouldHandleBulkRecords_WhenProcessing200() {
36
+ // Given: 200+ records to verify bulkification
37
+ // List<Account> accounts = [SELECT Id FROM Account];
38
+ // System.assertEquals(200, accounts.size(), 'Should have 200 test records');
39
+
40
+ // When
41
+ Test.startTest();
42
+ // {ClassUnderTest}.bulkMethod(accounts);
43
+ Test.stopTest();
44
+
45
+ // Then: Verify all records processed
46
+ // List<Account> results = [SELECT Id, Status__c FROM Account];
47
+ // for (Account acc : results) {
48
+ // System.assertEquals('Processed', acc.Status__c, 'All records should be processed');
49
+ // }
50
+ }
51
+
52
+ // ─── Negative Tests ───────────────────────────────────────────────────
53
+
54
+ @isTest
55
+ static void shouldThrowException_WhenNullInput() {
56
+ Boolean exceptionThrown = false;
57
+ String exceptionMessage = '';
58
+
59
+ Test.startTest();
60
+ try {
61
+ // {ClassUnderTest}.methodUnderTest(null);
62
+ } catch (Exception e) {
63
+ exceptionThrown = true;
64
+ exceptionMessage = e.getMessage();
65
+ }
66
+ Test.stopTest();
67
+
68
+ System.assert(exceptionThrown, 'Exception should be thrown for null input');
69
+ System.assert(exceptionMessage.contains('cannot be null'),
70
+ 'Exception message should mention null input');
71
+ }
72
+
73
+ @isTest
74
+ static void shouldReturnEmpty_WhenEmptyInput() {
75
+ Test.startTest();
76
+ // List<SObject> results = {ClassUnderTest}.methodUnderTest(new List<Id>());
77
+ Test.stopTest();
78
+
79
+ // System.assert(results.isEmpty(), 'Should return empty list for empty input');
80
+ }
81
+
82
+ // ─── Edge Case Tests ──────────────────────────────────────────────────
83
+
84
+ @isTest
85
+ static void shouldHandleMixedRecords_WhenSomeQualify() {
86
+ // Given: Mix of qualifying and non-qualifying records
87
+ // List<Account> accounts = [SELECT Id, Status__c FROM Account];
88
+ // Integer half = accounts.size() / 2;
89
+ // for (Integer i = 0; i < half; i++) {
90
+ // accounts[i].Status__c = 'Qualifying';
91
+ // }
92
+ // update accounts;
93
+
94
+ // When
95
+ Test.startTest();
96
+ // {ClassUnderTest}.conditionalMethod(accounts);
97
+ Test.stopTest();
98
+
99
+ // Then: Only qualifying records should be affected
100
+ // List<Account> qualifying = [SELECT Id FROM Account WHERE Processed__c = true];
101
+ // System.assertEquals(half, qualifying.size(), 'Only qualifying records should be processed');
102
+ }
103
+
104
+ // ─── Governor Limit Tests ─────────────────────────────────────────────
105
+
106
+ @isTest
107
+ static void shouldNotExceedGovernorLimits_WhenBulkProcessing() {
108
+ // Given
109
+ // List<Account> accounts = [SELECT Id FROM Account];
110
+
111
+ Test.startTest();
112
+ // {ClassUnderTest}.heavyMethod(accounts);
113
+ Test.stopTest();
114
+
115
+ System.assert(Limits.getDmlStatements() < Limits.getLimitDmlStatements(),
116
+ 'Should not exceed DML statement limit');
117
+ System.assert(Limits.getQueries() < Limits.getLimitQueries(),
118
+ 'Should not exceed SOQL query limit');
119
+ }
120
+
121
+ // ─── Helper Methods ───────────────────────────────────────────────────
122
+
123
+ // Add test-specific helper methods here
124
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @description Centralized factory for creating test data with sensible defaults.
3
+ * All methods accept a doInsert flag for flexibility.
4
+ * Bulk methods create multiple records; single-record methods delegate to bulk.
5
+ * @author Generated by Apex Test Writer Skill
6
+ */
7
+ @isTest
8
+ public class TestDataFactory {
9
+
10
+ // ─── Accounts ─────────────────────────────────────────────────────────
11
+
12
+ public static List<Account> createAccounts(Integer count, Boolean doInsert) {
13
+ List<Account> accounts = new List<Account>();
14
+ for (Integer i = 0; i < count; i++) {
15
+ accounts.add(new Account(
16
+ Name = 'Test Account ' + i,
17
+ BillingCity = 'San Francisco',
18
+ BillingState = 'CA',
19
+ BillingCountry = 'USA',
20
+ Industry = 'Technology',
21
+ Type = 'Customer'
22
+ ));
23
+ }
24
+ if (doInsert) insert accounts;
25
+ return accounts;
26
+ }
27
+
28
+ public static Account createAccount(Boolean doInsert) {
29
+ return createAccounts(1, doInsert)[0];
30
+ }
31
+
32
+ // ─── Contacts ─────────────────────────────────────────────────────────
33
+
34
+ public static List<Contact> createContacts(List<Account> accounts, Integer countPerAccount, Boolean doInsert) {
35
+ List<Contact> contacts = new List<Contact>();
36
+ Integer idx = 0;
37
+ for (Account acc : accounts) {
38
+ for (Integer i = 0; i < countPerAccount; i++) {
39
+ contacts.add(new Contact(
40
+ FirstName = 'Test',
41
+ LastName = 'Contact ' + idx,
42
+ Email = 'test.contact' + idx + '@example.com',
43
+ AccountId = acc.Id
44
+ ));
45
+ idx++;
46
+ }
47
+ }
48
+ if (doInsert) insert contacts;
49
+ return contacts;
50
+ }
51
+
52
+ // ─── Opportunities ────────────────────────────────────────────────────
53
+
54
+ public static List<Opportunity> createOpportunities(List<Account> accounts, Integer countPerAccount, Boolean doInsert) {
55
+ List<Opportunity> opps = new List<Opportunity>();
56
+ Integer idx = 0;
57
+ for (Account acc : accounts) {
58
+ for (Integer i = 0; i < countPerAccount; i++) {
59
+ opps.add(new Opportunity(
60
+ Name = 'Test Opportunity ' + idx,
61
+ AccountId = acc.Id,
62
+ StageName = 'Prospecting',
63
+ CloseDate = Date.today().addDays(30),
64
+ Amount = 10000 + (idx * 1000)
65
+ ));
66
+ idx++;
67
+ }
68
+ }
69
+ if (doInsert) insert opps;
70
+ return opps;
71
+ }
72
+
73
+ // ─── Users ────────────────────────────────────────────────────────────
74
+
75
+ public static User createUser(String profileName, Boolean doInsert) {
76
+ Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
77
+ String uniqueKey = String.valueOf(DateTime.now().getTime());
78
+
79
+ User u = new User(
80
+ FirstName = 'Test',
81
+ LastName = 'User ' + uniqueKey,
82
+ Email = 'testuser' + uniqueKey + '@example.com',
83
+ Username = 'testuser' + uniqueKey + '@example.com.test',
84
+ Alias = 'tuser',
85
+ TimeZoneSidKey = 'America/Los_Angeles',
86
+ LocaleSidKey = 'en_US',
87
+ EmailEncodingKey = 'UTF-8',
88
+ LanguageLocaleKey = 'en_US',
89
+ ProfileId = p.Id
90
+ );
91
+ if (doInsert) insert u;
92
+ return u;
93
+ }
94
+
95
+ // ─── Field Override Pattern ────────────────────────────────────────────
96
+
97
+ public static Account createAccount(Map<String, Object> fieldOverrides, Boolean doInsert) {
98
+ Account acc = new Account(
99
+ Name = 'Test Account',
100
+ Industry = 'Technology'
101
+ );
102
+ for (String fieldName : fieldOverrides.keySet()) {
103
+ acc.put(fieldName, fieldOverrides.get(fieldName));
104
+ }
105
+ if (doInsert) insert acc;
106
+ return acc;
107
+ }
108
+
109
+ // ─── Custom Objects ───────────────────────────────────────────────────
110
+ // Add methods for your custom objects following the same pattern:
111
+ // public static List<MyObject__c> createMyObjects(Integer count, Boolean doInsert) { ... }
112
+ }
@@ -0,0 +1,165 @@
1
+ # Assertion Patterns
2
+
3
+ ## Assertion Methods
4
+
5
+ | Method | Use Case |
6
+ |--------|----------|
7
+ | `System.assertEquals(expected, actual, msg)` | Exact equality |
8
+ | `System.assertNotEquals(expected, actual, msg)` | Value should differ |
9
+ | `System.assert(condition, msg)` | Boolean condition |
10
+
11
+ **Always include the third parameter (message)** - Makes test failures meaningful.
12
+
13
+ ## Good vs Bad Assertions
14
+
15
+ ### ❌ Bad: No message, tests coverage not behavior
16
+
17
+ ```apex
18
+ System.assertEquals(true, result);
19
+ System.assert(accounts.size() > 0);
20
+ ```
21
+
22
+ ### ✅ Good: Descriptive message, tests specific behavior
23
+
24
+ ```apex
25
+ System.assertEquals(true, result, 'Service should return true for valid input');
26
+ System.assertEquals(200, accounts.size(), 'All 200 accounts should be processed');
27
+ ```
28
+
29
+ ## Common Assertion Patterns
30
+
31
+ ### Collection Size
32
+
33
+ ```apex
34
+ // Exact count
35
+ System.assertEquals(200, results.size(), 'Should process all 200 records');
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');
42
+ ```
43
+
44
+ ### Field Values
45
+
46
+ ```apex
47
+ // Single record
48
+ System.assertEquals('Processed', acc.Status__c, 'Account status should be updated to Processed');
49
+
50
+ // All records in collection
51
+ for (Account acc : updatedAccounts) {
52
+ System.assertEquals('Active', acc.Status__c,
53
+ 'Account ' + acc.Name + ' should have Active status');
54
+ }
55
+ ```
56
+
57
+ ### Exception Testing
58
+
59
+ ```apex
60
+ @isTest
61
+ static void shouldThrowException_WhenInputInvalid() {
62
+ Boolean exceptionThrown = false;
63
+ String exceptionMessage = '';
64
+
65
+ Test.startTest();
66
+ try {
67
+ MyService.process(null);
68
+ } catch (MyCustomException e) {
69
+ exceptionThrown = true;
70
+ exceptionMessage = e.getMessage();
71
+ }
72
+ 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
+ }
78
+ ```
79
+
80
+ ### DML Results
81
+
82
+ ```apex
83
+ // Insert success
84
+ Database.SaveResult[] results = Database.insert(accounts, false);
85
+ for (Database.SaveResult sr : results) {
86
+ System.assert(sr.isSuccess(), 'Insert should succeed: ' + sr.getErrors());
87
+ }
88
+
89
+ // Expected failures
90
+ Database.SaveResult sr = Database.insert(invalidAccount, false);
91
+ System.assert(!sr.isSuccess(), 'Insert should fail for invalid data');
92
+ System.assert(sr.getErrors()[0].getMessage().contains('REQUIRED_FIELD_MISSING'),
93
+ 'Error should indicate missing required field');
94
+ ```
95
+
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
+ ### Null Checks
124
+
125
+ ```apex
126
+ // Should be null
127
+ System.assertEquals(null, result.ErrorMessage__c, 'No error expected for valid input');
128
+
129
+ // Should not be null
130
+ System.assertNotEquals(null, result.Id, 'Record should have been inserted');
131
+ ```
132
+
133
+ ## Anti-Patterns to Avoid
134
+
135
+ ### ❌ Testing implementation, not behavior
136
+
137
+ ```apex
138
+ // Bad: Testing that a specific method was called
139
+ System.assert(MyClass.methodWasCalled, 'Method should be called');
140
+
141
+ // Good: Testing the observable outcome
142
+ System.assertEquals('Expected Value', record.Field__c, 'Field should be updated');
143
+ ```
144
+
145
+ ### ❌ Overly generic assertions
146
+
147
+ ```apex
148
+ // Bad: Passes for any non-empty result
149
+ System.assert(results.size() > 0);
150
+
151
+ // Good: Verifies exact expected count
152
+ System.assertEquals(200, results.size(), 'All 200 records should be returned');
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
+ ```
@@ -0,0 +1,276 @@
1
+ # Async Testing Patterns
2
+
3
+ ## Key Principle
4
+
5
+ `Test.stopTest()` forces all async operations to execute synchronously, allowing assertions on their results.
6
+
7
+ ## Batch Apex Testing
8
+
9
+ ### Basic Batch Test
10
+
11
+ ```apex
12
+ @isTest
13
+ static void shouldProcessAllRecords_WhenBatchExecutes() {
14
+ // Given: Create test data
15
+ List<Account> accounts = TestDataFactory.createAccounts(200, true);
16
+
17
+ // When: Execute batch
18
+ Test.startTest();
19
+ MyBatchClass batch = new MyBatchClass();
20
+ Id batchId = Database.executeBatch(batch, 200);
21
+ Test.stopTest(); // Forces batch to complete
22
+
23
+ // Then: Verify results
24
+ List<Account> updated = [SELECT Id, Status__c FROM Account];
25
+ for (Account acc : updated) {
26
+ System.assertEquals('Processed', acc.Status__c,
27
+ 'Batch should update all account statuses');
28
+ }
29
+ }
30
+ ```
31
+
32
+ ### Testing Batch with Failures
33
+
34
+ ```apex
35
+ @isTest
36
+ static void shouldLogErrors_WhenRecordsFail() {
37
+ // Given: Create mix of valid and invalid records
38
+ List<Account> accounts = TestDataFactory.createAccounts(198, true);
39
+
40
+ // Create 2 accounts that will fail processing
41
+ List<Account> invalidAccounts = new List<Account>();
42
+ for (Integer i = 0; i < 2; i++) {
43
+ invalidAccounts.add(new Account(
44
+ Name = 'Invalid Account ' + i,
45
+ Invalid_Field__c = 'triggers_validation_error'
46
+ ));
47
+ }
48
+ insert invalidAccounts;
49
+
50
+ // When
51
+ Test.startTest();
52
+ MyBatchClass batch = new MyBatchClass();
53
+ Database.executeBatch(batch, 50);
54
+ Test.stopTest();
55
+
56
+ // Then
57
+ List<Error_Log__c> errors = [SELECT Id, Message__c FROM Error_Log__c];
58
+ System.assertEquals(2, errors.size(), 'Should log 2 failed records');
59
+ }
60
+ ```
61
+
62
+ ### Testing Batch Scope
63
+
64
+ ```apex
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
+ ```
80
+
81
+ ## Queueable Testing
82
+
83
+ ### Basic Queueable Test
84
+
85
+ ```apex
86
+ @isTest
87
+ static void shouldCompleteProcessing_WhenQueueableEnqueued() {
88
+ // Given
89
+ Account acc = TestDataFactory.createAccount(true);
90
+
91
+ // When
92
+ Test.startTest();
93
+ MyQueueableClass queueable = new MyQueueableClass(acc.Id);
94
+ System.enqueueJob(queueable);
95
+ Test.stopTest(); // Forces queueable to complete
96
+
97
+ // Then
98
+ Account updated = [SELECT Id, Status__c FROM Account WHERE Id = :acc.Id];
99
+ System.assertEquals('Processed', updated.Status__c,
100
+ 'Queueable should update account status');
101
+ }
102
+ ```
103
+
104
+ ### Testing Queueable Chaining
105
+
106
+ Chained queueables only execute the first job in tests:
107
+
108
+ ```apex
109
+ @isTest
110
+ static void shouldChainNextJob_WhenMoreRecordsExist() {
111
+ // Given: More records than one queueable can process
112
+ List<Account> accounts = TestDataFactory.createAccounts(500, true);
113
+
114
+ Test.startTest();
115
+ // First queueable processes batch 1 and chains next
116
+ MyChainedQueueable queueable = new MyChainedQueueable(0, 100);
117
+ System.enqueueJob(queueable);
118
+ Test.stopTest();
119
+
120
+ // Verify first batch processed
121
+ List<Account> processed = [SELECT Id FROM Account WHERE Processed__c = true];
122
+ System.assertEquals(100, processed.size(), 'First batch should process 100 records');
123
+
124
+ // Verify chain was enqueued (check AsyncApexJob)
125
+ List<AsyncApexJob> jobs = [
126
+ SELECT Id, Status, JobType
127
+ FROM AsyncApexJob
128
+ WHERE ApexClass.Name = 'MyChainedQueueable'
129
+ ];
130
+ System.assert(jobs.size() >= 1, 'Chained job should be enqueued');
131
+ }
132
+ ```
133
+
134
+ ### Testing Queueable with Callouts
135
+
136
+ ```apex
137
+ @isTest
138
+ static void shouldMakeCallout_WhenQueueableWithCallout() {
139
+ // Given
140
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponse(200, '{"status":"ok"}'));
141
+ Account acc = TestDataFactory.createAccount(true);
142
+
143
+ // When
144
+ Test.startTest();
145
+ MyQueueableWithCallout queueable = new MyQueueableWithCallout(acc.Id);
146
+ System.enqueueJob(queueable);
147
+ Test.stopTest();
148
+
149
+ // Then
150
+ Account updated = [SELECT Id, External_Status__c FROM Account WHERE Id = :acc.Id];
151
+ System.assertEquals('Synced', updated.External_Status__c,
152
+ 'Should update status after successful callout');
153
+ }
154
+ ```
155
+
156
+ ## Future Method Testing
157
+
158
+ ```apex
159
+ @isTest
160
+ static void shouldExecuteFutureMethod() {
161
+ // Given
162
+ Account acc = TestDataFactory.createAccount(true);
163
+
164
+ // When
165
+ Test.startTest();
166
+ MyClass.processFuture(acc.Id); // @future method
167
+ Test.stopTest(); // Forces future to complete
168
+
169
+ // Then
170
+ Account updated = [SELECT Id, Processed__c FROM Account WHERE Id = :acc.Id];
171
+ System.assertEquals(true, updated.Processed__c, 'Future should process record');
172
+ }
173
+ ```
174
+
175
+ ## Scheduled Apex Testing
176
+
177
+ ### Testing Scheduled Execution
178
+
179
+ ```apex
180
+ @isTest
181
+ static void shouldExecuteScheduledJob() {
182
+ // Given
183
+ List<Account> accounts = TestDataFactory.createAccounts(50, true);
184
+
185
+ // When
186
+ 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
+ MyScheduledClass scheduled = new MyScheduledClass();
192
+ scheduled.execute(null); // Pass null SchedulableContext in tests
193
+ Test.stopTest();
194
+
195
+ // Then
196
+ List<Account> processed = [SELECT Id FROM Account WHERE Processed__c = true];
197
+ System.assertEquals(50, processed.size(), 'Scheduled job should process records');
198
+ }
199
+ ```
200
+
201
+ ### Testing Schedule Registration
202
+
203
+ ```apex
204
+ @isTest
205
+ static void shouldScheduleJob() {
206
+ Test.startTest();
207
+ String cronExp = '0 0 6 * * ?'; // Daily at 6 AM
208
+ String jobId = System.schedule('Daily Processing', cronExp, new MyScheduledClass());
209
+ Test.stopTest();
210
+
211
+ // Verify job is scheduled
212
+ CronTrigger ct = [
213
+ SELECT Id, CronExpression, State
214
+ FROM CronTrigger
215
+ WHERE Id = :jobId
216
+ ];
217
+ System.assertEquals('0 0 6 * * ?', ct.CronExpression, 'CRON should match');
218
+ System.assertEquals('WAITING', ct.State, 'Job should be waiting');
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);
241
+ }
242
+ ```
243
+
244
+ ## Common Pitfalls
245
+
246
+ ### ❌ Forgetting Test.stopTest()
247
+
248
+ ```apex
249
+ // Bad: Async never executes
250
+ Test.startTest();
251
+ System.enqueueJob(new MyQueueable());
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
+ ```