mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-08 16:25:58 +08:00
@W-21623314 Adding Apex skill and Apex testing skill
This commit is contained in:
parent
3f5b5f0baf
commit
17621bb6d1
@ -199,3 +199,7 @@ A maintainer will merge your PR after approval.
|
|||||||
- Open a PR.
|
- Open a PR.
|
||||||
- Respond to feedback.
|
- Respond to feedback.
|
||||||
- Keep your fork updated.
|
- Keep your fork updated.
|
||||||
|
|
||||||
|
> **Note:** Our rules forbid specific mentions of community members or
|
||||||
|
> endorsements of sellable materials like training courses, books, etc.
|
||||||
|
> Please ensure your contributions do not include these.
|
||||||
|
|||||||
28
skills/CREDITS.md
Normal file
28
skills/CREDITS.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Credits & Acknowledgments
|
||||||
|
|
||||||
|
The skills in this library were built upon the collective wisdom of the Salesforce developer community. We gratefully acknowledge the following authors and resources whose ideas, patterns, and best practices have shaped these skills.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authors & Contributors
|
||||||
|
|
||||||
|
### Jag Valaiyapathy (**[Jaganpro)](https://github.com/Jaganpro)**
|
||||||
|
|
||||||
|
**[sf-skills](https://github.com/Jaganpro/sf-skills)**
|
||||||
|
|
||||||
|
Key contributions:
|
||||||
|
|
||||||
|
- Pioneering open-source Salesforce skills for agentic coding tools
|
||||||
|
- Apex code generation and review patterns
|
||||||
|
- Best practices, anti-patterns, and design patterns reference material
|
||||||
|
- Template library for common Apex class types
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Special Thanks
|
||||||
|
|
||||||
|
To the entire Salesforce developer community for sharing knowledge, writing blogs, creating open-source tools, and helping each other build better solutions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*If we've missed anyone whose work influenced these skills, please let us know so we can add proper attribution.*
|
||||||
@ -37,6 +37,22 @@ Before generating or fixing tests, identify:
|
|||||||
|
|
||||||
Apply the structure, naming conventions, and patterns below. Reference the appropriate asset templates and reference docs for the component type.
|
Apply the structure, naming conventions, and patterns below. Reference the appropriate asset templates and reference docs for the component type.
|
||||||
|
|
||||||
|
**MANDATORY — File Deliverables:** For every test class you generate, you MUST create BOTH files:
|
||||||
|
1. `{ClassName}Test.cls` — the test class source
|
||||||
|
2. `{ClassName}Test.cls-meta.xml` — the accompanying metadata file (use the meta XML template below)
|
||||||
|
|
||||||
|
Never create the `.cls` without its `.cls-meta.xml`. Both files are required for deployment.
|
||||||
|
|
||||||
|
Meta XML template for test classes:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<apiVersion>66.0</apiVersion>
|
||||||
|
<status>Active</status>
|
||||||
|
</ApexClass>
|
||||||
|
```
|
||||||
|
|
||||||
#### Test Class Structure
|
#### Test Class Structure
|
||||||
|
|
||||||
```apex
|
```apex
|
||||||
@ -88,12 +104,17 @@ private class MyServiceTest {
|
|||||||
|
|
||||||
#### Naming Convention
|
#### Naming Convention
|
||||||
|
|
||||||
Use descriptive method names: `test[SubjectOrAction]_[Scenario]_[ExpectedResult]`
|
Use descriptive method names following one of these patterns:
|
||||||
|
|
||||||
- `testAccountUpdate_ChangeName_Success`
|
- `[SubjectOrAction]_[Scenario]_[ExpectedResult]`: `AccountUpdate_ChangeName_Success`
|
||||||
- `testEmailValidation_InvalidFormat_ThrowsException`
|
- `should[ExpectedResult]_When[Scenario]`: `shouldSendNotification_WhenOpportunityClosedWon`
|
||||||
- `testOpportunity_ClosedWon_SendsNotification`
|
|
||||||
- `testBatchExecution_RunningAsBatch_TriggerBypassed`
|
Examples:
|
||||||
|
|
||||||
|
- `AccountUpdate_ChangeName_Success`
|
||||||
|
- `EmailValidation_InvalidFormat_ThrowsException`
|
||||||
|
- `shouldSendNotification_WhenOpportunityClosedWon`
|
||||||
|
- `BatchExecution_RunningAsBatch_TriggerBypassed`
|
||||||
|
|
||||||
### Step 3 — Run the Smallest Useful Test Set
|
### Step 3 — Run the Smallest Useful Test Set
|
||||||
|
|
||||||
@ -144,6 +165,13 @@ Cover all paths: positive, negative/exception, bulk (251+ records), callout/asyn
|
|||||||
| Selector | Query results for valid/null/empty inputs, bulk (251+), field population, sort order verification, `WITH USER_MODE` enforcement via restricted-permission user (`System.runAs`) |
|
| Selector | Query results for valid/null/empty inputs, bulk (251+), field population, sort order verification, `WITH USER_MODE` enforcement via restricted-permission user (`System.runAs`) |
|
||||||
| Scheduled | Execution, CRON validation |
|
| Scheduled | Execution, CRON validation |
|
||||||
|
|
||||||
|
## Output Expectations
|
||||||
|
|
||||||
|
Deliverables per test class:
|
||||||
|
- `{ClassName}Test.cls`
|
||||||
|
- `{ClassName}Test.cls-meta.xml` (MANDATORY — match the API version of the class under test; default `66.0`)
|
||||||
|
- `TestDataFactory.cls` + `TestDataFactory.cls-meta.xml` (if not already present in the project)
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
When reporting test results, use this structure:
|
When reporting test results, use this structure:
|
||||||
|
|||||||
@ -26,7 +26,7 @@ private class {{ClassName}}Test {
|
|||||||
* Tests successful {{methodUnderTest}} with valid input
|
* Tests successful {{methodUnderTest}} with valid input
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void test{{MethodUnderTest}}_ValidInput_Success() {
|
static void {{MethodUnderTest}}_ValidInput_Success() {
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
// GIVEN - Set up test conditions
|
// GIVEN - Set up test conditions
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
@ -56,7 +56,7 @@ private class {{ClassName}}Test {
|
|||||||
* Tests {{methodUnderTest}} throws exception with null input
|
* Tests {{methodUnderTest}} throws exception with null input
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void test{{MethodUnderTest}}_NullInput_ThrowsException() {
|
static void {{MethodUnderTest}}_NullInput_ThrowsException() {
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
// GIVEN - Null input
|
// GIVEN - Null input
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
@ -83,7 +83,7 @@ private class {{ClassName}}Test {
|
|||||||
* Tests {{methodUnderTest}} handles invalid data gracefully
|
* Tests {{methodUnderTest}} handles invalid data gracefully
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void test{{MethodUnderTest}}_InvalidData_ReturnsError() {
|
static void {{MethodUnderTest}}_InvalidData_ReturnsError() {
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
// GIVEN - Invalid input data
|
// GIVEN - Invalid input data
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
@ -113,7 +113,7 @@ private class {{ClassName}}Test {
|
|||||||
* 251 records crosses the 200-record trigger batch boundary
|
* 251 records crosses the 200-record trigger batch boundary
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void test{{MethodUnderTest}}_BulkOperation_Success() {
|
static void {{MethodUnderTest}}_BulkOperation_Success() {
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
// GIVEN - 251 records (crosses 200-record batch boundary)
|
// GIVEN - 251 records (crosses 200-record batch boundary)
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
@ -147,7 +147,7 @@ private class {{ClassName}}Test {
|
|||||||
* Tests {{methodUnderTest}} with empty list input
|
* Tests {{methodUnderTest}} with empty list input
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void test{{MethodUnderTest}}_EmptyList_NoError() {
|
static void {{MethodUnderTest}}_EmptyList_NoError() {
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
// GIVEN - Empty list
|
// GIVEN - Empty list
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -20,7 +20,7 @@ private class {{ClassName}}BulkTest {
|
|||||||
* Verifies trigger handles multiple batch chunks correctly
|
* Verifies trigger handles multiple batch chunks correctly
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testBulkInsert_251Records_AllProcessed() {
|
static void BulkInsert_251Records_AllProcessed() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
List<{{ObjectName}}> records = TestDataFactory.create{{ObjectName}}s(BULK_SIZE);
|
List<{{ObjectName}}> records = TestDataFactory.create{{ObjectName}}s(BULK_SIZE);
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ private class {{ClassName}}BulkTest {
|
|||||||
* Tests bulk insert at exact batch boundary (200 records)
|
* Tests bulk insert at exact batch boundary (200 records)
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testBulkInsert_ExactBatchSize_AllProcessed() {
|
static void BulkInsert_ExactBatchSize_AllProcessed() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
List<{{ObjectName}}> records = TestDataFactory.create{{ObjectName}}s(SINGLE_BATCH);
|
List<{{ObjectName}}> records = TestDataFactory.create{{ObjectName}}s(SINGLE_BATCH);
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ private class {{ClassName}}BulkTest {
|
|||||||
* Verifies update triggers handle multiple batches
|
* Verifies update triggers handle multiple batches
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testBulkUpdate_251Records_AllUpdated() {
|
static void BulkUpdate_251Records_AllUpdated() {
|
||||||
// GIVEN - Insert records first
|
// GIVEN - Insert records first
|
||||||
List<{{ObjectName}}> records = TestDataFactory.create{{ObjectName}}s(BULK_SIZE);
|
List<{{ObjectName}}> records = TestDataFactory.create{{ObjectName}}s(BULK_SIZE);
|
||||||
insert records;
|
insert records;
|
||||||
@ -116,7 +116,7 @@ private class {{ClassName}}BulkTest {
|
|||||||
* Verifies delete triggers handle cleanup correctly
|
* Verifies delete triggers handle cleanup correctly
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testBulkDelete_251Records_AllDeleted() {
|
static void BulkDelete_251Records_AllDeleted() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
List<{{ObjectName}}> records = TestDataFactory.create{{ObjectName}}s(BULK_SIZE);
|
List<{{ObjectName}}> records = TestDataFactory.create{{ObjectName}}s(BULK_SIZE);
|
||||||
insert records;
|
insert records;
|
||||||
@ -150,7 +150,7 @@ private class {{ClassName}}BulkTest {
|
|||||||
* Verifies partial success scenarios are handled
|
* Verifies partial success scenarios are handled
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testBulkMixedOperations_PartialSuccess() {
|
static void BulkMixedOperations_PartialSuccess() {
|
||||||
// GIVEN - Some valid, some invalid records
|
// GIVEN - Some valid, some invalid records
|
||||||
List<{{ObjectName}}> validRecords = TestDataFactory.create{{ObjectName}}s(200);
|
List<{{ObjectName}}> validRecords = TestDataFactory.create{{ObjectName}}s(200);
|
||||||
List<{{ObjectName}}> invalidRecords = new List<{{ObjectName}}>();
|
List<{{ObjectName}}> invalidRecords = new List<{{ObjectName}}>();
|
||||||
@ -199,23 +199,23 @@ private class {{ClassName}}BulkTest {
|
|||||||
private static void assertGovernorLimitsNotExceeded() {
|
private static void assertGovernorLimitsNotExceeded() {
|
||||||
// SOQL Queries (limit: 100)
|
// SOQL Queries (limit: 100)
|
||||||
Assert.isTrue(Limits.getQueries() < 90,
|
Assert.isTrue(Limits.getQueries() < 90,
|
||||||
'SOQL queries approaching limit: ' + Limits.getQueries() + '/100');
|
'SOQL queries approaching limit: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());
|
||||||
|
|
||||||
// DML Statements (limit: 150)
|
// DML Statements (limit: 150)
|
||||||
Assert.isTrue(Limits.getDmlStatements() < 140,
|
Assert.isTrue(Limits.getDmlStatements() < 140,
|
||||||
'DML statements approaching limit: ' + Limits.getDmlStatements() + '/150');
|
'DML statements approaching limit: ' + Limits.getDmlStatements() + '/' + Limits.getLimitDMLStatements());
|
||||||
|
|
||||||
// DML Rows (limit: 10,000)
|
// DML Rows (limit: 10,000)
|
||||||
Assert.isTrue(Limits.getDmlRows() < 9500,
|
Assert.isTrue(Limits.getDmlRows() < 9500,
|
||||||
'DML rows approaching limit: ' + Limits.getDmlRows() + '/10000');
|
'DML rows approaching limit: ' + Limits.getDmlRows() + '/' + Limits.getLimitDMLRows());
|
||||||
|
|
||||||
// Heap Size (limit: 6MB sync, 12MB async)
|
// Heap Size (limit: 6MB sync, 12MB async)
|
||||||
Assert.isTrue(Limits.getHeapSize() < 5000000,
|
Assert.isTrue(Limits.getHeapSize() < 5000000,
|
||||||
'Heap size approaching limit: ' + Limits.getHeapSize() + '/6000000');
|
'Heap size approaching limit: ' + Limits.getHeapSize() + '/' + Limits.getLimitHeapSize());
|
||||||
|
|
||||||
// CPU Time (limit: 10,000ms sync, 60,000ms async)
|
// CPU Time (limit: 10,000ms sync, 60,000ms async)
|
||||||
Assert.isTrue(Limits.getCpuTime() < 9000,
|
Assert.isTrue(Limits.getCpuTime() < 9000,
|
||||||
'CPU time approaching limit: ' + Limits.getCpuTime() + '/10000');
|
'CPU time approaching limit: ' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime());
|
||||||
|
|
||||||
System.debug('═══════════════════════════════════════════════════════');
|
System.debug('═══════════════════════════════════════════════════════');
|
||||||
System.debug('GOVERNOR LIMIT USAGE:');
|
System.debug('GOVERNOR LIMIT USAGE:');
|
||||||
@ -236,7 +236,7 @@ private class {{ClassName}}BulkTest {
|
|||||||
* Use sparingly - consumes significant test execution time
|
* Use sparingly - consumes significant test execution time
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testStressTest_501Records_AllProcessed() {
|
static void StressTest_501Records_AllProcessed() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
List<{{ObjectName}}> records = TestDataFactory.create{{ObjectName}}s(LARGE_BULK_SIZE);
|
List<{{ObjectName}}> records = TestDataFactory.create{{ObjectName}}s(LARGE_BULK_SIZE);
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,6 @@
|
|||||||
*
|
*
|
||||||
* Enables true unit testing by replacing database operations with in-memory tracking.
|
* Enables true unit testing by replacing database operations with in-memory tracking.
|
||||||
* Tests using this pattern run ~35x faster than tests with actual DML.
|
* Tests using this pattern run ~35x faster than tests with actual DML.
|
||||||
*
|
|
||||||
* @see https://www.jamessimone.net/blog/joys-of-apex/mocking-dml/
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@ -298,7 +296,7 @@ public class AccountService {
|
|||||||
private class AccountServiceTest {
|
private class AccountServiceTest {
|
||||||
|
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testCreateAccount_Success() {
|
static void CreateAccount_ValidInput_Success() {
|
||||||
// Arrange
|
// Arrange
|
||||||
DMLMock.reset();
|
DMLMock.reset();
|
||||||
AccountService service = new AccountService(new DMLMock());
|
AccountService service = new AccountService(new DMLMock());
|
||||||
@ -320,7 +318,7 @@ private class AccountServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testCreateAccount_NullInput_ThrowsException() {
|
static void CreateAccount_NullInput_ThrowsException() {
|
||||||
// Arrange
|
// Arrange
|
||||||
DMLMock.reset();
|
DMLMock.reset();
|
||||||
AccountService service = new AccountService(new DMLMock());
|
AccountService service = new AccountService(new DMLMock());
|
||||||
|
|||||||
@ -98,7 +98,7 @@ private class {{ClassName}}CalloutTest {
|
|||||||
* Tests successful API call returns expected data
|
* Tests successful API call returns expected data
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testCallout_Success_ReturnsData() {
|
static void Callout_Success_ReturnsData() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
Test.setMock(HttpCalloutMock.class, new SuccessMock());
|
Test.setMock(HttpCalloutMock.class, new SuccessMock());
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ private class {{ClassName}}CalloutTest {
|
|||||||
* Tests POST request with body
|
* Tests POST request with body
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testCallout_PostWithBody_Success() {
|
static void Callout_PostWithBody_Success() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
Test.setMock(HttpCalloutMock.class, new SuccessMock());
|
Test.setMock(HttpCalloutMock.class, new SuccessMock());
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ private class {{ClassName}}CalloutTest {
|
|||||||
* Tests handling of 400 Bad Request
|
* Tests handling of 400 Bad Request
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testCallout_BadRequest_HandlesGracefully() {
|
static void Callout_BadRequest_HandlesGracefully() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
Test.setMock(HttpCalloutMock.class, new ErrorMock(400, 'Invalid request parameters'));
|
Test.setMock(HttpCalloutMock.class, new ErrorMock(400, 'Invalid request parameters'));
|
||||||
|
|
||||||
@ -179,7 +179,7 @@ private class {{ClassName}}CalloutTest {
|
|||||||
* Tests handling of 500 Internal Server Error
|
* Tests handling of 500 Internal Server Error
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testCallout_ServerError_HandlesGracefully() {
|
static void Callout_ServerError_HandlesGracefully() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
Test.setMock(HttpCalloutMock.class, new ErrorMock(500, 'Internal server error'));
|
Test.setMock(HttpCalloutMock.class, new ErrorMock(500, 'Internal server error'));
|
||||||
|
|
||||||
@ -199,7 +199,7 @@ private class {{ClassName}}CalloutTest {
|
|||||||
* Tests handling of timeout exception
|
* Tests handling of timeout exception
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testCallout_Timeout_ThrowsException() {
|
static void Callout_Timeout_ThrowsException() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
Test.setMock(HttpCalloutMock.class, new TimeoutMock());
|
Test.setMock(HttpCalloutMock.class, new TimeoutMock());
|
||||||
|
|
||||||
@ -222,7 +222,7 @@ private class {{ClassName}}CalloutTest {
|
|||||||
* Tests handling of 401 Unauthorized
|
* Tests handling of 401 Unauthorized
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testCallout_Unauthorized_HandlesGracefully() {
|
static void Callout_Unauthorized_HandlesGracefully() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
Test.setMock(HttpCalloutMock.class, new ErrorMock(401, 'Invalid or expired token'));
|
Test.setMock(HttpCalloutMock.class, new ErrorMock(401, 'Invalid or expired token'));
|
||||||
|
|
||||||
@ -246,7 +246,7 @@ private class {{ClassName}}CalloutTest {
|
|||||||
* Verifies correct endpoint and method are used
|
* Verifies correct endpoint and method are used
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testCallout_ValidatesRequestParameters() {
|
static void Callout_ValidatesRequestParameters() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
Test.setMock(HttpCalloutMock.class, new ValidatingMock('/api/v1/resource', 'POST'));
|
Test.setMock(HttpCalloutMock.class, new ValidatingMock('/api/v1/resource', 'POST'));
|
||||||
|
|
||||||
@ -272,7 +272,7 @@ private class {{ClassName}}CalloutTest {
|
|||||||
* Note: Must use Test.startTest/stopTest to execute @future
|
* Note: Must use Test.startTest/stopTest to execute @future
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testFutureCallout_ExecutesSuccessfully() {
|
static void FutureCallout_ExecutesSuccessfully() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
Test.setMock(HttpCalloutMock.class, new SuccessMock());
|
Test.setMock(HttpCalloutMock.class, new SuccessMock());
|
||||||
|
|
||||||
@ -290,7 +290,7 @@ private class {{ClassName}}CalloutTest {
|
|||||||
* Tests Queueable with callout
|
* Tests Queueable with callout
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testQueueableCallout_ExecutesSuccessfully() {
|
static void QueueableCallout_ExecutesSuccessfully() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
Test.setMock(HttpCalloutMock.class, new SuccessMock());
|
Test.setMock(HttpCalloutMock.class, new SuccessMock());
|
||||||
|
|
||||||
@ -327,7 +327,7 @@ private class {{ClassName}}CalloutTest {
|
|||||||
* Tests multiple callouts in sequence (limit: 100 per transaction)
|
* Tests multiple callouts in sequence (limit: 100 per transaction)
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testMultipleCallouts_AllSucceed() {
|
static void MultipleCallouts_AllSucceed() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
Test.setMock(HttpCalloutMock.class, new MultiCalloutMock());
|
Test.setMock(HttpCalloutMock.class, new MultiCalloutMock());
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,6 @@
|
|||||||
*
|
*
|
||||||
* The Stub API enables dynamic mocking of interfaces and virtual classes.
|
* The Stub API enables dynamic mocking of interfaces and virtual classes.
|
||||||
* Use when you need conditional logic or method tracking in your mocks.
|
* Use when you need conditional logic or method tracking in your mocks.
|
||||||
*
|
|
||||||
* @see https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_interface_System_StubProvider.htm
|
|
||||||
* @see https://blog.beyondthecloud.dev/blog/salesforce-mock-in-apex-tests
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@ -115,7 +112,7 @@ public class AccountServiceStub implements System.StubProvider {
|
|||||||
private class AccountServiceStubTest {
|
private class AccountServiceStubTest {
|
||||||
|
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testWithConfiguredResponse() {
|
static void ConfiguredResponse_ReturnsExpectedValues() {
|
||||||
// Arrange - Configure stub responses
|
// Arrange - Configure stub responses
|
||||||
AccountServiceStub stub = new AccountServiceStub()
|
AccountServiceStub stub = new AccountServiceStub()
|
||||||
.whenCalled('getAccountCount', 42)
|
.whenCalled('getAccountCount', 42)
|
||||||
@ -144,7 +141,7 @@ private class AccountServiceStubTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testDefaultResponses() {
|
static void DefaultResponse_ReturnsDefaults() {
|
||||||
// Arrange - No configured responses
|
// Arrange - No configured responses
|
||||||
AccountServiceStub stub = new AccountServiceStub();
|
AccountServiceStub stub = new AccountServiceStub();
|
||||||
IAccountService service = (IAccountService) Test.createStub(
|
IAccountService service = (IAccountService) Test.createStub(
|
||||||
@ -241,7 +238,7 @@ public class ErrorStub implements System.StubProvider {
|
|||||||
private class ErrorStubTest {
|
private class ErrorStubTest {
|
||||||
|
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testErrorHandling() {
|
static void ErrorHandling_ThrowsConfiguredException() {
|
||||||
// Arrange
|
// Arrange
|
||||||
ErrorStub stub = new ErrorStub().failOn('getAccount', 'Account not found');
|
ErrorStub stub = new ErrorStub().failOn('getAccount', 'Account not found');
|
||||||
IAccountService service = (IAccountService) Test.createStub(
|
IAccountService service = (IAccountService) Test.createStub(
|
||||||
@ -265,8 +262,6 @@ private class ErrorStubTest {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Highly flexible stub that can mock any interface
|
* Highly flexible stub that can mock any interface
|
||||||
*
|
|
||||||
* Inspired by: Suraj Pillai's UniversalMock pattern
|
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
public class UniversalMock implements System.StubProvider {
|
public class UniversalMock implements System.StubProvider {
|
||||||
|
|||||||
@ -37,8 +37,8 @@ sf apex run test \
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sf apex run test \
|
sf apex run test \
|
||||||
--tests AccountServiceTest.testCreate \
|
--tests AccountServiceTest.CreateAccount_Success \
|
||||||
--tests AccountServiceTest.testUpdate \
|
--tests AccountServiceTest.UpdateAccount_Success \
|
||||||
--target-org my-sandbox
|
--target-org my-sandbox
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -213,7 +213,7 @@ sf apex run test \
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sf apex run test \
|
sf apex run test \
|
||||||
--tests AccountServiceTest.testCreate \
|
--tests AccountServiceTest.CreateAccount_Success \
|
||||||
--target-org my-sandbox
|
--target-org my-sandbox
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
This guide covers mocking and stubbing patterns that enable true unit testing in Apex. By replacing database operations and external services with mock implementations, you can write fast, isolated, reliable tests.
|
This guide covers mocking and stubbing patterns that enable true unit testing in Apex. By replacing database operations and external services with mock implementations, you can write fast, isolated, reliable tests.
|
||||||
|
|
||||||
> **Sources**: [Beyond the Cloud](https://blog.beyondthecloud.dev/blog/salesforce-mock-in-apex-tests), [James Simone](https://www.jamessimone.net/blog/joys-of-apex/mocking-dml/), [Trailhead](https://trailhead.salesforce.com/content/learn/modules/unit-testing-on-the-lightning-platform/mock-stub-objects)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Mocking vs Stubbing
|
## Mocking vs Stubbing
|
||||||
@ -28,8 +26,6 @@ Salesforce **requires** mock callouts - you cannot make real HTTP requests in te
|
|||||||
```apex
|
```apex
|
||||||
/**
|
/**
|
||||||
* Simple HTTP mock returning a fixed response
|
* Simple HTTP mock returning a fixed response
|
||||||
*
|
|
||||||
* @see https://www.apexhours.com/testing-web-services-callouts-in-salesforce/
|
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
public class MockHttpResponse implements HttpCalloutMock {
|
public class MockHttpResponse implements HttpCalloutMock {
|
||||||
@ -53,7 +49,7 @@ public class MockHttpResponse implements HttpCalloutMock {
|
|||||||
|
|
||||||
// Usage in test
|
// Usage in test
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testApiCall_Success() {
|
static void shouldReturnData_WhenApiCallSucceeds() {
|
||||||
String mockBody = '{"success": true, "data": {"id": "12345"}}';
|
String mockBody = '{"success": true, "data": {"id": "12345"}}';
|
||||||
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(200, mockBody));
|
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(200, mockBody));
|
||||||
|
|
||||||
@ -102,7 +98,7 @@ public class MultiEndpointMock implements HttpCalloutMock {
|
|||||||
* Mock for testing error handling
|
* Mock for testing error handling
|
||||||
*/
|
*/
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testApiCall_ServerError_HandlesGracefully() {
|
static void shouldHandleGracefully_WhenServerReturnsError() {
|
||||||
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(500, '{"error": "Server Error"}'));
|
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(500, '{"error": "Server Error"}'));
|
||||||
|
|
||||||
Test.startTest();
|
Test.startTest();
|
||||||
@ -127,8 +123,6 @@ This pattern eliminates database operations from tests, achieving 35x faster exe
|
|||||||
```apex
|
```apex
|
||||||
/**
|
/**
|
||||||
* Interface for DML operations - enables mocking
|
* Interface for DML operations - enables mocking
|
||||||
*
|
|
||||||
* @see https://www.jamessimone.net/blog/joys-of-apex/mocking-dml/
|
|
||||||
*/
|
*/
|
||||||
public interface IDML {
|
public interface IDML {
|
||||||
void doInsert(SObject record);
|
void doInsert(SObject record);
|
||||||
@ -307,7 +301,7 @@ public class AccountService {
|
|||||||
|
|
||||||
// Test using mock DML
|
// Test using mock DML
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testCreateAccount_NoDatabase() {
|
static void shouldCreateAccount_WithMockDML() {
|
||||||
// Arrange
|
// Arrange
|
||||||
DMLMock.reset();
|
DMLMock.reset();
|
||||||
AccountService service = new AccountService(new DMLMock());
|
AccountService service = new AccountService(new DMLMock());
|
||||||
@ -393,7 +387,7 @@ public class AccountServiceStub implements System.StubProvider {
|
|||||||
```apex
|
```apex
|
||||||
// Create stub with Test.createStub()
|
// Create stub with Test.createStub()
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testWithStub() {
|
static void shouldReturnConfiguredValues_WhenUsingStub() {
|
||||||
// Create the stub
|
// Create the stub
|
||||||
AccountServiceStub stub = new AccountServiceStub()
|
AccountServiceStub stub = new AccountServiceStub()
|
||||||
.withMethodReturn('getAccountCount', 42);
|
.withMethodReturn('getAccountCount', 42);
|
||||||
@ -450,7 +444,7 @@ public class AccountSelector {
|
|||||||
|
|
||||||
// Usage in test
|
// Usage in test
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testWithMockedQuery() {
|
static void shouldReturnMockData_WhenQueryMocked() {
|
||||||
// Arrange - no database insert needed!
|
// Arrange - no database insert needed!
|
||||||
List<Account> mockAccounts = new List<Account>{
|
List<Account> mockAccounts = new List<Account>{
|
||||||
new Account(Name = 'Mock 1', Industry = 'Tech'),
|
new Account(Name = 'Mock 1', Industry = 'Tech'),
|
||||||
|
|||||||
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
Fast tests enable faster development. When test suites run quickly, developers refactor confidently. This guide covers techniques to dramatically reduce test execution time.
|
Fast tests enable faster development. When test suites run quickly, developers refactor confidently. This guide covers techniques to dramatically reduce test execution time.
|
||||||
|
|
||||||
> **Source**: [James Simone - Writing Performant Apex Tests](https://www.jamessimone.net/blog/joys-of-apex/writing-performant-apex-tests/)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Why Test Speed Matters
|
## Why Test Speed Matters
|
||||||
@ -172,7 +170,7 @@ List<List<SObject>> results = [FIND 'test' IN ALL FIELDS RETURNING Account];
|
|||||||
|
|
||||||
// ✅ SOLUTION: Use Test.setFixedSearchResults()
|
// ✅ SOLUTION: Use Test.setFixedSearchResults()
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testSearch() {
|
static void Search_WithFixedResults_FindsRecord() {
|
||||||
Account acc = new Account(Name = 'Searchable');
|
Account acc = new Account(Name = 'Searchable');
|
||||||
insert acc;
|
insert acc;
|
||||||
|
|
||||||
@ -201,7 +199,7 @@ sf apex run test --test-level RunLocalTests --target-org sandbox
|
|||||||
sf apex run test --class-names MyClassTest --target-org sandbox
|
sf apex run test --class-names MyClassTest --target-org sandbox
|
||||||
|
|
||||||
# ✅ FASTER: Run single test method
|
# ✅ FASTER: Run single test method
|
||||||
sf apex run test --tests MyClassTest.testSpecificMethod --target-org sandbox
|
sf apex run test --tests MyClassTest.ProcessRecords_Success --target-org sandbox
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -251,7 +249,7 @@ Before committing tests, verify:
|
|||||||
|
|
||||||
```apex
|
```apex
|
||||||
@IsTest
|
@IsTest
|
||||||
static void testPerformance() {
|
static void shouldCompleteInUnderOneSecond() {
|
||||||
Long startTime = System.currentTimeMillis();
|
Long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// Your test code here
|
// Your test code here
|
||||||
|
|||||||
@ -19,13 +19,13 @@ User: "Run tests for AccountService with auto-fix enabled"
|
|||||||
|
|
||||||
Claude:
|
Claude:
|
||||||
1. sf apex run test --class-names AccountServiceTest --code-coverage --result-format json
|
1. sf apex run test --class-names AccountServiceTest --code-coverage --result-format json
|
||||||
2. Parse results: 1 failure - testBulkInsert line 45 NullPointerException
|
2. Parse results: 1 failure - BulkInsert_Success line 45 NullPointerException
|
||||||
3. Read AccountServiceTest.cls (line 45 context)
|
3. Read AccountServiceTest.cls (line 45 context)
|
||||||
4. Read AccountService.cls (trace the null reference)
|
4. Read AccountService.cls (trace the null reference)
|
||||||
5. Identify: Missing null check in AccountService.processAccounts()
|
5. Identify: Missing null check in AccountService.processAccounts()
|
||||||
6. generating-apex: Add null safety to AccountService.processAccounts()
|
6. generating-apex: Add null safety to AccountService.processAccounts()
|
||||||
7. Deploy fix
|
7. Deploy fix
|
||||||
8. Re-run: sf apex run test --tests AccountServiceTest.testBulkInsert
|
8. Re-run: sf apex run test --tests AccountServiceTest.BulkInsert_Success
|
||||||
9. ✅ Passing! Report success.
|
9. ✅ Passing! Report success.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,6 @@ All steps in this workflow are MANDATORY and must be executed in order. Execute
|
|||||||
|
|
||||||
**CRITICAL -- WORKFLOW INTEGRITY RULES:**
|
**CRITICAL -- WORKFLOW INTEGRITY RULES:**
|
||||||
- NEVER remove, rename, or consolidate checklist items from your task progress. Every step listed below must appear in task_progress from start to finish.
|
- NEVER remove, rename, or consolidate checklist items from your task progress. Every step listed below must appear in task_progress from start to finish.
|
||||||
- NEVER replace specific step names (e.g., "Run static analysis", "Execute tests") with vague alternatives (e.g., "Complete workflow", "Finalize").
|
|
||||||
- The task is NOT complete after writing files. Writing `.cls` and `.cls-meta.xml` files is the MIDPOINT of this workflow, not the end. Steps 6 and 7 are mandatory tool invocations that MUST execute before completion.
|
- The task is NOT complete after writing files. Writing `.cls` and `.cls-meta.xml` files is the MIDPOINT of this workflow, not the end. Steps 6 and 7 are mandatory tool invocations that MUST execute before completion.
|
||||||
- NEVER call `attempt_completion` or present a final summary until Steps 6, 7, and 8 are all executed and documented with their actual tool outputs.
|
- NEVER call `attempt_completion` or present a final summary until Steps 6, 7, and 8 are all executed and documented with their actual tool outputs.
|
||||||
|
|
||||||
@ -63,7 +62,7 @@ All steps in this workflow are MANDATORY and must be executed in order. Execute
|
|||||||
- Generate `{ClassName}.cls` with ApexDoc
|
- Generate `{ClassName}.cls` with ApexDoc
|
||||||
- Generate `{ClassName}.cls-meta.xml`
|
- Generate `{ClassName}.cls-meta.xml`
|
||||||
|
|
||||||
5. [MANDATORY] **Generate test classes** -- Use the skill `generating-apex-test` and follow its complete workflow to generate `{ClassName}Test.cls` and `{ClassName}Test.cls-meta.xml`. You MUST use that skill now and execute its instructions before proceeding to Step 6. After the test skill workflow completes, return here and continue with Step 6 -- do NOT end the task.
|
5. [MANDATORY] **Generate test classes** -- STOP: Do not write any apex test code in this skill. Immediately activate `generating-apex-test` skill and follow its complete workflow to generate `{ClassName}Test.cls` and `{ClassName}Test.cls-meta.xml`. Only return here after that skill reports completion; if activation is unavailable, abort this step and record `test_skill=unavailable: <reason>` in Step 8.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -73,18 +72,18 @@ You have written files. You are NOT done. The two steps below require MCP tool i
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
6. [MANDATORY] **Static analysis -- DO NOT SKIP -- REQUIRES TOOL INVOCATION**
|
6. [MANDATORY] **run_code_analyzer MCP tool -- DO NOT SKIP -- REQUIRES TOOL INVOCATION**
|
||||||
- You MUST call the MCP tool `run_code_analyzer` RIGHT NOW on the newly generated/updated `.cls` files.
|
- You MUST call the MCP tool `run_code_analyzer` RIGHT NOW to performe static analysis against code on the newly generated/updated `.cls` files.
|
||||||
- This is a tool invocation, not a file write. Invoke `run_code_analyzer` as an MCP tool call.
|
- This is a tool invocation, not a file write. Invoke `run_code_analyzer` as an MCP tool call.
|
||||||
- Remediate all violations with severity levels `sev0`, `sev1`, and `sev2`.
|
- Remediate all violations with severity levels `sev0`, `sev1`, and `sev2`.
|
||||||
- Re-run `run_code_analyzer` until no `sev0-sev2` issues remain.
|
- Re-run `run_code_analyzer` until no `sev0-sev2` issues remain.
|
||||||
- Record the final `run_code_analyzer` output (clean or with remaining sev3+ only) -- you will need it for the report in Step 8.
|
- Record the final `run_code_analyzer` output (clean or with remaining sev3+ only) -- you will need it for the report in Step 8.
|
||||||
- If the MCP tool is unavailable after a real invocation attempt, record `run_code_analyzer=unavailable` with the error and state this in the report. Do NOT silently skip.
|
- If the MCP tool is unavailable after a real invocation attempt cli command `sf code-analyzer run --target <target>`, if that also doesnt work record `run_code_analyzer=unavailable` with the error and state this in the report. Do NOT silently skip.
|
||||||
|
|
||||||
7. [MANDATORY] **Execute Apex tests -- DO NOT SKIP -- REQUIRES TOOL INVOCATION**
|
7. [MANDATORY] **Execute Apex tests -- DO NOT SKIP -- REQUIRES TOOL INVOCATION**
|
||||||
- You MUST run the org's Apex test suite RIGHT NOW, including `{ClassName}Test`.
|
- You MUST run the org's Apex test suite RIGHT NOW, including `{ClassName}Test`.
|
||||||
- Execute tests using the `sf apex run test` CLI command or the appropriate MCP tool.
|
- Execute tests using the `sf apex run test` CLI command or the appropriate MCP tool.
|
||||||
- All test authoring, failure remediation, and coverage improvements MUST be performed by reading and following `skills/generating-apex-test/SKILL.md`; iterate until green.
|
- All test authoring, failure remediation, and coverage improvements MUST be performed by reading and following `generating-apex-test` skill; iterate until green.
|
||||||
- Record the test execution results (pass/fail counts, coverage percentage) -- you will need them for the report in Step 8.
|
- Record the test execution results (pass/fail counts, coverage percentage) -- you will need them for the report in Step 8.
|
||||||
- If test execution is unavailable (no org connected, no CLI access), record `test_execution=unavailable` with the error and state this in the report. Do NOT silently skip.
|
- If test execution is unavailable (no org connected, no CLI access), record `test_execution=unavailable` with the error and state this in the report. Do NOT silently skip.
|
||||||
|
|
||||||
@ -94,7 +93,7 @@ You have written files. You are NOT done. The two steps below require MCP tool i
|
|||||||
- NEVER omit the Analyzer or Testing lines. NEVER write "N/A" without having attempted the tool invocation first.
|
- NEVER omit the Analyzer or Testing lines. NEVER write "N/A" without having attempted the tool invocation first.
|
||||||
|
|
||||||
9. [MANDATORY] **Enforce cross-skill boundaries**
|
9. [MANDATORY] **Enforce cross-skill boundaries**
|
||||||
- Test class creation, updates, refactors, data setup, and advanced fixtures: ALWAYS delegate by reading and following `skills/generating-apex-test/SKILL.md`
|
- Test class creation, updates, refactors, data setup, and advanced fixtures: ALWAYS delegate by reading and following `generating-apex-test` skill
|
||||||
- NEVER write test code directly in this skill; all test work flows through the test skill
|
- NEVER write test code directly in this skill; all test work flows through the test skill
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -339,8 +338,8 @@ Prefer current language features:
|
|||||||
Deliverables per class:
|
Deliverables per class:
|
||||||
- `{ClassName}.cls`
|
- `{ClassName}.cls`
|
||||||
- `{ClassName}.cls-meta.xml` (default API version `66.0` or higher unless specified)
|
- `{ClassName}.cls-meta.xml` (default API version `66.0` or higher unless specified)
|
||||||
- `{ClassName}Test.cls` (generated via `skills/generating-apex-test/SKILL.md`)
|
- `{ClassName}Test.cls` (generated via `generating-apex-test` skill)
|
||||||
- `{ClassName}Test.cls-meta.xml` (generated via `skills/generating-apex-test/SKILL.md`)
|
- `{ClassName}Test.cls-meta.xml` (generated via `generating-apex-test` skill)
|
||||||
|
|
||||||
Meta XML template:
|
Meta XML template:
|
||||||
|
|
||||||
@ -373,7 +372,7 @@ Deploy: <dry-run or next step>
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Describe objects / fields first | metadata skill | Ensure alignment with the correct schema |
|
| Describe objects / fields first | metadata skill | Ensure alignment with the correct schema |
|
||||||
| Seed bulk or edge-case data | data skill | Create realistic datasets |
|
| Seed bulk or edge-case data | data skill | Create realistic datasets |
|
||||||
| Run Apex tests / fix failing tests | Read and follow `skills/generating-apex-test/SKILL.md` | Execute and iterate on failures |
|
| Run Apex tests / fix failing tests | Read and follow `generating-apex-test` skill | Execute and iterate on failures |
|
||||||
| Deploy to org | deploy skill | Validation and deployment orchestration |
|
| Deploy to org | deploy skill | Validation and deployment orchestration |
|
||||||
| Build Flow that calls Apex | Flow skill | Declarative orchestration via `@InvocableMethod` |
|
| Build Flow that calls Apex | Flow skill | Declarative orchestration via `@InvocableMethod` |
|
||||||
| Build LWC that calls Apex | LWC skill | UI/controller integration via `@AuraEnabled` |
|
| Build LWC that calls Apex | LWC skill | UI/controller integration via `@AuraEnabled` |
|
||||||
|
|||||||
@ -32,7 +32,7 @@ These patterns indicate poor code quality and should be refactored.
|
|||||||
| **No trigger bypass flag** | Can't disable for data loads | Add Custom Setting bypass |
|
| **No trigger bypass flag** | Can't disable for data loads | Add Custom Setting bypass |
|
||||||
| **`System.debug()` in main code paths** | Performance impact from concatenating variables, even when logs are disabled | Use logging framework with levels |
|
| **`System.debug()` in main code paths** | Performance impact from concatenating variables, even when logs are disabled | Use logging framework with levels |
|
||||||
| **Unnecessary `isEmpty()` before DML** | Wastes CPU | Remove - DML handles empty lists |
|
| **Unnecessary `isEmpty()` before DML** | Wastes CPU | Remove - DML handles empty lists |
|
||||||
| **`!= false` comparisons** | Confusing double negative | Use `== true` or just the boolean |
|
| **`!= false` comparisons** | Confusing double negative | Use `== true` or just the boolean if the value has been null checked |
|
||||||
| **God Class** | Single class does everything | Split into Service/Selector/Domain |
|
| **God Class** | Single class does everything | Split into Service/Selector/Domain |
|
||||||
| **Magic Numbers** | Hardcoded values like `if (score > 75)` | Use named constants |
|
| **Magic Numbers** | Hardcoded values like `if (score > 75)` | Use named constants |
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ These patterns indicate poor code quality and should be refactored.
|
|||||||
**❌ BAD:**
|
**❌ BAD:**
|
||||||
```apex
|
```apex
|
||||||
List<Account> accounts = [SELECT Id FROM Account];
|
List<Account> accounts = [SELECT Id FROM Account];
|
||||||
// Returns ALL accounts - could be millions!
|
// Returns ALL accounts - could exceed the Total number of records retrieved by SOQL queries limit
|
||||||
```
|
```
|
||||||
|
|
||||||
**✅ GOOD:**
|
**✅ GOOD:**
|
||||||
@ -148,7 +148,7 @@ if (acc.IsActive__c == true) {
|
|||||||
// Clear intent
|
// Clear intent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Or even better
|
// Or even better if acc.IsActive__c can't be null
|
||||||
if (acc.IsActive__c) {
|
if (acc.IsActive__c) {
|
||||||
// Most concise
|
// Most concise
|
||||||
}
|
}
|
||||||
@ -173,13 +173,13 @@ for (Account acc : accounts) {
|
|||||||
|
|
||||||
**✅ GOOD:**
|
**✅ GOOD:**
|
||||||
```apex
|
```apex
|
||||||
Map<Id, Account> accountsWithContacts = new Map<Id, Account>([
|
List<Account> accountsWithContacts = [
|
||||||
SELECT Id, (SELECT Id FROM Contacts)
|
SELECT Id, (SELECT Id FROM Contacts)
|
||||||
FROM Account
|
FROM Account
|
||||||
WHERE Id IN :accountIds
|
WHERE Id IN :accountIds
|
||||||
]);
|
];
|
||||||
|
|
||||||
for (Account acc : accountsWithContacts.values()) {
|
for (Account acc : accountsWithContacts) {
|
||||||
for (Contact con : acc.Contacts) {
|
for (Contact con : acc.Contacts) {
|
||||||
// No SOQL in loop
|
// No SOQL in loop
|
||||||
}
|
}
|
||||||
@ -204,19 +204,21 @@ public class AccountService {
|
|||||||
**✅ GOOD:**
|
**✅ GOOD:**
|
||||||
```apex
|
```apex
|
||||||
public class AccountService {
|
public class AccountService {
|
||||||
private List<Account> accounts;
|
|
||||||
|
private List<Account> accounts {
|
||||||
|
get {
|
||||||
|
// lazy load only when needed
|
||||||
|
if (accounts == null) {
|
||||||
|
accounts = [SELECT Id FROM Account LIMIT 200];
|
||||||
|
}
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
set { accounts = value; }
|
||||||
|
}
|
||||||
|
|
||||||
public AccountService(List<Account> accounts) {
|
public AccountService(List<Account> accounts) {
|
||||||
this.accounts = accounts; // Inject dependencies
|
this.accounts = accounts; // Inject dependencies
|
||||||
}
|
}
|
||||||
|
|
||||||
// Or lazy load only when needed
|
|
||||||
private List<Account> getAccounts() {
|
|
||||||
if (accounts == null) {
|
|
||||||
accounts = [SELECT Id FROM Account LIMIT 200];
|
|
||||||
}
|
|
||||||
return accounts;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -267,8 +269,6 @@ Set<Id> uniqueIds = new Set<Id>(allIds); // O(1) deduplication
|
|||||||
|
|
||||||
## Code Smell Catalog
|
## Code Smell Catalog
|
||||||
|
|
||||||
Based on "Clean Apex Code" by Pablo Gonzalez and clean code principles.
|
|
||||||
|
|
||||||
### Long Methods
|
### Long Methods
|
||||||
|
|
||||||
#### The Smell
|
#### The Smell
|
||||||
|
|||||||
@ -75,9 +75,6 @@ public class TriggerHelper {
|
|||||||
|
|
||||||
## 3. Guard Clauses & Fail-Fast
|
## 3. Guard Clauses & Fail-Fast
|
||||||
|
|
||||||
> 💡 *Principles inspired by "Clean Apex Code" by Pablo Gonzalez.
|
|
||||||
> [Purchase the book](https://link.springer.com/book/10.1007/979-8-8688-1411-2) for complete coverage.*
|
|
||||||
|
|
||||||
### The Problem
|
### The Problem
|
||||||
|
|
||||||
Deeply nested validation leads to hard-to-read code where business logic is buried.
|
Deeply nested validation leads to hard-to-read code where business logic is buried.
|
||||||
@ -162,9 +159,6 @@ public Database.LeadConvertResult convertLead(Id leadId, Id accountId) {
|
|||||||
|
|
||||||
## 4. Comment Best Practices
|
## 4. Comment Best Practices
|
||||||
|
|
||||||
> 💡 *Principles inspired by "Clean Apex Code" by Pablo Gonzalez.
|
|
||||||
> [Purchase the book](https://link.springer.com/book/10.1007/979-8-8688-1411-2) for complete coverage.*
|
|
||||||
|
|
||||||
### Core Principle
|
### Core Principle
|
||||||
|
|
||||||
Comments should explain **"why"**, not **"what"**. The code itself should communicate the "what".
|
Comments should explain **"why"**, not **"what"**. The code itself should communicate the "what".
|
||||||
@ -181,10 +175,6 @@ private static final Integer BULK_TEST_SIZE = 201;
|
|||||||
// Safe navigation (?.) doesn't work in formulas - must use IF(ISBLANK())
|
// Safe navigation (?.) doesn't work in formulas - must use IF(ISBLANK())
|
||||||
// See Known Issue W-12345678
|
// See Known Issue W-12345678
|
||||||
|
|
||||||
// GOOD: References external documentation
|
|
||||||
// Algorithm based on RFC 7519 (JSON Web Token specification)
|
|
||||||
// See: https://tools.ietf.org/html/rfc7519#section-4.1
|
|
||||||
|
|
||||||
// GOOD: Explains non-obvious optimization
|
// GOOD: Explains non-obvious optimization
|
||||||
// SOQL query in a loop replaced with map-based lookup — O(1) per record.
|
// SOQL query in a loop replaced with map-based lookup — O(1) per record.
|
||||||
Account acc = accountsByExternalId.get(record.ExternalId__c);
|
Account acc = accountsByExternalId.get(record.ExternalId__c);
|
||||||
|
|||||||
@ -896,9 +896,6 @@ if (result.success) {
|
|||||||
|
|
||||||
## Domain Class Pattern
|
## Domain Class Pattern
|
||||||
|
|
||||||
> 💡 *Principles inspired by "Clean Apex Code" by Pablo Gonzalez.
|
|
||||||
> [Purchase the book](https://link.springer.com/book/10.1007/979-8-8688-1411-2) for complete coverage.*
|
|
||||||
|
|
||||||
### Purpose
|
### Purpose
|
||||||
|
|
||||||
Encapsulate business rules in domain-specific classes, making code read like plain English and enabling reuse across the application.
|
Encapsulate business rules in domain-specific classes, making code read like plain English and enabling reuse across the application.
|
||||||
@ -991,9 +988,6 @@ public void processOpportunity(Opportunity opp, Account account) {
|
|||||||
|
|
||||||
## Abstraction Level Management
|
## Abstraction Level Management
|
||||||
|
|
||||||
> 💡 *Principles inspired by "Clean Apex Code" by Pablo Gonzalez.
|
|
||||||
> [Purchase the book](https://link.springer.com/book/10.1007/979-8-8688-1411-2) for complete coverage.*
|
|
||||||
|
|
||||||
### Purpose
|
### Purpose
|
||||||
|
|
||||||
Ensure each method operates at a consistent level of abstraction. Don't mix high-level orchestration with low-level implementation details.
|
Ensure each method operates at a consistent level of abstraction. Don't mix high-level orchestration with low-level implementation details.
|
||||||
|
|||||||
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
This guide documents systematic errors that LLMs (including Claude) commonly make when generating Salesforce Apex code. These patterns are critical to validate in generated code.
|
This guide documents systematic errors that LLMs (including Claude) commonly make when generating Salesforce Apex code. These patterns are critical to validate in generated code.
|
||||||
|
|
||||||
> **Source**: [LLM Mistakes in Apex & LWC - Salesforce Diaries](https://salesforcediaries.com/2026/01/16/llm-mistakes-in-apex-lwc-salesforce-code-generation-rules/)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
@ -703,4 +701,3 @@ def validate_apex(content):
|
|||||||
|
|
||||||
- **Existing Anti-Patterns**: See `references/anti-patterns.md` for traditional Apex anti-patterns
|
- **Existing Anti-Patterns**: See `references/anti-patterns.md` for traditional Apex anti-patterns
|
||||||
- **Best Practices**: See `references/best-practices.md` for correct patterns
|
- **Best Practices**: See `references/best-practices.md` for correct patterns
|
||||||
- **Source**: [Salesforce Diaries - LLM Mistakes](https://salesforcediaries.com/2026/01/16/llm-mistakes-in-apex-lwc-salesforce-code-generation-rules/)
|
|
||||||
|
|||||||
@ -10,8 +10,6 @@ The Trigger Actions Framework provides a metadata-driven approach to trigger man
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install from: https://github.com/mitchspano/trigger-actions-framework
|
|
||||||
|
|
||||||
## Basic Setup
|
## Basic Setup
|
||||||
|
|
||||||
### 1. Create the Trigger
|
### 1. Create the Trigger
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user