mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-07-30 11:43:26 +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.
|
||||
- Respond to feedback.
|
||||
- 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.
|
||||
|
||||
**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
|
||||
|
||||
```apex
|
||||
@ -88,12 +104,17 @@ private class MyServiceTest {
|
||||
|
||||
#### Naming Convention
|
||||
|
||||
Use descriptive method names: `test[SubjectOrAction]_[Scenario]_[ExpectedResult]`
|
||||
Use descriptive method names following one of these patterns:
|
||||
|
||||
- `testAccountUpdate_ChangeName_Success`
|
||||
- `testEmailValidation_InvalidFormat_ThrowsException`
|
||||
- `testOpportunity_ClosedWon_SendsNotification`
|
||||
- `testBatchExecution_RunningAsBatch_TriggerBypassed`
|
||||
- `[SubjectOrAction]_[Scenario]_[ExpectedResult]`: `AccountUpdate_ChangeName_Success`
|
||||
- `should[ExpectedResult]_When[Scenario]`: `shouldSendNotification_WhenOpportunityClosedWon`
|
||||
|
||||
Examples:
|
||||
|
||||
- `AccountUpdate_ChangeName_Success`
|
||||
- `EmailValidation_InvalidFormat_ThrowsException`
|
||||
- `shouldSendNotification_WhenOpportunityClosedWon`
|
||||
- `BatchExecution_RunningAsBatch_TriggerBypassed`
|
||||
|
||||
### 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`) |
|
||||
| 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
|
||||
|
||||
When reporting test results, use this structure:
|
||||
|
||||
@ -26,7 +26,7 @@ private class {{ClassName}}Test {
|
||||
* Tests successful {{methodUnderTest}} with valid input
|
||||
*/
|
||||
@IsTest
|
||||
static void test{{MethodUnderTest}}_ValidInput_Success() {
|
||||
static void {{MethodUnderTest}}_ValidInput_Success() {
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// GIVEN - Set up test conditions
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
@ -56,7 +56,7 @@ private class {{ClassName}}Test {
|
||||
* Tests {{methodUnderTest}} throws exception with null input
|
||||
*/
|
||||
@IsTest
|
||||
static void test{{MethodUnderTest}}_NullInput_ThrowsException() {
|
||||
static void {{MethodUnderTest}}_NullInput_ThrowsException() {
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// GIVEN - Null input
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
@ -83,7 +83,7 @@ private class {{ClassName}}Test {
|
||||
* Tests {{methodUnderTest}} handles invalid data gracefully
|
||||
*/
|
||||
@IsTest
|
||||
static void test{{MethodUnderTest}}_InvalidData_ReturnsError() {
|
||||
static void {{MethodUnderTest}}_InvalidData_ReturnsError() {
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// GIVEN - Invalid input data
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
@ -113,7 +113,7 @@ private class {{ClassName}}Test {
|
||||
* 251 records crosses the 200-record trigger batch boundary
|
||||
*/
|
||||
@IsTest
|
||||
static void test{{MethodUnderTest}}_BulkOperation_Success() {
|
||||
static void {{MethodUnderTest}}_BulkOperation_Success() {
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// GIVEN - 251 records (crosses 200-record batch boundary)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
@ -147,7 +147,7 @@ private class {{ClassName}}Test {
|
||||
* Tests {{methodUnderTest}} with empty list input
|
||||
*/
|
||||
@IsTest
|
||||
static void test{{MethodUnderTest}}_EmptyList_NoError() {
|
||||
static void {{MethodUnderTest}}_EmptyList_NoError() {
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// GIVEN - Empty list
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@ -20,7 +20,7 @@ private class {{ClassName}}BulkTest {
|
||||
* Verifies trigger handles multiple batch chunks correctly
|
||||
*/
|
||||
@IsTest
|
||||
static void testBulkInsert_251Records_AllProcessed() {
|
||||
static void BulkInsert_251Records_AllProcessed() {
|
||||
// GIVEN
|
||||
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)
|
||||
*/
|
||||
@IsTest
|
||||
static void testBulkInsert_ExactBatchSize_AllProcessed() {
|
||||
static void BulkInsert_ExactBatchSize_AllProcessed() {
|
||||
// GIVEN
|
||||
List<{{ObjectName}}> records = TestDataFactory.create{{ObjectName}}s(SINGLE_BATCH);
|
||||
|
||||
@ -78,7 +78,7 @@ private class {{ClassName}}BulkTest {
|
||||
* Verifies update triggers handle multiple batches
|
||||
*/
|
||||
@IsTest
|
||||
static void testBulkUpdate_251Records_AllUpdated() {
|
||||
static void BulkUpdate_251Records_AllUpdated() {
|
||||
// GIVEN - Insert records first
|
||||
List<{{ObjectName}}> records = TestDataFactory.create{{ObjectName}}s(BULK_SIZE);
|
||||
insert records;
|
||||
@ -116,7 +116,7 @@ private class {{ClassName}}BulkTest {
|
||||
* Verifies delete triggers handle cleanup correctly
|
||||
*/
|
||||
@IsTest
|
||||
static void testBulkDelete_251Records_AllDeleted() {
|
||||
static void BulkDelete_251Records_AllDeleted() {
|
||||
// GIVEN
|
||||
List<{{ObjectName}}> records = TestDataFactory.create{{ObjectName}}s(BULK_SIZE);
|
||||
insert records;
|
||||
@ -150,7 +150,7 @@ private class {{ClassName}}BulkTest {
|
||||
* Verifies partial success scenarios are handled
|
||||
*/
|
||||
@IsTest
|
||||
static void testBulkMixedOperations_PartialSuccess() {
|
||||
static void BulkMixedOperations_PartialSuccess() {
|
||||
// GIVEN - Some valid, some invalid records
|
||||
List<{{ObjectName}}> validRecords = TestDataFactory.create{{ObjectName}}s(200);
|
||||
List<{{ObjectName}}> invalidRecords = new List<{{ObjectName}}>();
|
||||
@ -199,23 +199,23 @@ private class {{ClassName}}BulkTest {
|
||||
private static void assertGovernorLimitsNotExceeded() {
|
||||
// SOQL Queries (limit: 100)
|
||||
Assert.isTrue(Limits.getQueries() < 90,
|
||||
'SOQL queries approaching limit: ' + Limits.getQueries() + '/100');
|
||||
'SOQL queries approaching limit: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());
|
||||
|
||||
// DML Statements (limit: 150)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
Assert.isTrue(Limits.getCpuTime() < 9000,
|
||||
'CPU time approaching limit: ' + Limits.getCpuTime() + '/10000');
|
||||
'CPU time approaching limit: ' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime());
|
||||
|
||||
System.debug('═══════════════════════════════════════════════════════');
|
||||
System.debug('GOVERNOR LIMIT USAGE:');
|
||||
@ -236,7 +236,7 @@ private class {{ClassName}}BulkTest {
|
||||
* Use sparingly - consumes significant test execution time
|
||||
*/
|
||||
@IsTest
|
||||
static void testStressTest_501Records_AllProcessed() {
|
||||
static void StressTest_501Records_AllProcessed() {
|
||||
// GIVEN
|
||||
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.
|
||||
* 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 {
|
||||
|
||||
@IsTest
|
||||
static void testCreateAccount_Success() {
|
||||
static void CreateAccount_ValidInput_Success() {
|
||||
// Arrange
|
||||
DMLMock.reset();
|
||||
AccountService service = new AccountService(new DMLMock());
|
||||
@ -320,7 +318,7 @@ private class AccountServiceTest {
|
||||
}
|
||||
|
||||
@IsTest
|
||||
static void testCreateAccount_NullInput_ThrowsException() {
|
||||
static void CreateAccount_NullInput_ThrowsException() {
|
||||
// Arrange
|
||||
DMLMock.reset();
|
||||
AccountService service = new AccountService(new DMLMock());
|
||||
|
||||
@ -98,7 +98,7 @@ private class {{ClassName}}CalloutTest {
|
||||
* Tests successful API call returns expected data
|
||||
*/
|
||||
@IsTest
|
||||
static void testCallout_Success_ReturnsData() {
|
||||
static void Callout_Success_ReturnsData() {
|
||||
// GIVEN
|
||||
Test.setMock(HttpCalloutMock.class, new SuccessMock());
|
||||
|
||||
@ -124,7 +124,7 @@ private class {{ClassName}}CalloutTest {
|
||||
* Tests POST request with body
|
||||
*/
|
||||
@IsTest
|
||||
static void testCallout_PostWithBody_Success() {
|
||||
static void Callout_PostWithBody_Success() {
|
||||
// GIVEN
|
||||
Test.setMock(HttpCalloutMock.class, new SuccessMock());
|
||||
|
||||
@ -155,7 +155,7 @@ private class {{ClassName}}CalloutTest {
|
||||
* Tests handling of 400 Bad Request
|
||||
*/
|
||||
@IsTest
|
||||
static void testCallout_BadRequest_HandlesGracefully() {
|
||||
static void Callout_BadRequest_HandlesGracefully() {
|
||||
// GIVEN
|
||||
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
|
||||
*/
|
||||
@IsTest
|
||||
static void testCallout_ServerError_HandlesGracefully() {
|
||||
static void Callout_ServerError_HandlesGracefully() {
|
||||
// GIVEN
|
||||
Test.setMock(HttpCalloutMock.class, new ErrorMock(500, 'Internal server error'));
|
||||
|
||||
@ -199,7 +199,7 @@ private class {{ClassName}}CalloutTest {
|
||||
* Tests handling of timeout exception
|
||||
*/
|
||||
@IsTest
|
||||
static void testCallout_Timeout_ThrowsException() {
|
||||
static void Callout_Timeout_ThrowsException() {
|
||||
// GIVEN
|
||||
Test.setMock(HttpCalloutMock.class, new TimeoutMock());
|
||||
|
||||
@ -222,7 +222,7 @@ private class {{ClassName}}CalloutTest {
|
||||
* Tests handling of 401 Unauthorized
|
||||
*/
|
||||
@IsTest
|
||||
static void testCallout_Unauthorized_HandlesGracefully() {
|
||||
static void Callout_Unauthorized_HandlesGracefully() {
|
||||
// GIVEN
|
||||
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
|
||||
*/
|
||||
@IsTest
|
||||
static void testCallout_ValidatesRequestParameters() {
|
||||
static void Callout_ValidatesRequestParameters() {
|
||||
// GIVEN
|
||||
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
|
||||
*/
|
||||
@IsTest
|
||||
static void testFutureCallout_ExecutesSuccessfully() {
|
||||
static void FutureCallout_ExecutesSuccessfully() {
|
||||
// GIVEN
|
||||
Test.setMock(HttpCalloutMock.class, new SuccessMock());
|
||||
|
||||
@ -290,7 +290,7 @@ private class {{ClassName}}CalloutTest {
|
||||
* Tests Queueable with callout
|
||||
*/
|
||||
@IsTest
|
||||
static void testQueueableCallout_ExecutesSuccessfully() {
|
||||
static void QueueableCallout_ExecutesSuccessfully() {
|
||||
// GIVEN
|
||||
Test.setMock(HttpCalloutMock.class, new SuccessMock());
|
||||
|
||||
@ -327,7 +327,7 @@ private class {{ClassName}}CalloutTest {
|
||||
* Tests multiple callouts in sequence (limit: 100 per transaction)
|
||||
*/
|
||||
@IsTest
|
||||
static void testMultipleCallouts_AllSucceed() {
|
||||
static void MultipleCallouts_AllSucceed() {
|
||||
// GIVEN
|
||||
Test.setMock(HttpCalloutMock.class, new MultiCalloutMock());
|
||||
|
||||
|
||||
@ -3,9 +3,6 @@
|
||||
*
|
||||
* The Stub API enables dynamic mocking of interfaces and virtual classes.
|
||||
* 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 {
|
||||
|
||||
@IsTest
|
||||
static void testWithConfiguredResponse() {
|
||||
static void ConfiguredResponse_ReturnsExpectedValues() {
|
||||
// Arrange - Configure stub responses
|
||||
AccountServiceStub stub = new AccountServiceStub()
|
||||
.whenCalled('getAccountCount', 42)
|
||||
@ -144,7 +141,7 @@ private class AccountServiceStubTest {
|
||||
}
|
||||
|
||||
@IsTest
|
||||
static void testDefaultResponses() {
|
||||
static void DefaultResponse_ReturnsDefaults() {
|
||||
// Arrange - No configured responses
|
||||
AccountServiceStub stub = new AccountServiceStub();
|
||||
IAccountService service = (IAccountService) Test.createStub(
|
||||
@ -241,7 +238,7 @@ public class ErrorStub implements System.StubProvider {
|
||||
private class ErrorStubTest {
|
||||
|
||||
@IsTest
|
||||
static void testErrorHandling() {
|
||||
static void ErrorHandling_ThrowsConfiguredException() {
|
||||
// Arrange
|
||||
ErrorStub stub = new ErrorStub().failOn('getAccount', 'Account not found');
|
||||
IAccountService service = (IAccountService) Test.createStub(
|
||||
@ -265,8 +262,6 @@ private class ErrorStubTest {
|
||||
|
||||
/**
|
||||
* Highly flexible stub that can mock any interface
|
||||
*
|
||||
* Inspired by: Suraj Pillai's UniversalMock pattern
|
||||
*/
|
||||
@IsTest
|
||||
public class UniversalMock implements System.StubProvider {
|
||||
|
||||
@ -37,8 +37,8 @@ sf apex run test \
|
||||
|
||||
```bash
|
||||
sf apex run test \
|
||||
--tests AccountServiceTest.testCreate \
|
||||
--tests AccountServiceTest.testUpdate \
|
||||
--tests AccountServiceTest.CreateAccount_Success \
|
||||
--tests AccountServiceTest.UpdateAccount_Success \
|
||||
--target-org my-sandbox
|
||||
```
|
||||
|
||||
@ -213,7 +213,7 @@ sf apex run test \
|
||||
|
||||
```bash
|
||||
sf apex run test \
|
||||
--tests AccountServiceTest.testCreate \
|
||||
--tests AccountServiceTest.CreateAccount_Success \
|
||||
--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.
|
||||
|
||||
> **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
|
||||
@ -28,8 +26,6 @@ Salesforce **requires** mock callouts - you cannot make real HTTP requests in te
|
||||
```apex
|
||||
/**
|
||||
* Simple HTTP mock returning a fixed response
|
||||
*
|
||||
* @see https://www.apexhours.com/testing-web-services-callouts-in-salesforce/
|
||||
*/
|
||||
@IsTest
|
||||
public class MockHttpResponse implements HttpCalloutMock {
|
||||
@ -53,7 +49,7 @@ public class MockHttpResponse implements HttpCalloutMock {
|
||||
|
||||
// Usage in test
|
||||
@IsTest
|
||||
static void testApiCall_Success() {
|
||||
static void shouldReturnData_WhenApiCallSucceeds() {
|
||||
String mockBody = '{"success": true, "data": {"id": "12345"}}';
|
||||
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(200, mockBody));
|
||||
|
||||
@ -102,7 +98,7 @@ public class MultiEndpointMock implements HttpCalloutMock {
|
||||
* Mock for testing error handling
|
||||
*/
|
||||
@IsTest
|
||||
static void testApiCall_ServerError_HandlesGracefully() {
|
||||
static void shouldHandleGracefully_WhenServerReturnsError() {
|
||||
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(500, '{"error": "Server Error"}'));
|
||||
|
||||
Test.startTest();
|
||||
@ -127,8 +123,6 @@ This pattern eliminates database operations from tests, achieving 35x faster exe
|
||||
```apex
|
||||
/**
|
||||
* Interface for DML operations - enables mocking
|
||||
*
|
||||
* @see https://www.jamessimone.net/blog/joys-of-apex/mocking-dml/
|
||||
*/
|
||||
public interface IDML {
|
||||
void doInsert(SObject record);
|
||||
@ -307,7 +301,7 @@ public class AccountService {
|
||||
|
||||
// Test using mock DML
|
||||
@IsTest
|
||||
static void testCreateAccount_NoDatabase() {
|
||||
static void shouldCreateAccount_WithMockDML() {
|
||||
// Arrange
|
||||
DMLMock.reset();
|
||||
AccountService service = new AccountService(new DMLMock());
|
||||
@ -393,7 +387,7 @@ public class AccountServiceStub implements System.StubProvider {
|
||||
```apex
|
||||
// Create stub with Test.createStub()
|
||||
@IsTest
|
||||
static void testWithStub() {
|
||||
static void shouldReturnConfiguredValues_WhenUsingStub() {
|
||||
// Create the stub
|
||||
AccountServiceStub stub = new AccountServiceStub()
|
||||
.withMethodReturn('getAccountCount', 42);
|
||||
@ -450,7 +444,7 @@ public class AccountSelector {
|
||||
|
||||
// Usage in test
|
||||
@IsTest
|
||||
static void testWithMockedQuery() {
|
||||
static void shouldReturnMockData_WhenQueryMocked() {
|
||||
// Arrange - no database insert needed!
|
||||
List<Account> mockAccounts = new List<Account>{
|
||||
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.
|
||||
|
||||
> **Source**: [James Simone - Writing Performant Apex Tests](https://www.jamessimone.net/blog/joys-of-apex/writing-performant-apex-tests/)
|
||||
|
||||
---
|
||||
|
||||
## Why Test Speed Matters
|
||||
@ -172,7 +170,7 @@ List<List<SObject>> results = [FIND 'test' IN ALL FIELDS RETURNING Account];
|
||||
|
||||
// ✅ SOLUTION: Use Test.setFixedSearchResults()
|
||||
@IsTest
|
||||
static void testSearch() {
|
||||
static void Search_WithFixedResults_FindsRecord() {
|
||||
Account acc = new Account(Name = 'Searchable');
|
||||
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
|
||||
|
||||
# ✅ 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
|
||||
@IsTest
|
||||
static void testPerformance() {
|
||||
static void shouldCompleteInUnderOneSecond() {
|
||||
Long startTime = System.currentTimeMillis();
|
||||
|
||||
// Your test code here
|
||||
|
||||
@ -19,13 +19,13 @@ User: "Run tests for AccountService with auto-fix enabled"
|
||||
|
||||
Claude:
|
||||
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)
|
||||
4. Read AccountService.cls (trace the null reference)
|
||||
5. Identify: Missing null check in AccountService.processAccounts()
|
||||
6. generating-apex: Add null safety to AccountService.processAccounts()
|
||||
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.
|
||||
```
|
||||
|
||||
|
||||
@ -44,7 +44,6 @@ All steps in this workflow are MANDATORY and must be executed in order. Execute
|
||||
|
||||
**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 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.
|
||||
- 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-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**
|
||||
- You MUST call the MCP tool `run_code_analyzer` RIGHT NOW on the newly generated/updated `.cls` files.
|
||||
6. [MANDATORY] **run_code_analyzer MCP tool -- DO NOT SKIP -- REQUIRES TOOL INVOCATION**
|
||||
- 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.
|
||||
- Remediate all violations with severity levels `sev0`, `sev1`, and `sev2`.
|
||||
- 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.
|
||||
- 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**
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
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
|
||||
|
||||
---
|
||||
@ -339,8 +338,8 @@ Prefer current language features:
|
||||
Deliverables per class:
|
||||
- `{ClassName}.cls`
|
||||
- `{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-meta.xml` (generated via `skills/generating-apex-test/SKILL.md`)
|
||||
- `{ClassName}Test.cls` (generated via `generating-apex-test` skill)
|
||||
- `{ClassName}Test.cls-meta.xml` (generated via `generating-apex-test` skill)
|
||||
|
||||
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 |
|
||||
| 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 |
|
||||
| Build Flow that calls Apex | Flow skill | Declarative orchestration via `@InvocableMethod` |
|
||||
| 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 |
|
||||
| **`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 |
|
||||
| **`!= 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 |
|
||||
| **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:**
|
||||
```apex
|
||||
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:**
|
||||
@ -148,7 +148,7 @@ if (acc.IsActive__c == true) {
|
||||
// Clear intent
|
||||
}
|
||||
|
||||
// Or even better
|
||||
// Or even better if acc.IsActive__c can't be null
|
||||
if (acc.IsActive__c) {
|
||||
// Most concise
|
||||
}
|
||||
@ -173,13 +173,13 @@ for (Account acc : accounts) {
|
||||
|
||||
**✅ GOOD:**
|
||||
```apex
|
||||
Map<Id, Account> accountsWithContacts = new Map<Id, Account>([
|
||||
List<Account> accountsWithContacts = [
|
||||
SELECT Id, (SELECT Id FROM Contacts)
|
||||
FROM Account
|
||||
WHERE Id IN :accountIds
|
||||
]);
|
||||
];
|
||||
|
||||
for (Account acc : accountsWithContacts.values()) {
|
||||
for (Account acc : accountsWithContacts) {
|
||||
for (Contact con : acc.Contacts) {
|
||||
// No SOQL in loop
|
||||
}
|
||||
@ -204,19 +204,21 @@ public class AccountService {
|
||||
**✅ GOOD:**
|
||||
```apex
|
||||
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) {
|
||||
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
|
||||
|
||||
Based on "Clean Apex Code" by Pablo Gonzalez and clean code principles.
|
||||
|
||||
### Long Methods
|
||||
|
||||
#### The Smell
|
||||
|
||||
@ -75,9 +75,6 @@ public class TriggerHelper {
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
> 💡 *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
|
||||
|
||||
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())
|
||||
// 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
|
||||
// SOQL query in a loop replaced with map-based lookup — O(1) per record.
|
||||
Account acc = accountsByExternalId.get(record.ExternalId__c);
|
||||
|
||||
@ -896,9 +896,6 @@ if (result.success) {
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
> 💡 *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
|
||||
|
||||
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.
|
||||
|
||||
> **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
|
||||
@ -703,4 +701,3 @@ def validate_apex(content):
|
||||
|
||||
- **Existing Anti-Patterns**: See `references/anti-patterns.md` for traditional Apex anti-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
|
||||
|
||||
Install from: https://github.com/mitchspano/trigger-actions-framework
|
||||
|
||||
## Basic Setup
|
||||
|
||||
### 1. Create the Trigger
|
||||
|
||||
Loading…
Reference in New Issue
Block a user