@salesforce/afv-skills 1.5.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.
- package/package.json +3 -3
- package/skills/creating-webapp/SKILL.md +0 -2
- package/skills/generating-apex/SKILL.md +253 -0
- package/skills/generating-apex/assets/abstract.cls +128 -0
- package/skills/generating-apex/assets/batch.cls +125 -0
- package/skills/generating-apex/assets/domain.cls +102 -0
- package/skills/generating-apex/assets/dto.cls +108 -0
- package/skills/generating-apex/assets/exception.cls +51 -0
- package/skills/generating-apex/assets/interface.cls +25 -0
- package/skills/generating-apex/assets/queueable.cls +92 -0
- package/skills/generating-apex/assets/schedulable.cls +75 -0
- package/skills/generating-apex/assets/selector.cls +92 -0
- package/skills/generating-apex/assets/service.cls +69 -0
- package/skills/generating-apex/assets/utility.cls +97 -0
- package/skills/generating-apex/references/AccountDeduplicationBatch.cls +148 -0
- package/skills/generating-apex/references/AccountSelector.cls +193 -0
- package/skills/generating-apex/references/AccountService.cls +201 -0
- package/skills/generating-apex-test/SKILL.md +108 -0
- package/skills/generating-apex-test/assets/test-class-template.cls +124 -0
- package/skills/generating-apex-test/assets/test-data-factory-template.cls +112 -0
- package/skills/generating-apex-test/references/assertion-patterns.md +165 -0
- package/skills/generating-apex-test/references/async-testing.md +276 -0
- package/skills/generating-apex-test/references/mocking-patterns.md +219 -0
- package/skills/generating-apex-test/references/test-data-factory.md +176 -0
- package/skills/generating-experience-react-site/SKILL.md +11 -0
- package/skills/generating-flexipage/SKILL.md +39 -57
- 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
|
+
```
|