2026-06-26 20:21:51 +08:00
|
|
|
|
<!-- Parent: platform-apex-test-run/SKILL.md -->
|
|
|
|
|
|
# Apex Testing Best Practices
|
|
|
|
|
|
|
|
|
|
|
|
## Overview
|
|
|
|
|
|
|
|
|
|
|
|
This guide covers best practices for writing effective, maintainable Apex tests that ensure code quality and prevent regressions.
|
|
|
|
|
|
|
|
|
|
|
|
## The Testing Pyramid
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
╱╲
|
|
|
|
|
|
╱ ╲
|
|
|
|
|
|
╱ E2E╲ Few end-to-end tests
|
|
|
|
|
|
╱──────╲
|
|
|
|
|
|
╱ ╲
|
|
|
|
|
|
╱Integration╲ Moderate integration tests
|
|
|
|
|
|
╱────────────╲
|
|
|
|
|
|
╱ ╲
|
|
|
|
|
|
╱ Unit Tests ╲ Many unit tests
|
|
|
|
|
|
╱──────────────────╲
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Core Principles
|
|
|
|
|
|
|
|
|
|
|
|
### 1. Test One Thing Per Method
|
|
|
|
|
|
|
|
|
|
|
|
```apex
|
|
|
|
|
|
// ❌ BAD: Testing multiple things
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testEverything() {
|
|
|
|
|
|
Account acc = new Account(Name = 'Test');
|
|
|
|
|
|
insert acc;
|
|
|
|
|
|
acc.Name = 'Updated';
|
|
|
|
|
|
update acc;
|
|
|
|
|
|
delete acc;
|
|
|
|
|
|
// What exactly are we testing?
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ GOOD: Single responsibility
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testAccountInsert_ValidName_Success() {
|
|
|
|
|
|
Account acc = new Account(Name = 'Test');
|
|
|
|
|
|
insert acc;
|
|
|
|
|
|
Assert.isNotNull(acc.Id, 'Account should be inserted');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testAccountUpdate_ChangeName_Success() {
|
|
|
|
|
|
Account acc = TestDataFactory.createAndInsertAccounts(1)[0];
|
|
|
|
|
|
acc.Name = 'Updated';
|
|
|
|
|
|
update acc;
|
|
|
|
|
|
Account updated = [SELECT Name FROM Account WHERE Id = :acc.Id];
|
|
|
|
|
|
Assert.areEqual('Updated', updated.Name, 'Name should be updated');
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. Use Descriptive Test Names
|
|
|
|
|
|
|
|
|
|
|
|
```apex
|
|
|
|
|
|
// ❌ BAD: Unclear names
|
|
|
|
|
|
@IsTest static void test1() { }
|
|
|
|
|
|
@IsTest static void testMethod() { }
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ GOOD: Describes scenario, condition, and expectation
|
|
|
|
|
|
@IsTest static void testCreateAccount_WithValidData_ReturnsId() { }
|
|
|
|
|
|
@IsTest static void testCreateAccount_WithNullName_ThrowsException() { }
|
|
|
|
|
|
@IsTest static void testBulkInsert_251Records_AllSucceed() { }
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. Follow AAA Pattern (Arrange-Act-Assert)
|
|
|
|
|
|
|
|
|
|
|
|
```apex
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testCalculateDiscount_VIPCustomer_Returns20Percent() {
|
|
|
|
|
|
// ARRANGE - Set up test data
|
|
|
|
|
|
Account vipAccount = new Account(
|
|
|
|
|
|
Name = 'VIP Customer',
|
|
|
|
|
|
Type = 'VIP'
|
|
|
|
|
|
);
|
|
|
|
|
|
insert vipAccount;
|
|
|
|
|
|
|
|
|
|
|
|
// ACT - Execute the code under test
|
|
|
|
|
|
Test.startTest();
|
|
|
|
|
|
Decimal discount = DiscountService.calculateDiscount(vipAccount.Id);
|
|
|
|
|
|
Test.stopTest();
|
|
|
|
|
|
|
|
|
|
|
|
// ASSERT - Verify results
|
|
|
|
|
|
Assert.areEqual(0.20, discount, 'VIP customers should get 20% discount');
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Test Data Factory Pattern
|
|
|
|
|
|
|
|
|
|
|
|
### Why Use Test Data Factory?
|
|
|
|
|
|
|
|
|
|
|
|
1. **Consistency**: Same data creation logic everywhere
|
|
|
|
|
|
2. **Maintainability**: Change in one place affects all tests
|
|
|
|
|
|
3. **Readability**: Tests focus on logic, not data setup
|
|
|
|
|
|
4. **Flexibility**: Easy to create variations
|
|
|
|
|
|
|
|
|
|
|
|
### Factory Methods
|
|
|
|
|
|
|
|
|
|
|
|
```apex
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
public class TestDataFactory {
|
|
|
|
|
|
|
|
|
|
|
|
// Basic creation (no insert)
|
|
|
|
|
|
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'
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
return accounts;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create and insert
|
|
|
|
|
|
public static List<Account> createAndInsertAccounts(Integer count) {
|
|
|
|
|
|
List<Account> accounts = createAccounts(count);
|
|
|
|
|
|
insert accounts;
|
|
|
|
|
|
return accounts;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// With specific attributes
|
|
|
|
|
|
public static Account createAccount(String name, String industry) {
|
|
|
|
|
|
return new Account(Name = name, Industry = industry);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## @TestSetup for Efficiency
|
|
|
|
|
|
|
|
|
|
|
|
```apex
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
private class AccountServiceTest {
|
|
|
|
|
|
|
|
|
|
|
|
@TestSetup
|
|
|
|
|
|
static void setupTestData() {
|
|
|
|
|
|
// This runs ONCE for all test methods in the class
|
|
|
|
|
|
// Data is rolled back after each test method
|
|
|
|
|
|
List<Account> accounts = TestDataFactory.createAccounts(10);
|
|
|
|
|
|
insert accounts;
|
|
|
|
|
|
|
|
|
|
|
|
List<Contact> contacts = new List<Contact>();
|
|
|
|
|
|
for (Account acc : accounts) {
|
|
|
|
|
|
contacts.addAll(TestDataFactory.createContacts(5, acc.Id));
|
|
|
|
|
|
}
|
|
|
|
|
|
insert contacts;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testMethod1() {
|
|
|
|
|
|
// Has access to 10 accounts and 50 contacts
|
|
|
|
|
|
List<Account> accounts = [SELECT Id FROM Account];
|
|
|
|
|
|
Assert.areEqual(10, accounts.size());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testMethod2() {
|
|
|
|
|
|
// ALSO has access to 10 accounts and 50 contacts
|
|
|
|
|
|
// @TestSetup data is available to all test methods
|
|
|
|
|
|
List<Contact> contacts = [SELECT Id FROM Contact];
|
|
|
|
|
|
Assert.areEqual(50, contacts.size());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Bulk Testing (251+ Records)
|
|
|
|
|
|
|
|
|
|
|
|
### Why 251 Records?
|
|
|
|
|
|
|
|
|
|
|
|
Triggers process records in batches of 200. Testing with 251 records ensures:
|
|
|
|
|
|
- First batch: 200 records
|
|
|
|
|
|
- Second batch: 51 records
|
|
|
|
|
|
- Crosses the batch boundary, revealing bulkification issues
|
|
|
|
|
|
|
|
|
|
|
|
```apex
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testTrigger_BulkInsert_NoDMLInLoop() {
|
|
|
|
|
|
// ARRANGE
|
|
|
|
|
|
List<Account> accounts = TestDataFactory.createAccounts(251);
|
|
|
|
|
|
|
|
|
|
|
|
// ACT
|
|
|
|
|
|
Test.startTest();
|
|
|
|
|
|
insert accounts; // Trigger fires twice: 200, then 51
|
|
|
|
|
|
Test.stopTest();
|
|
|
|
|
|
|
|
|
|
|
|
// ASSERT
|
|
|
|
|
|
Assert.areEqual(251, [SELECT COUNT() FROM Account],
|
|
|
|
|
|
'All 251 accounts should be created');
|
|
|
|
|
|
|
|
|
|
|
|
// Verify governor limits not approached
|
|
|
|
|
|
Assert.isTrue(Limits.getQueries() < 90,
|
|
|
|
|
|
'Should not approach SOQL limit');
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Negative Testing
|
|
|
|
|
|
|
|
|
|
|
|
### Test for Expected Failures
|
|
|
|
|
|
|
|
|
|
|
|
```apex
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testCreateAccount_NullName_ThrowsException() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
Account acc = new Account(Name = null);
|
|
|
|
|
|
insert acc;
|
|
|
|
|
|
Assert.fail('Expected DmlException was not thrown');
|
|
|
|
|
|
} catch (DmlException e) {
|
|
|
|
|
|
Assert.isTrue(
|
|
|
|
|
|
e.getMessage().contains('REQUIRED_FIELD_MISSING'),
|
|
|
|
|
|
'Should throw required field error'
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testWithdraw_InsufficientFunds_ReturnsFalse() {
|
|
|
|
|
|
Account acc = TestDataFactory.createAndInsertAccounts(1)[0];
|
|
|
|
|
|
acc.AnnualRevenue = 100;
|
|
|
|
|
|
update acc;
|
|
|
|
|
|
|
|
|
|
|
|
Test.startTest();
|
|
|
|
|
|
Boolean result = AccountService.withdraw(acc.Id, 200);
|
|
|
|
|
|
Test.stopTest();
|
|
|
|
|
|
|
|
|
|
|
|
Assert.isFalse(result, 'Withdrawal should fail with insufficient funds');
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Mock Framework Patterns
|
|
|
|
|
|
|
|
|
|
|
|
### HttpCalloutMock
|
|
|
|
|
|
|
|
|
|
|
|
```apex
|
|
|
|
|
|
private class SuccessHttpMock implements HttpCalloutMock {
|
|
|
|
|
|
public HttpResponse respond(HttpRequest req) {
|
|
|
|
|
|
HttpResponse res = new HttpResponse();
|
|
|
|
|
|
res.setStatusCode(200);
|
|
|
|
|
|
res.setBody('{"success": true}');
|
|
|
|
|
|
return res;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testExternalAPICall_Success() {
|
|
|
|
|
|
Test.setMock(HttpCalloutMock.class, new SuccessHttpMock());
|
|
|
|
|
|
|
|
|
|
|
|
Test.startTest();
|
|
|
|
|
|
String result = MyService.callExternalAPI();
|
|
|
|
|
|
Test.stopTest();
|
|
|
|
|
|
|
|
|
|
|
|
Assert.isTrue(result.contains('success'));
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Stub API (Test.StubProvider)
|
|
|
|
|
|
|
|
|
|
|
|
```apex
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
private class AccountServiceTest {
|
|
|
|
|
|
|
|
|
|
|
|
// Stub for dependency injection
|
|
|
|
|
|
private class SelectorStub implements System.StubProvider {
|
|
|
|
|
|
public Object handleMethodCall(
|
|
|
|
|
|
Object stubbedObject,
|
|
|
|
|
|
String stubbedMethodName,
|
|
|
|
|
|
Type returnType,
|
|
|
|
|
|
List<Type> paramTypes,
|
|
|
|
|
|
List<String> paramNames,
|
|
|
|
|
|
List<Object> args
|
|
|
|
|
|
) {
|
|
|
|
|
|
if (stubbedMethodName == 'getAccountById') {
|
|
|
|
|
|
return new Account(Name = 'Mocked Account');
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testServiceWithMockedSelector() {
|
|
|
|
|
|
// Create stub
|
|
|
|
|
|
AccountSelector mockSelector = (AccountSelector) Test.createStub(
|
|
|
|
|
|
AccountSelector.class,
|
|
|
|
|
|
new SelectorStub()
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Inject into service
|
|
|
|
|
|
AccountService service = new AccountService(mockSelector);
|
|
|
|
|
|
|
|
|
|
|
|
// Test with mocked dependency
|
|
|
|
|
|
Test.startTest();
|
|
|
|
|
|
Account result = service.getAccount('001xx000000001');
|
|
|
|
|
|
Test.stopTest();
|
|
|
|
|
|
|
|
|
|
|
|
Assert.areEqual('Mocked Account', result.Name);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Governor Limit Testing
|
|
|
|
|
|
|
|
|
|
|
|
```apex
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testBulkOperation_StaysWithinLimits() {
|
|
|
|
|
|
List<Account> accounts = TestDataFactory.createAccounts(200);
|
|
|
|
|
|
|
|
|
|
|
|
Test.startTest();
|
|
|
|
|
|
insert accounts;
|
|
|
|
|
|
Test.stopTest();
|
|
|
|
|
|
|
|
|
|
|
|
// Assert limits not exceeded
|
|
|
|
|
|
System.debug('SOQL queries used: ' + Limits.getQueries());
|
|
|
|
|
|
System.debug('DML statements used: ' + Limits.getDmlStatements());
|
|
|
|
|
|
|
|
|
|
|
|
Assert.isTrue(Limits.getQueries() < 100,
|
|
|
|
|
|
'Should stay under SOQL limit: ' + Limits.getQueries());
|
|
|
|
|
|
Assert.isTrue(Limits.getDmlStatements() < 150,
|
|
|
|
|
|
'Should stay under DML limit: ' + Limits.getDmlStatements());
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Common Anti-Patterns to Avoid
|
|
|
|
|
|
|
2026-07-03 13:25:02 +08:00
|
|
|
|
### SeeAllData=true
|
2026-06-26 20:21:51 +08:00
|
|
|
|
|
|
|
|
|
|
```apex
|
|
|
|
|
|
// ❌ BAD: Depends on org data
|
|
|
|
|
|
@IsTest(SeeAllData=true)
|
|
|
|
|
|
static void testBadPattern() {
|
|
|
|
|
|
Account acc = [SELECT Id FROM Account LIMIT 1];
|
|
|
|
|
|
// This will fail in empty orgs!
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ GOOD: Creates own data
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testGoodPattern() {
|
|
|
|
|
|
Account acc = TestDataFactory.createAndInsertAccounts(1)[0];
|
|
|
|
|
|
// Works in any org
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-03 13:25:02 +08:00
|
|
|
|
### No Assertions
|
2026-06-26 20:21:51 +08:00
|
|
|
|
|
|
|
|
|
|
```apex
|
|
|
|
|
|
// ❌ BAD: No assertions - test passes even if code is broken
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testNoAssertions() {
|
|
|
|
|
|
Account acc = new Account(Name = 'Test');
|
|
|
|
|
|
insert acc;
|
|
|
|
|
|
// Test "passes" but proves nothing
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ GOOD: Meaningful assertions
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testWithAssertions() {
|
|
|
|
|
|
Account acc = new Account(Name = 'Test');
|
|
|
|
|
|
insert acc;
|
|
|
|
|
|
Assert.isNotNull(acc.Id, 'Account should have an Id after insert');
|
|
|
|
|
|
Account inserted = [SELECT Name FROM Account WHERE Id = :acc.Id];
|
|
|
|
|
|
Assert.areEqual('Test', inserted.Name, 'Name should be preserved');
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-03 13:25:02 +08:00
|
|
|
|
### Hardcoded IDs
|
2026-06-26 20:21:51 +08:00
|
|
|
|
|
|
|
|
|
|
```apex
|
|
|
|
|
|
// ❌ BAD: Hardcoded IDs fail across orgs
|
|
|
|
|
|
Account acc = [SELECT Id FROM Account WHERE Id = '001xx0000000001'];
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ GOOD: Query or create dynamically
|
|
|
|
|
|
Account acc = TestDataFactory.createAndInsertAccounts(1)[0];
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Test Method Template
|
|
|
|
|
|
|
|
|
|
|
|
```apex
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @description Tests [method] with [condition] expecting [outcome]
|
|
|
|
|
|
*/
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void test[Method]_[Condition]_[ExpectedOutcome]() {
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
// ARRANGE - Set up test data and conditions
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
// Create test data using TestDataFactory
|
|
|
|
|
|
// Set up any mocks needed
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
// ACT - Execute the code under test
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
Test.startTest();
|
|
|
|
|
|
// Call the method being tested
|
|
|
|
|
|
Test.stopTest();
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
// ASSERT - Verify expected outcomes
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
// Assert.areEqual(expected, actual, 'Descriptive message');
|
|
|
|
|
|
// Assert.isTrue(condition, 'Descriptive message');
|
|
|
|
|
|
// Assert.isNotNull(value, 'Descriptive message');
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Test Speed Philosophy
|
|
|
|
|
|
|
|
|
|
|
|
### Why Test Speed Matters
|
|
|
|
|
|
|
|
|
|
|
|
Fast tests enable continuous integration. Slow tests become barriers to frequent commits.
|
|
|
|
|
|
|
|
|
|
|
|
### Target Metrics
|
|
|
|
|
|
|
|
|
|
|
|
| Test Type | Target Speed | Purpose |
|
|
|
|
|
|
|-----------|--------------|---------|
|
|
|
|
|
|
| Unit test (no DML) | < 50ms | Test pure business logic |
|
|
|
|
|
|
| Unit test (mocked DML) | < 100ms | Test with stubbed database |
|
|
|
|
|
|
| Integration test | < 500ms | Verify real database behavior |
|
|
|
|
|
|
| Full scenario test | < 2000ms | End-to-end validation |
|
|
|
|
|
|
|
|
|
|
|
|
### Speed Strategy
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
|
|
|
|
│ INTEGRATION TESTS (Few) │
|
|
|
|
|
|
│ - Real DML, real triggers │
|
|
|
|
|
|
│ - Test happy path completely │
|
|
|
|
|
|
│ - Slow but high confidence │
|
|
|
|
|
|
└─────────────────────────────────────────────────────────────┘
|
|
|
|
|
|
│
|
|
|
|
|
|
▼
|
|
|
|
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
|
|
|
|
│ UNIT TESTS WITH MOCKS (Many) │
|
|
|
|
|
|
│ - Stub database operations │
|
|
|
|
|
|
│ - Test edge cases, variations │
|
|
|
|
|
|
│ - Fast, run on every save │
|
|
|
|
|
|
└─────────────────────────────────────────────────────────────┘
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Balance Reality vs Speed
|
|
|
|
|
|
|
|
|
|
|
|
```apex
|
|
|
|
|
|
// INTEGRATION TEST: Full database interaction (slower, realistic)
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testAccountCreation_Integration() {
|
|
|
|
|
|
Test.startTest();
|
|
|
|
|
|
Account acc = new Account(Name = 'Test');
|
|
|
|
|
|
insert acc;
|
|
|
|
|
|
Test.stopTest();
|
|
|
|
|
|
|
|
|
|
|
|
Account queried = [SELECT Name, Status__c FROM Account WHERE Id = :acc.Id];
|
|
|
|
|
|
Assert.areEqual('Active', queried.Status__c, 'Trigger should set status');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UNIT TEST: Mocked database (faster, tests logic only)
|
|
|
|
|
|
@IsTest
|
|
|
|
|
|
static void testAccountRules_IsHighValue() {
|
|
|
|
|
|
// No database interaction - tests pure logic
|
|
|
|
|
|
Account acc = new Account(AnnualRevenue = 1500000);
|
|
|
|
|
|
Assert.isTrue(AccountRules.isHighValue(acc), 'Should be high value');
|
|
|
|
|
|
|
|
|
|
|
|
Account lowValue = new Account(AnnualRevenue = 500000);
|
|
|
|
|
|
Assert.isFalse(AccountRules.isHighValue(lowValue), 'Should not be high value');
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Techniques for Faster Tests
|
|
|
|
|
|
|
|
|
|
|
|
| Technique | Benefit | Trade-off |
|
|
|
|
|
|
|-----------|---------|-----------|
|
|
|
|
|
|
| @TestSetup | Reuse data across methods | Small setup overhead |
|
|
|
|
|
|
| Stub API | No real DML | Less realistic |
|
|
|
|
|
|
| Selector mocking | Skip SOQL | Must trust Selector |
|
|
|
|
|
|
| Domain class testing | Pure logic, no DB | Limited scope |
|
|
|
|
|
|
| Avoid SeeAllData | Predictable data | Must create test data |
|
|
|
|
|
|
|
|
|
|
|
|
### When to Prioritize Speed vs Reality
|
|
|
|
|
|
|
|
|
|
|
|
| Scenario | Priority | Approach |
|
|
|
|
|
|
|----------|----------|----------|
|
|
|
|
|
|
| Business logic validation | Speed | Unit test with mocks |
|
|
|
|
|
|
| Trigger behavior | Reality | Integration test |
|
|
|
|
|
|
| Edge case coverage | Speed | Many unit tests |
|
|
|
|
|
|
| Deployment validation | Reality | RunLocalTests |
|
|
|
|
|
|
| Developer feedback loop | Speed | Fast unit tests |
|
|
|
|
|
|
|
|
|
|
|
|
### Measuring Test Speed
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# Run tests and view timing
|
|
|
|
|
|
sf apex run test --test-level RunLocalTests --target-org alias --result-format human
|
|
|
|
|
|
|
|
|
|
|
|
# Check individual test timing
|
|
|
|
|
|
sf apex get test --test-run-id [id] --code-coverage --result-format json | jq '.tests[] | {name: .MethodName, time: .RunTime}'
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Fast Test Checklist
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
□ Is there a pure logic portion that can be unit tested?
|
|
|
|
|
|
□ Can database operations be mocked for edge cases?
|
|
|
|
|
|
□ Is @TestSetup reused across multiple test methods?
|
|
|
|
|
|
□ Are tests avoiding SeeAllData=true?
|
|
|
|
|
|
□ Are integration tests reserved for critical paths only?
|
|
|
|
|
|
□ Can SOQL be mocked via Selector pattern?
|
|
|
|
|
|
```
|