Expert testing engineer specializing in Apex test execution, code coverage analysis, mock frameworks, and agentic test-fix loops. Execute tests, analyze failures, and automatically fix issues.
Core Responsibilities
-
Test Execution: Run Apex tests via
sf apex run testwith coverage analysis -
Coverage Analysis: Parse coverage reports, identify untested code paths
-
Failure Analysis: Parse test failures, identify root causes, suggest fixes
-
Agentic Test-Fix Loop: Automatically fix failing tests and re-run until passing
-
Test Generation: Create test classes using sf-apex patterns
-
Bulk Testing: Validate with 251+ records for governor limit safety
Workflow (5-Phase Pattern)
Phase 1: Test Discovery
Use AskUserQuestion to gather:
-
Test scope (single class, all tests, specific test suite)
-
Target org alias
-
Coverage threshold requirement (default: 75%, recommended: 90%)
-
Whether to enable agentic fix loop
Then:
-
Check existing tests:
Glob: **/*Test*.cls,Glob: **/*_Test.cls -
Check for Test Data Factories:
Glob: **/*TestDataFactory*.cls -
Create TodoWrite tasks
Phase 2: Test Execution
Run Single Test Class:
sf apex run test --class-names MyClassTest --code-coverage --result-format json --output-dir test-results --target-org [alias]
Run All Tests:
sf apex run test --test-level RunLocalTests --code-coverage --result-format json --output-dir test-results --target-org [alias]
Run Specific Methods:
sf apex run test --tests MyClassTest.testMethod1 --tests MyClassTest.testMethod2 --code-coverage --result-format json --target-org [alias]
Run Test Suite:
sf apex run test --suite-names MySuite --code-coverage --result-format json --target-org [alias]
Phase 3: Results Analysis
Parse test-results JSON:
Read: test-results/test-run-id.json
Coverage Summary Output:
📊 TEST EXECUTION RESULTS
════════════════════════════════════════════════════════════════
Test Run ID: 707xx0000000000
Org: my-sandbox
Duration: 45.2s
SUMMARY
───────────────────────────────────────────────────────────────
✅ Passed: 42
❌ Failed: 3
⏭️ Skipped: 0
📈 Coverage: 78.5%
FAILED TESTS
───────────────────────────────────────────────────────────────
❌ AccountServiceTest.testBulkInsert
Line 45: System.AssertException: Assertion Failed
Expected: 200, Actual: 199
❌ LeadScoringTest.testNullHandling
Line 23: System.NullPointerException: Attempt to de-reference null
❌ OpportunityTriggerTest.testValidation
Line 67: System.DmlException: FIELD_CUSTOM_VALIDATION_EXCEPTION
COVERAGE BY CLASS
───────────────────────────────────────────────────────────────
Class Lines Covered Uncovered %
AccountService 150 142 8 94.7% ✅
LeadScoringService 85 68 17 80.0% ✅
OpportunityTrigger 45 28 17 62.2% ⚠️
ContactHelper 30 15 15 50.0% ❌
UNCOVERED LINES (OpportunityTrigger)
───────────────────────────────────────────────────────────────
Lines 23-28: Exception handling block
Lines 45-52: Bulk processing edge case
Lines 78-82: Null check branch
Phase 4: Agentic Test-Fix Loop
When tests fail, automatically:
┌─────────────────────────────────────────────────────────────────┐
│ AGENTIC TEST-FIX LOOP │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Parse failure message and stack trace │
│ 2. Identify root cause: │
│ - Assertion failure → Check expected vs actual │
│ - NullPointerException → Add null checks │
│ - DmlException → Check validation rules, required fields │
│ - LimitException → Reduce SOQL/DML in test │
│ 3. Read the failing test class │
│ 4. Read the class under test │
│ 5. Generate fix using sf-apex skill │
│ 6. Re-run the specific failing test │
│ 7. Repeat until passing (max 3 attempts) │
│ │
└─────────────────────────────────────────────────────────────────┘
Failure Analysis Decision Tree:
| System.AssertException
| Wrong expected value or logic bug
| Analyze assertion, check if test or code is wrong
| System.NullPointerException
| Missing null check or test data
| Add null safety or fix test data setup
| System.DmlException
| Validation rule, required field, trigger
| Check org config, add required fields to test data
| System.LimitException
| Governor limit hit
| Refactor to use bulkified patterns
| System.QueryException
| No rows returned
| Add test data or adjust query
| System.TypeException
| Type mismatch
| Fix type casting or data format
Auto-Fix Command:
Skill(skill="sf-apex", args="Fix failing test [TestClassName].[methodName] - Error: [error message]")
Phase 5: Coverage Improvement
If coverage < threshold:
- Identify Uncovered Lines:
sf apex run test --class-names MyClassTest --code-coverage --detailed-coverage --result-format json --target-org [alias]
- Generate Tests for Uncovered Code:
Read: force-app/main/default/classes/MyClass.cls (lines 45-52)
Then use sf-apex to generate test methods targeting those lines.
- Bulk Test Validation:
Skill(skill="sf-data", args="Create 251 [ObjectName] records for bulk testing")
- Re-run with New Tests:
sf apex run test --class-names MyClassTest --code-coverage --result-format json --target-org [alias]
Best Practices (120-Point Scoring)
| Test Coverage | 25 | 90%+ class coverage; all public methods tested; edge cases covered
| Assertion Quality | 25 | Assert class used; meaningful messages; positive AND negative tests
| Bulk Testing | 20 | Test with 251+ records; verify no SOQL/DML in loops under load
| Test Data | 20 | Test Data Factory used; no hardcoded IDs; @TestSetup for efficiency
| Isolation | 15 | SeeAllData=false; no org dependencies; mock external callouts
| Documentation | 15 | Test method names describe scenario; comments for complex setup
Scoring Thresholds:
⭐⭐⭐⭐⭐ 108-120 pts (90%+) → Production Ready
⭐⭐⭐⭐ 96-107 pts (80-89%) → Good, minor improvements
⭐⭐⭐ 84-95 pts (70-79%) → Acceptable, needs work
⭐⭐ 72-83 pts (60-69%) → Below standard
⭐ <72 pts (<60%) → BLOCKED - Major issues
⛔ TESTING GUARDRAILS (MANDATORY)
BEFORE running tests, verify:
| Org authenticated
| sf org display --target-org [alias]
| Tests need valid org connection
| Classes deployed
| sf project deploy report --target-org [alias]
| Can't test undeployed code
| Test data exists | Check @TestSetup or TestDataFactory | Tests need data to operate on
NEVER do these:
| @IsTest(SeeAllData=true)
| Tests depend on org data, break in clean orgs
| Always SeeAllData=false (default)
| Hardcoded Record IDs | IDs differ between orgs | Query or create in test
| No assertions | Tests pass without validating anything | Assert every expected outcome
| Single record tests only | Misses bulk trigger issues | Always test with 200+ records
| Test.startTest() without Test.stopTest()
| Async code won't execute
| Always pair start/stop
CLI Command Reference
Test Execution Commands
| sf apex run test
| Run tests
| See examples above
| sf apex get test
| Get async test status
| sf apex get test --test-run-id 707xx...
| sf apex list log
| List debug logs
| sf apex list log --target-org alias
| sf apex tail log
| Stream logs real-time
| sf apex tail log --target-org alias
Useful Flags
| --code-coverage
| Include coverage in results
| --detailed-coverage
| Line-by-line coverage (slower)
| --result-format json
| Machine-parseable output
| --output-dir
| Save results to directory
| --synchronous
| Wait for completion (default)
| --test-level RunLocalTests
| All tests except managed packages
| --test-level RunAllTestsInOrg
| Every test including packages
Test Patterns & Templates
Pattern 1: Basic Test Class
Use template: templates/basic-test.cls
@IsTest
private class AccountServiceTest {
@TestSetup
static void setupTestData() {
// Use Test Data Factory for consistent data creation
List<Account> accounts = TestDataFactory.createAccounts(5);
insert accounts;
}
@IsTest
static void testCreateAccount_Success() {
// Given
Account testAccount = new Account(Name = 'Test Account');
// When
Test.startTest();
Id accountId = AccountService.createAccount(testAccount);
Test.stopTest();
// Then
Assert.isNotNull(accountId, 'Account ID should not be null');
Account inserted = [SELECT Name FROM Account WHERE Id = :accountId];
Assert.areEqual('Test Account', inserted.Name, 'Account name should match');
}
@IsTest
static void testCreateAccount_NullInput_ThrowsException() {
// Given
Account nullAccount = null;
// When/Then
try {
Test.startTest();
AccountService.createAccount(nullAccount);
Test.stopTest();
Assert.fail('Expected IllegalArgumentException was not thrown');
} catch (IllegalArgumentException e) {
Assert.isTrue(e.getMessage().contains('cannot be null'),
'Error message should mention null: ' + e.getMessage());
}
}
}
Pattern 2: Bulk Test (251+ Records)
Use template: templates/bulk-test.cls
@IsTest
static void testBulkInsert_251Records() {
// Given - 251 records crosses the 200-record batch boundary
List<Account> accounts = TestDataFactory.createAccounts(251);
// When
Test.startTest();
insert accounts; // Triggers fire in batches of 200, then 51
Test.stopTest();
// Then
Integer count = [SELECT COUNT() FROM Account];
Assert.areEqual(251, count, 'All 251 accounts should be inserted');
// Verify no governor limits hit
Assert.isTrue(Limits.getQueries() < 100,
'Should not approach SOQL limit: ' + Limits.getQueries());
}
Pattern 3: Mock Callout Test
Use template: templates/mock-callout-test.cls
@IsTest
private class ExternalAPIServiceTest {
// Mock class for HTTP callouts
private class MockHttpResponse implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setBody('{"success": true, "data": {"id": "12345"}}');
return res;
}
}
@IsTest
static void testCallExternalAPI_Success() {
// Given
Test.setMock(HttpCalloutMock.class, new MockHttpResponse());
// When
Test.startTest();
String result = ExternalAPIService.callAPI('test-endpoint');
Test.stopTest();
// Then
Assert.isTrue(result.contains('success'), 'Response should indicate success');
}
}
Pattern 4: Test Data Factory
Use template: templates/test-data-factory.cls
@IsTest
public class TestDataFactory {
public static List<Account> createAccounts(Integer count) {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < count; i++) {
accounts.add(new Account(
Name = 'Test Account ' + i,
Industry = 'Technology',
BillingCity = 'San Francisco'
));
}
return accounts;
}
public static List<Contact> createContacts(Integer count, Id accountId) {
List<Contact> contacts = new List<Contact>();
for (Integer i = 0; i < count; i++) {
contacts.add(new Contact(
FirstName = 'Test',
LastName = 'Contact ' + i,
AccountId = accountId,
Email = 'test' + i + '@example.com'
));
}
return contacts;
}
// Convenience method with insert
public static List<Account> createAndInsertAccounts(Integer count) {
List<Account> accounts = createAccounts(count);
insert accounts;
return accounts;
}
}
Agentic Test-Fix Loop Implementation
How It Works
When the agentic loop is enabled, sf-testing will:
-
Run tests and capture results
-
Parse failures to identify error type and location
-
Read source files (test class + class under test)
-
Analyze root cause using the decision tree above
-
Generate fix by invoking sf-apex skill
-
Re-run failing test to verify fix
-
Iterate until passing or max attempts (3)
Example Agentic Flow
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
3. Read AccountServiceTest.cls (line 45 context)
4. Read AccountService.cls (trace the null reference)
5. Identify: Missing null check in AccountService.processAccounts()
6. Skill(sf-apex): Add null safety to AccountService.processAccounts()
7. Deploy fix
8. Re-run: sf apex run test --tests AccountServiceTest.testBulkInsert
9. ✅ Passing! Report success.
Cross-Skill Integration
| sf-apex
| Generate test classes, fix failing code
| Skill(skill="sf-apex", args="Create test class for LeadService")
| sf-data
| Create bulk test data (251+ records)
| Skill(skill="sf-data", args="Create 251 Leads for bulk testing")
| sf-deploy
| Deploy test classes to org
| Skill(skill="sf-deploy", args="Deploy tests to sandbox")
| sf-debug
| Analyze failures with debug logs
| Skill(skill="sf-debug", args="Analyze test failure logs")
Common Test Failures & Fixes
| MIXED_DML_OPERATION
| User + non-setup object in same transaction
| Use System.runAs() or separate transactions
| CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY
| Trigger or flow error
| Check trigger logic with debug logs
| REQUIRED_FIELD_MISSING
| Test data incomplete
| Add required fields to TestDataFactory
| DUPLICATE_VALUE
| Unique field conflict
| Use dynamic values or delete existing
| FIELD_CUSTOM_VALIDATION_EXCEPTION
| Validation rule fired
| Meet validation criteria in test data
| UNABLE_TO_LOCK_ROW
| Record lock conflict
| Use FOR UPDATE or retry logic
Dependencies
Required: Target org with sf CLI authenticated
Recommended: sf-apex (for auto-fix), sf-data (for bulk test data), sf-debug (for log analysis)
Install: /plugin install github:Jaganpro/sf-skills/sf-testing
Documentation
| testing-best-practices.md | General testing guidelines
| cli-commands.md | SF CLI test commands
| mocking-patterns.md | Mocking vs Stubbing, DML mocking, HttpCalloutMock
| performance-optimization.md | Fast tests, reduce execution time
Templates
| basic-test.cls | Standard test class with Given-When-Then
| bulk-test.cls | 251+ record bulk testing
| mock-callout-test.cls | HTTP callout mocking
| test-data-factory.cls | Reusable test data creation
| dml-mock.cls | DML abstraction for 35x faster tests
| stub-provider-example.cls | StubProvider for dynamic behavior
Credits
See CREDITS.md for acknowledgments of community resources that shaped this skill.
License
MIT License. See LICENSE file. Copyright (c) 2024-2025 Jag Valaiyapathy