@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.
Files changed (27) hide show
  1. package/package.json +3 -3
  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-react-site/SKILL.md +11 -0
  26. package/skills/generating-flexipage/SKILL.md +39 -57
  27. package/skills/searching-media/SKILL.md +342 -0
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @description Batch Apex class for identifying and flagging duplicate Account records.
3
+ * Compares Accounts by Name and BillingPostalCode to find potential duplicates.
4
+ * Flags duplicates by setting the Is_Potential_Duplicate__c checkbox.
5
+ * Implements Database.Stateful to track results across batch chunks.
6
+ * @author Generated by Apex Class Writer Skill
7
+ *
8
+ * @example
9
+ * // Run with default batch size (200)
10
+ * Id jobId = AccountDeduplicationBatch.run();
11
+ *
12
+ * // Run with smaller batch size for complex processing
13
+ * Database.executeBatch(new AccountDeduplicationBatch(), 50);
14
+ */
15
+ public with sharing class AccountDeduplicationBatch implements Database.Batchable<SObject>, Database.Stateful {
16
+
17
+ // ─── Constants ───────────────────────────────────────────────────────
18
+ private static final Integer DEFAULT_BATCH_SIZE = 200;
19
+
20
+ // ─── Stateful Tracking ───────────────────────────────────────────────
21
+ private Integer totalScanned = 0;
22
+ private Integer duplicatesFound = 0;
23
+ private Integer totalErrors = 0;
24
+ private List<String> errorMessages = new List<String>();
25
+
26
+ // ─── Batchable Interface ─────────────────────────────────────────────
27
+
28
+ /**
29
+ * @description Queries all active Accounts that haven't already been flagged
30
+ * @param bc The batch context
31
+ * @return QueryLocator scoped to unflagged active Accounts
32
+ */
33
+ public Database.QueryLocator start(Database.BatchableContext bc) {
34
+ return Database.getQueryLocator([
35
+ SELECT Id, Name, BillingPostalCode, Is_Potential_Duplicate__c
36
+ FROM Account
37
+ WHERE IsDeleted = FALSE
38
+ AND Is_Potential_Duplicate__c = FALSE
39
+ ORDER BY Name ASC
40
+ ]);
41
+ }
42
+
43
+ /**
44
+ * @description Processes each batch by building a duplicate key and checking for matches.
45
+ * Uses a composite key of normalized Name + BillingPostalCode.
46
+ * @param bc The batch context
47
+ * @param scope List of Account records in the current batch
48
+ */
49
+ public void execute(Database.BatchableContext bc, List<Account> scope) {
50
+ totalScanned += scope.size();
51
+
52
+ // Build duplicate keys for this batch
53
+ Map<String, List<Account>> dupeKeyMap = new Map<String, List<Account>>();
54
+ for (Account acct : scope) {
55
+ String key = buildDuplicateKey(acct);
56
+ if (String.isNotBlank(key)) {
57
+ if (!dupeKeyMap.containsKey(key)) {
58
+ dupeKeyMap.put(key, new List<Account>());
59
+ }
60
+ dupeKeyMap.get(key).add(acct);
61
+ }
62
+ }
63
+
64
+ // Flag records that share a key with another record
65
+ List<Account> toUpdate = new List<Account>();
66
+ for (String key : dupeKeyMap.keySet()) {
67
+ List<Account> group = dupeKeyMap.get(key);
68
+ if (group.size() > 1) {
69
+ for (Account acct : group) {
70
+ toUpdate.add(new Account(
71
+ Id = acct.Id,
72
+ Is_Potential_Duplicate__c = true
73
+ ));
74
+ }
75
+ }
76
+ }
77
+
78
+ if (!toUpdate.isEmpty()) {
79
+ List<Database.SaveResult> results = Database.update(toUpdate, false);
80
+ processResults(results);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * @description Logs a summary of the deduplication batch run
86
+ * @param bc The batch context
87
+ */
88
+ public void finish(Database.BatchableContext bc) {
89
+ String summary = String.format(
90
+ 'AccountDeduplicationBatch completed. Scanned: {0}, Duplicates flagged: {1}, Errors: {2}',
91
+ new List<Object>{ totalScanned, duplicatesFound, totalErrors }
92
+ );
93
+
94
+ System.debug(LoggingLevel.INFO, summary);
95
+
96
+ if (!errorMessages.isEmpty()) {
97
+ System.debug(LoggingLevel.ERROR, 'Error details:\n' + String.join(errorMessages, '\n'));
98
+ }
99
+ }
100
+
101
+ // ─── Private Helpers ─────────────────────────────────────────────────
102
+
103
+ /**
104
+ * @description Builds a normalized composite key for duplicate detection
105
+ * @param acct The Account record
106
+ * @return Normalized key string, or null if insufficient data
107
+ */
108
+ private String buildDuplicateKey(Account acct) {
109
+ if (String.isBlank(acct.Name)) {
110
+ return null;
111
+ }
112
+
113
+ String normalizedName = acct.Name.trim().toUpperCase().replaceAll('\\s+', ' ');
114
+ String postalCode = (acct.BillingPostalCode ?? '').trim().toUpperCase();
115
+
116
+ return normalizedName + '|' + postalCode;
117
+ }
118
+
119
+ /**
120
+ * @description Processes DML results, tracking successes and failures
121
+ * @param results List of Database.SaveResult from update operation
122
+ */
123
+ private void processResults(List<Database.SaveResult> results) {
124
+ for (Database.SaveResult sr : results) {
125
+ if (sr.isSuccess()) {
126
+ duplicatesFound++;
127
+ } else {
128
+ totalErrors++;
129
+ for (Database.Error err : sr.getErrors()) {
130
+ errorMessages.add(
131
+ 'Record ' + sr.getId() + ': ' +
132
+ err.getStatusCode() + ' - ' + err.getMessage()
133
+ );
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ // ─── Static Helpers ──────────────────────────────────────────────────
140
+
141
+ /**
142
+ * @description Convenience method to execute with default batch size
143
+ * @return The batch job Id
144
+ */
145
+ public static Id run() {
146
+ return Database.executeBatch(new AccountDeduplicationBatch(), DEFAULT_BATCH_SIZE);
147
+ }
148
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * @description Selector class for Account queries.
3
+ * Encapsulates all SOQL for Account records.
4
+ * All methods return bulkified results (Lists or Maps).
5
+ * @author Generated by Apex Class Writer Skill
6
+ */
7
+ public with sharing class AccountSelector {
8
+
9
+ // ─── Field Lists ─────────────────────────────────────────────────────
10
+
11
+ /**
12
+ * @description Returns the default set of fields to query for Account.
13
+ * Centralizes field references to keep queries DRY.
14
+ * @return Comma-separated field list as a String
15
+ */
16
+ private static String getDefaultFields() {
17
+ return String.join(
18
+ new List<String>{
19
+ 'Id',
20
+ 'Name',
21
+ 'AccountNumber',
22
+ 'Type',
23
+ 'Industry',
24
+ 'AnnualRevenue',
25
+ 'NumberOfEmployees',
26
+ 'Phone',
27
+ 'Website',
28
+ 'OwnerId',
29
+ 'CreatedDate',
30
+ 'LastModifiedDate'
31
+ },
32
+ ', '
33
+ );
34
+ }
35
+
36
+ /**
37
+ * @description Returns fields needed for billing/territory operations
38
+ * @return Comma-separated field list as a String
39
+ */
40
+ private static String getBillingFields() {
41
+ return String.join(
42
+ new List<String>{
43
+ 'Id',
44
+ 'Name',
45
+ 'BillingStreet',
46
+ 'BillingCity',
47
+ 'BillingState',
48
+ 'BillingPostalCode',
49
+ 'BillingCountry',
50
+ 'Territory__c'
51
+ },
52
+ ', '
53
+ );
54
+ }
55
+
56
+ // ─── Query Methods ───────────────────────────────────────────────────
57
+
58
+ /**
59
+ * @description Selects Account records by their Ids
60
+ * @param recordIds Set of Account Ids to query
61
+ * @return List of Account records matching the provided Ids
62
+ * @example
63
+ * Set<Id> ids = new Set<Id>{ '001xx000003DGbY' };
64
+ * List<Account> results = AccountSelector.selectByIds(ids);
65
+ */
66
+ public static List<Account> selectByIds(Set<Id> recordIds) {
67
+ if (recordIds == null || recordIds.isEmpty()) {
68
+ return new List<Account>();
69
+ }
70
+
71
+ return Database.query(
72
+ 'SELECT ' + getDefaultFields() +
73
+ ' FROM Account' +
74
+ ' WHERE Id IN :recordIds'
75
+ );
76
+ }
77
+
78
+ /**
79
+ * @description Selects Account records as a Map keyed by Id
80
+ * @param recordIds Set of Account Ids to query
81
+ * @return Map of Id to Account
82
+ */
83
+ public static Map<Id, Account> selectMapByIds(Set<Id> recordIds) {
84
+ return new Map<Id, Account>(selectByIds(recordIds));
85
+ }
86
+
87
+ /**
88
+ * @description Selects Accounts with billing address fields for territory assignment
89
+ * @param recordIds Set of Account Ids to query
90
+ * @return List of Account records with billing address fields populated
91
+ */
92
+ public static List<Account> selectWithBillingAddress(Set<Id> recordIds) {
93
+ if (recordIds == null || recordIds.isEmpty()) {
94
+ return new List<Account>();
95
+ }
96
+
97
+ return Database.query(
98
+ 'SELECT ' + getBillingFields() +
99
+ ' FROM Account' +
100
+ ' WHERE Id IN :recordIds'
101
+ );
102
+ }
103
+
104
+ /**
105
+ * @description Selects Accounts by Account Type
106
+ * @param accountTypes Set of Account Type values to filter by
107
+ * @return List of matching Account records
108
+ * @example
109
+ * List<Account> prospects = AccountSelector.selectByType(
110
+ * new Set<String>{ 'Prospect', 'Customer - Direct' }
111
+ * );
112
+ */
113
+ public static List<Account> selectByType(Set<String> accountTypes) {
114
+ if (accountTypes == null || accountTypes.isEmpty()) {
115
+ return new List<Account>();
116
+ }
117
+
118
+ return Database.query(
119
+ 'SELECT ' + getDefaultFields() +
120
+ ' FROM Account' +
121
+ ' WHERE Type IN :accountTypes' +
122
+ ' ORDER BY Name ASC'
123
+ );
124
+ }
125
+
126
+ /**
127
+ * @description Selects Accounts by Industry with a minimum annual revenue
128
+ * @param industries Set of Industry values to filter by
129
+ * @param minRevenue Minimum AnnualRevenue threshold
130
+ * @return List of matching Account records ordered by revenue descending
131
+ * @example
132
+ * List<Account> techAccounts = AccountSelector.selectByIndustryAndRevenue(
133
+ * new Set<String>{ 'Technology' },
134
+ * 1000000
135
+ * );
136
+ */
137
+ public static List<Account> selectByIndustryAndRevenue(Set<String> industries, Decimal minRevenue) {
138
+ if (industries == null || industries.isEmpty()) {
139
+ return new List<Account>();
140
+ }
141
+
142
+ Decimal revenueThreshold = minRevenue ?? 0;
143
+
144
+ return Database.query(
145
+ 'SELECT ' + getDefaultFields() +
146
+ ' FROM Account' +
147
+ ' WHERE Industry IN :industries' +
148
+ ' AND AnnualRevenue >= :revenueThreshold' +
149
+ ' ORDER BY AnnualRevenue DESC'
150
+ );
151
+ }
152
+
153
+ /**
154
+ * @description Selects Accounts with their related Contacts (subquery)
155
+ * @param recordIds Set of Account Ids to query
156
+ * @return List of Account records with nested Contacts
157
+ */
158
+ public static List<Account> selectWithContacts(Set<Id> recordIds) {
159
+ if (recordIds == null || recordIds.isEmpty()) {
160
+ return new List<Account>();
161
+ }
162
+
163
+ return [
164
+ SELECT Id, Name, Type, Industry,
165
+ (SELECT Id, FirstName, LastName, Email, Title
166
+ FROM Contacts
167
+ ORDER BY LastName ASC)
168
+ FROM Account
169
+ WHERE Id IN :recordIds
170
+ ];
171
+ }
172
+
173
+ // ─── Aggregate Queries ───────────────────────────────────────────────
174
+
175
+ /**
176
+ * @description Returns a count of Accounts grouped by Industry
177
+ * @return List of AggregateResult with Industry and record count
178
+ * @example
179
+ * List<AggregateResult> results = AccountSelector.countByIndustry();
180
+ * for (AggregateResult ar : results) {
181
+ * System.debug(ar.get('Industry') + ': ' + ar.get('cnt'));
182
+ * }
183
+ */
184
+ public static List<AggregateResult> countByIndustry() {
185
+ return [
186
+ SELECT Industry, COUNT(Id) cnt
187
+ FROM Account
188
+ WHERE Industry != NULL
189
+ GROUP BY Industry
190
+ ORDER BY COUNT(Id) DESC
191
+ ];
192
+ }
193
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * @description Service class for Account business logic.
3
+ * Provides account deduplication, enrichment, and territory assignment.
4
+ * Delegates queries to AccountSelector and SObject manipulation to AccountDomain.
5
+ * @author Generated by Apex Class Writer Skill
6
+ */
7
+ public with sharing class AccountService {
8
+
9
+ // ─── Constants ───────────────────────────────────────────────────────
10
+ private static final String ERROR_NULL_INPUT = 'Input cannot be null or empty.';
11
+ private static final String ERROR_MERGE_FAILED = 'Account merge failed for master Id: ';
12
+ private static final Integer MAX_MERGE_BATCH = 3;
13
+
14
+ // ─── Public API ──────────────────────────────────────────────────────
15
+
16
+ /**
17
+ * @description Merges duplicate Account records into a master record.
18
+ * The master record retains its field values; child records are reparented.
19
+ * @param masterIds Map of master Account Id to Set of duplicate Account Ids to merge
20
+ * @return List of master Account Ids that were successfully merged
21
+ * @throws AccountServiceException if merge processing fails
22
+ * @example
23
+ * Map<Id, Set<Id>> mergeMap = new Map<Id, Set<Id>>{
24
+ * masterAcctId => new Set<Id>{ dupeId1, dupeId2 }
25
+ * };
26
+ * List<Id> mergedIds = AccountService.mergeAccounts(mergeMap);
27
+ */
28
+ public static List<Id> mergeAccounts(Map<Id, Set<Id>> mergeMap) {
29
+ if (mergeMap == null || mergeMap.isEmpty()) {
30
+ throw new AccountServiceException(ERROR_NULL_INPUT);
31
+ }
32
+
33
+ // Collect all Ids for a single query
34
+ Set<Id> allIds = new Set<Id>();
35
+ allIds.addAll(mergeMap.keySet());
36
+ for (Set<Id> dupeIds : mergeMap.values()) {
37
+ allIds.addAll(dupeIds);
38
+ }
39
+
40
+ // Query all records at once via Selector
41
+ Map<Id, Account> accountMap = AccountSelector.selectMapByIds(allIds);
42
+
43
+ List<Id> successfulMerges = new List<Id>();
44
+ List<String> errors = new List<String>();
45
+
46
+ for (Id masterId : mergeMap.keySet()) {
47
+ Account master = accountMap.get(masterId);
48
+ if (master == null) {
49
+ errors.add('Master account not found: ' + masterId);
50
+ continue;
51
+ }
52
+
53
+ List<Account> duplicates = new List<Account>();
54
+ for (Id dupeId : mergeMap.get(masterId)) {
55
+ Account dupe = accountMap.get(dupeId);
56
+ if (dupe != null) {
57
+ duplicates.add(dupe);
58
+ }
59
+ }
60
+
61
+ try {
62
+ // Apex merge supports up to 3 records at a time
63
+ for (List<Account> chunk : chunkAccounts(duplicates, MAX_MERGE_BATCH)) {
64
+ Database.merge(master, chunk);
65
+ }
66
+ successfulMerges.add(masterId);
67
+ } catch (DmlException e) {
68
+ errors.add(ERROR_MERGE_FAILED + masterId + ' - ' + e.getMessage());
69
+ }
70
+ }
71
+
72
+ if (!errors.isEmpty()) {
73
+ System.debug(LoggingLevel.WARN, 'Merge errors: ' + String.join(errors, '\n'));
74
+ }
75
+
76
+ return successfulMerges;
77
+ }
78
+
79
+ /**
80
+ * @description Assigns accounts to territories based on Billing State/Country.
81
+ * Uses Custom Metadata Type (Territory_Mapping__mdt) for mappings.
82
+ * @param accountIds Set of Account Ids to assign territories for
83
+ * @return Number of accounts successfully updated
84
+ * @throws AccountServiceException if territory assignment fails
85
+ */
86
+ public static Integer assignTerritories(Set<Id> accountIds) {
87
+ if (accountIds == null || accountIds.isEmpty()) {
88
+ throw new AccountServiceException(ERROR_NULL_INPUT);
89
+ }
90
+
91
+ List<Account> accounts = AccountSelector.selectWithBillingAddress(accountIds);
92
+ Map<String, String> territoryMap = loadTerritoryMappings();
93
+
94
+ List<Account> toUpdate = new List<Account>();
95
+ for (Account acct : accounts) {
96
+ String key = buildTerritoryKey(acct.BillingState, acct.BillingCountry);
97
+ String territory = territoryMap.get(key);
98
+
99
+ if (territory != null && territory != acct.Territory__c) {
100
+ toUpdate.add(new Account(
101
+ Id = acct.Id,
102
+ Territory__c = territory
103
+ ));
104
+ }
105
+ }
106
+
107
+ if (!toUpdate.isEmpty()) {
108
+ List<Database.SaveResult> results = Database.update(toUpdate, false);
109
+ return countSuccesses(results);
110
+ }
111
+
112
+ return 0;
113
+ }
114
+
115
+ // ─── Convenience Overloads ───────────────────────────────────────────
116
+
117
+ /**
118
+ * @description Single-account territory assignment convenience method
119
+ * @param accountId The Account Id to assign a territory for
120
+ * @return 1 if updated, 0 if no change needed
121
+ */
122
+ public static Integer assignTerritory(Id accountId) {
123
+ return assignTerritories(new Set<Id>{ accountId });
124
+ }
125
+
126
+ // ─── Private Helpers ─────────────────────────────────────────────────
127
+
128
+ /**
129
+ * @description Loads territory mappings from Custom Metadata
130
+ * @return Map of territory key (State:Country) to territory name
131
+ */
132
+ private static Map<String, String> loadTerritoryMappings() {
133
+ Map<String, String> mappings = new Map<String, String>();
134
+ for (Territory_Mapping__mdt mapping : Territory_Mapping__mdt.getAll().values()) {
135
+ String key = buildTerritoryKey(mapping.State__c, mapping.Country__c);
136
+ mappings.put(key, mapping.Territory_Name__c);
137
+ }
138
+ return mappings;
139
+ }
140
+
141
+ /**
142
+ * @description Builds a consistent territory lookup key
143
+ * @param state The billing state
144
+ * @param country The billing country
145
+ * @return A normalized key string
146
+ */
147
+ private static String buildTerritoryKey(String state, String country) {
148
+ return (state ?? '').toUpperCase() + ':' + (country ?? '').toUpperCase();
149
+ }
150
+
151
+ /**
152
+ * @description Chunks a list of Accounts into sublists of the given size
153
+ * @param accounts The accounts to chunk
154
+ * @param chunkSize Maximum chunk size
155
+ * @return List of account sublists
156
+ */
157
+ private static List<List<Account>> chunkAccounts(List<Account> accounts, Integer chunkSize) {
158
+ List<List<Account>> chunks = new List<List<Account>>();
159
+ List<Account> current = new List<Account>();
160
+
161
+ for (Account acct : accounts) {
162
+ current.add(acct);
163
+ if (current.size() == chunkSize) {
164
+ chunks.add(current);
165
+ current = new List<Account>();
166
+ }
167
+ }
168
+ if (!current.isEmpty()) {
169
+ chunks.add(current);
170
+ }
171
+ return chunks;
172
+ }
173
+
174
+ /**
175
+ * @description Counts successful results from a DML operation
176
+ * @param results List of Database.SaveResult
177
+ * @return Count of successful operations
178
+ */
179
+ private static Integer countSuccesses(List<Database.SaveResult> results) {
180
+ Integer count = 0;
181
+ for (Database.SaveResult sr : results) {
182
+ if (sr.isSuccess()) {
183
+ count++;
184
+ } else {
185
+ for (Database.Error err : sr.getErrors()) {
186
+ System.debug(LoggingLevel.WARN,
187
+ 'Update failed for ' + sr.getId() + ': ' + err.getMessage()
188
+ );
189
+ }
190
+ }
191
+ }
192
+ return count;
193
+ }
194
+
195
+ // ─── Exception ───────────────────────────────────────────────────────
196
+
197
+ /**
198
+ * @description Custom exception for AccountService errors
199
+ */
200
+ public class AccountServiceException extends Exception {}
201
+ }
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: generating-apex-test
3
+ description: Apex test class generation with TestDataFactory patterns, bulk testing (200+ records), mocking strategies, and assertion best practices. Use this skill when the user asks to create, write, or improve Apex test classes, add coverage, build mocks, or implement testing patterns for triggers, services, batch jobs, queueables, and integrations.
4
+ ---
5
+
6
+ # Apex Test Class Skill
7
+
8
+ ## Core Principles
9
+
10
+ 1. **Bulkify tests** - Always test with 200+ records to catch governor limit issues
11
+ 2. **Isolate test data** - Use `@TestSetup` and TestDataFactory; never rely on org data
12
+ 3. **Assert meaningfully** - Test behavior, not just coverage; include failure messages
13
+ 4. **Mock external dependencies** - Use `HttpCalloutMock`, `Test.setMock()` for integrations
14
+ 5. **Test negative paths** - Validate error handling, not just happy paths
15
+
16
+ ## Test Class Structure
17
+
18
+ ```apex
19
+ @isTest
20
+ private class MyServiceTest {
21
+
22
+ @TestSetup
23
+ static void setupTestData() {
24
+ // Create shared test data using TestDataFactory
25
+ List<Account> accounts = TestDataFactory.createAccounts(200, true);
26
+ }
27
+
28
+ @isTest
29
+ static void shouldPerformExpectedBehavior_WhenValidInput() {
30
+ // Given: Setup specific test state
31
+ List<Account> accounts = [SELECT Id, Name FROM Account];
32
+
33
+ // When: Execute the code under test
34
+ Test.startTest();
35
+ MyService.processAccounts(accounts);
36
+ Test.stopTest();
37
+
38
+ // Then: Assert expected outcomes
39
+ List<Account> updated = [SELECT Id, Status__c FROM Account];
40
+ System.assertEquals(200, updated.size(), 'All accounts should be processed');
41
+ for (Account acc : updated) {
42
+ System.assertEquals('Processed', acc.Status__c, 'Status should be updated');
43
+ }
44
+ }
45
+
46
+ @isTest
47
+ static void shouldThrowException_WhenInvalidInput() {
48
+ // Given
49
+ List<Account> emptyList = new List<Account>();
50
+
51
+ // When/Then
52
+ Test.startTest();
53
+ try {
54
+ MyService.processAccounts(emptyList);
55
+ System.assert(false, 'Expected MyCustomException to be thrown');
56
+ } catch (MyCustomException e) {
57
+ System.assert(e.getMessage().contains('cannot be empty'),
58
+ 'Exception message should indicate empty input');
59
+ }
60
+ Test.stopTest();
61
+ }
62
+ }
63
+ ```
64
+
65
+ ## Naming Convention
66
+
67
+ Use descriptive method names: `should[ExpectedBehavior]_When[Condition]`
68
+
69
+ Examples:
70
+ - `shouldCreateContact_WhenAccountIsActive`
71
+ - `shouldThrowException_WhenEmailIsInvalid`
72
+ - `shouldSendNotification_WhenOpportunityClosedWon`
73
+ - `shouldBypassTrigger_WhenRunningAsBatch`
74
+
75
+ ## Test.startTest() / Test.stopTest()
76
+
77
+ Always wrap the code under test:
78
+ - Resets governor limits for accurate limit testing
79
+ - Executes async operations synchronously (queueables, batch, future)
80
+ - Fires scheduled jobs immediately
81
+
82
+ ## Asset Templates
83
+
84
+ Ready-to-use scaffolds for common test patterns:
85
+
86
+ - **[assets/test-class-template.cls](assets/test-class-template.cls)** - Starter test class with positive, negative, bulk, and governor limit test stubs
87
+ - **[assets/test-data-factory-template.cls](assets/test-data-factory-template.cls)** - TestDataFactory with Account, Contact, Opportunity, User factories and field override support
88
+
89
+ ## Reference Files
90
+
91
+ Detailed patterns for specific scenarios:
92
+
93
+ - **[references/test-data-factory.md](references/test-data-factory.md)** - TestDataFactory class patterns and field defaults
94
+ - **[references/assertion-patterns.md](references/assertion-patterns.md)** - Assertion best practices and common pitfalls
95
+ - **[references/mocking-patterns.md](references/mocking-patterns.md)** - HttpCalloutMock, Test.setMock(), stubbing
96
+ - **[references/async-testing.md](references/async-testing.md)** - Batch, Queueable, Future, Scheduled job testing
97
+
98
+ ## Quick Reference: What to Test
99
+
100
+ | Component | Key Test Scenarios |
101
+ |-----------|-------------------|
102
+ | Trigger | Bulk insert/update/delete, recursion, field changes |
103
+ | Service | Valid/invalid inputs, bulk operations, exceptions |
104
+ | Controller | Page load, action methods, view state |
105
+ | Batch | Start/execute/finish, chunking, error records |
106
+ | Queueable | Chaining, bulkification, error handling |
107
+ | Callout | Success response, error response, timeout |
108
+ | Scheduled | Execution, CRON validation |