
Apex Test Class
- 16 installs
- 787 repo stars
- Updated August 5, 2026
- forcedotcom/afv-library
Generates Apex test classes with TestDataFactory patterns, bulk testing of 200+ records, callout mocking, and meaningful assertions.
About
Produces Apex test classes using @TestSetup, TestDataFactory, bulk data, HttpCalloutMock, and negative-path assertions. A developer uses it when creating or improving test coverage for Apex triggers, services, controllers, batch jobs, and integrations.
- Bulkifies tests with 200+ records and isolated TestDataFactory data
- Mocks callouts with HttpCalloutMock and tests negative paths
Apex Test Class by the numbers
- 16 all-time installs (skills.sh)
- Ranked #1,468 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/forcedotcom/afv-library --skill apex-test-classAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 16 |
|---|---|
| repo stars | ★ 787 |
| Last updated | August 5, 2026 |
| Repository | forcedotcom/afv-library ↗ |
What it does
Generates Apex test classes with TestDataFactory patterns, bulk testing of 200+ records, callout mocking, and meaningful assertions.
Files
Apex Test Class Skill
Core Principles
1. Bulkify tests - Always test with 200+ records to catch governor limit issues 2. Isolate test data - Use @TestSetup and TestDataFactory; never rely on org data 3. Assert meaningfully - Test behavior, not just coverage; include failure messages 4. Mock external dependencies - Use HttpCalloutMock, Test.setMock() for integrations 5. Test negative paths - Validate error handling, not just happy paths
Test Class Structure
@IsTest
private class MyServiceTest {
@TestSetup
static void setupTestData() {
// Create shared test data using TestDataFactory
List<Account> accounts = TestDataFactory.createAccounts(200, true);
}
@IsTest
static void shouldPerformExpectedBehavior_WhenValidInput() {
// Given: Setup specific test state
List<Account> accounts = [SELECT Id, Name FROM Account];
// When: Execute the code under test
Test.startTest();
MyService.processAccounts(accounts);
Test.stopTest();
// Then: Assert expected outcomes
List<Account> updated = [SELECT Id, Status__c FROM Account];
System.Assert.areEqual(200, updated.size(), 'All accounts should be processed');
for (Account acc : updated) {
System.Assert.areEqual('Processed', acc.Status__c, 'Status should be updated');
}
}
@IsTest
static void shouldThrowException_WhenInvalidInput() {
// Given
List<Account> emptyList = new List<Account>();
// When/Then
Test.startTest();
try {
MyService.processAccounts(emptyList);
System.Assert.fail('Expected MyCustomException to be thrown');
} catch (MyCustomException e) {
System.Assert.isTrue(e.getMessage().contains('cannot be empty'),
'Exception message should indicate empty input');
}
Test.stopTest();
}
}Naming Convention
Use descriptive method names: should[ExpectedBehavior]_When[Condition]
Examples:
shouldCreateContact_WhenAccountIsActiveshouldThrowException_WhenEmailIsInvalidshouldSendNotification_WhenOpportunityClosedWonshouldBypassTrigger_WhenRunningAsBatch
Test.startTest() / Test.stopTest()
Always wrap the code under test:
- Resets governor limits for accurate limit testing
- Executes async operations synchronously (queueables, batch, future)
- Fires scheduled jobs immediately
Reference Files
Detailed patterns for specific scenarios:
- [references/test-data-factory.md](references/test-data-factory.md) - TestDataFactory class patterns and field defaults
- [references/assertion-patterns.md](references/assertion-patterns.md) - Assertion best practices and common pitfalls
- [references/mocking-patterns.md](references/mocking-patterns.md) - HttpCalloutMock, Test.setMock(), stubbing
- [references/async-testing.md](references/async-testing.md) - Batch, Queueable, Future, Scheduled job testing
Quick Reference: What to Test
| Component | Key Test Scenarios |
|---|---|
| Trigger | Bulk insert/update/delete, recursion, field changes |
| Service | Valid/invalid inputs, bulk operations, exceptions |
| Controller | Page load, action methods, view state |
| Batch | Start/execute/finish, chunking, error records |
| Queueable | Chaining, bulkification, error handling |
| Callout | Success response, error response, timeout |
| Scheduled | Execution, CRON validation |
Assertion Patterns
Assertion Methods
The System.Assert class provides methods to assert various conditions in test methods. All methods support an optional message parameter for better error reporting.
| Method | Use Case |
|---|---|
System.Assert.areEqual(expected, actual, msg) | Exact equality |
System.Assert.areNotEqual(notExpected, actual, msg) | Value should differ |
System.Assert.isTrue(condition, msg) | Boolean condition is true |
System.Assert.isFalse(condition, msg) | Boolean condition is false |
System.Assert.isNull(value, msg) | Value is null |
System.Assert.isNotNull(value, msg) | Value is not null |
System.Assert.isInstanceOfType(instance, expectedType, msg) | Instance is of specified type |
System.Assert.isNotInstanceOfType(instance, notExpectedType, msg) | Instance is not of specified type |
System.Assert.fail(msg) | Explicitly fail the test |
Always include the message parameter - Makes test failures meaningful and easier to debug.
Note: Assertion failures are fatal errors that halt code execution. You cannot catch assertion failures using try/catch blocks, even though they're logged as exceptions.
Note: Call startTest() and stopTest() only once per test method. Wrap only the code under test between these calls, not setup or verification code.
Good vs Bad Assertions
❌ Bad: No message, tests coverage not behavior
System.Assert.areEqual(true, result);
System.Assert.isTrue(accounts.size() > 0);✅ Good: Descriptive message, tests specific behavior
System.Assert.areEqual(true, result, 'Service should return true for valid input');
System.Assert.areEqual(200, accounts.size(), 'All 200 accounts should be processed');Common Assertion Patterns
Collection Size
// Exact count
System.Assert.areEqual(200, results.size(), 'Should process all 200 records');
// Not empty
System.Assert.isFalse(results.isEmpty(), 'Results should not be empty');
// Empty
System.Assert.isTrue(results.isEmpty(), 'No results expected for invalid input');Field Values
// Single record
System.Assert.areEqual('Processed', acc.Status__c, 'Account status should be updated to Processed');
// All records in collection
for (Account acc : updatedAccounts) {
System.Assert.areEqual('Active', acc.Status__c,
'Account ' + acc.Name + ' should have Active status');
}Exception Testing
@IsTest
private static void shouldThrowException_WhenInputInvalid() {
Boolean exceptionThrown = false;
String exceptionMessage = '';
Test.startTest();
try {
MyService.process(null);
} catch (MyCustomException e) {
exceptionThrown = true;
exceptionMessage = e.getMessage();
}
Test.stopTest();
System.Assert.isTrue(exceptionThrown, 'MyCustomException should be thrown for null input');
System.Assert.isTrue(exceptionMessage.contains('cannot be null'),
'Exception message should mention null input');
}DML Results
// Insert success
Database.SaveResult[] results = Database.insert(accounts, false);
for (Database.SaveResult sr : results) {
System.Assert.isTrue(sr.isSuccess(), 'Insert should succeed: ' + sr.getErrors());
}
// Expected failures
Database.SaveResult sr = Database.insert(invalidAccount, false);
System.Assert.isFalse(sr.isSuccess(), 'Insert should fail for invalid data');
System.Assert.isTrue(sr.getErrors()[0].getMessage().contains('REQUIRED_FIELD_MISSING'),
'Error should indicate missing required field');Comparing Objects
// Compare specific fields, not entire objects
System.Assert.areEqual(expected.Name, actual.Name, 'Names should match');
System.Assert.areEqual(expected.Status__c, actual.Status__c, 'Status should match');
// Or use JSON for deep comparison (use sparingly)
System.Assert.areEqual(
JSON.serialize(expected),
JSON.serialize(actual),
'Objects should be identical'
);Date/DateTime Assertions
// Exact date
System.Assert.areEqual(Date.today(), record.CreatedDate__c, 'Should be created today');
// Date within range
System.Assert.isTrue(record.DueDate__c >= Date.today(), 'Due date should be in the future');
System.Assert.isTrue(record.DueDate__c <= Date.today().addDays(30),
'Due date should be within 30 days');Null Checks
// Should be null
System.Assert.isNull(result.ErrorMessage__c, 'No error expected for valid input');
// Should not be null
System.Assert.isNotNull(result.Id, 'Record should have been inserted');Type Checking
// Verify instance is of expected type
Object result = MyService.processData();
System.Assert.isInstanceOfType(result, MyCustomClass.class,
'Result should be an instance of MyCustomClass');
// Verify instance is not of a specific type
Object handler = HandlerFactory.create('Account');
System.Assert.isNotInstanceOfType(handler, ContactHandler.class,
'Account handler should not be a ContactHandler');Explicit Test Failures
// Use Assert.fail() when an exception should have been thrown but wasn't
@IsTest
private static void shouldThrowException_WhenInputInvalid() {
try {
MyService.process(null);
System.Assert.fail('Expected MyCustomException to be thrown for null input');
} catch (MyCustomException e) {
// Exception was thrown as expected, test passes
System.Assert.isTrue(e.getMessage().contains('cannot be null'),
'Exception message should mention null input');
}
}Anti-Patterns to Avoid
❌ Testing implementation, not behavior
// Bad: Testing that a specific method was called
System.Assert.isTrue(MyClass.methodWasCalled, 'Method should be called');
// Good: Testing the observable outcome
System.Assert.areEqual('Expected Value', record.Field__c, 'Field should be updated');❌ Overly generic assertions
// Bad: Passes for any non-empty result
System.Assert.isTrue(results.size() > 0);
// Good: Verifies exact expected count
System.Assert.areEqual(200, results.size(), 'All 200 records should be returned');❌ Missing negative test assertions
// Bad: Only tests that no exception occurred
MyService.process(data); // Test passes if no exception
// Good: Verifies the actual outcome
Result r = MyService.process(data);
System.Assert.areEqual('Success', r.status, 'Processing should succeed');
System.Assert.areEqual(0, r.errorCount, 'No errors should occur');Async Testing Patterns
Key Principle
Test.stopTest() forces all async operations to execute synchronously, allowing assertions on their results.
Batch Apex Testing
Basic Batch Test
@IsTest
private static void shouldProcessAllRecords_WhenBatchExecutes() {
// Given: Create test data
List<Account> accounts = TestDataFactory.createAccounts(200, true);
// When: Execute batch
Test.startTest();
MyBatchClass batch = new MyBatchClass();
Id batchId = Database.executeBatch(batch, 200);
Test.stopTest(); // Forces batch to complete
// Then: Verify results
List<Account> updated = [SELECT Id, Status__c FROM Account];
for (Account acc : updated) {
System.Assert.areEqual('Processed', acc.Status__c,
'Batch should update all account statuses');
}
}Testing Batch with Failures
@IsTest
private static void shouldLogErrors_WhenRecordsFail() {
// Given: Create mix of valid and invalid records
List<Account> accounts = TestDataFactory.createAccounts(198, true);
// Create 2 accounts that will fail processing
List<Account> invalidAccounts = new List<Account>();
for (Integer i = 0; i < 2; i++) {
invalidAccounts.add(new Account(
Name = 'Invalid Account ' + i,
Invalid_Field__c = 'triggers_validation_error'
));
}
insert invalidAccounts;
// When
Test.startTest();
MyBatchClass batch = new MyBatchClass();
Database.executeBatch(batch, 50);
Test.stopTest();
// Then
List<Error_Log__c> errors = [SELECT Id, Message__c FROM Error_Log__c];
System.Assert.areEqual(2, errors.size(), 'Should log 2 failed records');
}Testing Batch Scope
@IsTest
private static void shouldRespectBatchSize() {
// Given
List<Account> accounts = TestDataFactory.createAccounts(250, true);
Test.startTest();
MyBatchClass batch = new MyBatchClass();
Database.executeBatch(batch, 50); // 5 batches of 50
Test.stopTest();
// Note: In tests, all batches execute but you can verify total processing
List<Account> processed = [SELECT Id FROM Account WHERE Processed__c = true];
System.Assert.areEqual(250, processed.size(), 'All records should be processed');
}Queueable Testing
Basic Queueable Test
@IsTest
private static void shouldCompleteProcessing_WhenQueueableEnqueued() {
// Given
Account acc = TestDataFactory.createAccount(true);
// When
Test.startTest();
MyQueueableClass queueable = new MyQueueableClass(acc.Id);
System.enqueueJob(queueable);
Test.stopTest(); // Forces queueable to complete
// Then
Account updated = [SELECT Id, Status__c FROM Account WHERE Id = :acc.Id];
System.Assert.areEqual('Processed', updated.Status__c,
'Queueable should update account status');
}Testing Queueable Chaining
Chained queueables only execute the first job in tests:
@IsTest
private static void shouldChainNextJob_WhenMoreRecordsExist() {
// Given: More records than one queueable can process
List<Account> accounts = TestDataFactory.createAccounts(500, true);
Test.startTest();
// First queueable processes batch 1 and chains next
MyChainedQueueable queueable = new MyChainedQueueable(0, 100);
System.enqueueJob(queueable);
Test.stopTest();
// Verify first batch processed
List<Account> processed = [SELECT Id FROM Account WHERE Processed__c = true];
System.Assert.areEqual(100, processed.size(), 'First batch should process 100 records');
// Verify chain was enqueued (check AsyncApexJob)
List<AsyncApexJob> jobs = [
SELECT Id, Status, JobType
FROM AsyncApexJob
WHERE ApexClass.Name = 'MyChainedQueueable'
];
System.Assert.isTrue(jobs.size() >= 1, 'Chained job should be enqueued');
}Testing Queueable with Callouts
@IsTest
private static void shouldMakeCallout_WhenQueueableWithCallout() {
// Given
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(200, '{"status":"ok"}'));
Account acc = TestDataFactory.createAccount(true);
// When
Test.startTest();
MyQueueableWithCallout queueable = new MyQueueableWithCallout(acc.Id);
System.enqueueJob(queueable);
Test.stopTest();
// Then
Account updated = [SELECT Id, External_Status__c FROM Account WHERE Id = :acc.Id];
System.Assert.areEqual('Synced', updated.External_Status__c,
'Should update status after successful callout');
}Future Method Testing
@IsTest
private static void shouldExecuteFutureMethod() {
// Given
Account acc = TestDataFactory.createAccount(true);
// When
Test.startTest();
MyClass.processFuture(acc.Id); // @future method
Test.stopTest(); // Forces future to complete
// Then
Account updated = [SELECT Id, Processed__c FROM Account WHERE Id = :acc.Id];
System.Assert.areEqual(true, updated.Processed__c, 'Future should process record');
}Scheduled Apex Testing
Testing Scheduled Execution
@IsTest
private static void shouldExecuteScheduledJob() {
// Given
List<Account> accounts = TestDataFactory.createAccounts(50, true);
// When
Test.startTest();
String cronExp = '0 0 0 1 1 ? 2099'; // Arbitrary future time
String jobId = System.schedule('Test Job', cronExp, new MyScheduledClass());
// Execute the scheduled job immediately
MyScheduledClass scheduled = new MyScheduledClass();
scheduled.execute(null); // Pass null SchedulableContext in tests
Test.stopTest();
// Then
List<Account> processed = [SELECT Id FROM Account WHERE Processed__c = true];
System.Assert.areEqual(50, processed.size(), 'Scheduled job should process records');
}Testing Schedule Registration
@IsTest
private static void shouldScheduleJob() {
Test.startTest();
String cronExp = '0 0 6 * * ?'; // Daily at 6 AM
String jobId = System.schedule('Daily Processing', cronExp, new MyScheduledClass());
Test.stopTest();
// Verify job is scheduled
CronTrigger ct = [
SELECT Id, CronExpression, State
FROM CronTrigger
WHERE Id = :jobId
];
System.Assert.areEqual('0 0 6 * * ?', ct.CronExpression, 'CRON should match');
System.Assert.areEqual('WAITING', ct.State, 'Job should be waiting');
}Testing Async Limits
@IsTest
private static void shouldNotExceedQueueableLimits() {
// Given: Setup that might enqueue multiple jobs
List<Account> accounts = TestDataFactory.createAccounts(100, true);
Test.startTest();
Integer queueablesBefore = Limits.getQueueableJobs();
MyService.processWithQueueables(accounts);
Integer queueablesUsed = Limits.getQueueableJobs() - queueablesBefore;
Test.stopTest();
// Verify limit not exceeded (50 in synchronous context, 1 in queueable)
System.Assert.isTrue(queueablesUsed <= 50,
'Should not exceed queueable limit. Used: ' + queueablesUsed);
}Common Pitfalls
❌ Forgetting Test.stopTest()
// Bad: Async never executes
Test.startTest();
System.enqueueJob(new MyQueueable());
// Missing Test.stopTest()!
List<Account> results = [SELECT Id FROM Account WHERE Processed__c = true];
System.Assert.areEqual(100, results.size()); // FAILS - queueable didn't run❌ Testing chained jobs without understanding limits
// Only the FIRST chained queueable runs in tests
// Design tests to verify:
// 1. First job completes correctly
// 2. Chain is properly enqueued (check AsyncApexJob)
// 3. Each job works independently❌ Not mocking callouts in async
// Async with callouts MUST have mock set BEFORE Test.startTest()
Test.setMock(HttpCalloutMock.class, new MockResponse()); // Before startTest!
Test.startTest();
System.enqueueJob(new QueueableWithCallout());
Test.stopTest();Mocking Patterns
HTTP Callout Mocking
Apex doesn't allow real HTTP callouts in tests. Use HttpCalloutMock interface.
Basic Mock Implementation
@IsTest
public class MockHttpResponse implements HttpCalloutMock {
private Integer statusCode;
private String body;
public MockHttpResponse(Integer statusCode, String body) {
this.statusCode = statusCode;
this.body = body;
}
public HTTPResponse respond(HTTPRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(statusCode);
res.setBody(body);
res.setHeader('Content-Type', 'application/json');
return res;
}
}Using the Mock
@IsTest
private static void shouldProcessApiResponse_WhenCalloutSucceeds() {
// Given
String mockResponse = '{"status": "success", "data": [{"id": "123"}]}';
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(200, mockResponse));
// When
Test.startTest();
List<ExternalRecord> results = MyIntegrationService.fetchRecords();
Test.stopTest();
// Then
System.Assert.areEqual(1, results.size(), 'Should parse one record from response');
System.Assert.areEqual('123', results[0].externalId, 'Should extract correct ID');
}
@IsTest
private static void shouldHandleError_WhenCalloutFails() {
// Given
String errorResponse = '{"error": "Unauthorized"}';
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(401, errorResponse));
// When
Test.startTest();
CalloutResult result = MyIntegrationService.fetchRecords();
Test.stopTest();
// Then
System.Assert.areEqual(false, result.isSuccess, 'Should indicate failure');
System.Assert.isTrue(result.errorMessage.contains('Unauthorized'), 'Should capture error');
}Multi-Request Mock
For services making multiple callouts:
@IsTest
public class MultiRequestMock implements HttpCalloutMock {
private Map<String, HttpResponse> endpointResponses;
public MultiRequestMock(Map<String, HttpResponse> responses) {
this.endpointResponses = responses;
}
public HTTPResponse respond(HTTPRequest req) {
String endpoint = req.getEndpoint();
for (String key : endpointResponses.keySet()) {
if (endpoint.contains(key)) {
return endpointResponses.get(key);
}
}
// Default 404 if no match
HttpResponse res = new HttpResponse();
res.setStatusCode(404);
res.setBody('{"error": "Not found"}');
return res;
}
}
// Usage:
Map<String, HttpResponse> mocks = new Map<String, HttpResponse>();
HttpResponse authResponse = new HttpResponse();
authResponse.setStatusCode(200);
authResponse.setBody('{"token": "abc123"}');
mocks.put('/oauth/token', authResponse);
HttpResponse dataResponse = new HttpResponse();
dataResponse.setStatusCode(200);
dataResponse.setBody('{"records": []}');
mocks.put('/api/records', dataResponse);
Test.setMock(HttpCalloutMock.class, new MultiRequestMock(mocks));StaticResourceCalloutMock
For complex response bodies, store JSON in Static Resources:
@IsTest
private static void shouldParseComplexResponse() {
StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
mock.setStaticResource('TestApiResponse'); // Static Resource name
mock.setStatusCode(200);
mock.setHeader('Content-Type', 'application/json');
Test.setMock(HttpCalloutMock.class, mock);
Test.startTest();
Result r = MyService.callExternalApi();
Test.stopTest();
System.Assert.isNotNull(r, 'Should parse response');
}Stub API (Enterprise Pattern)
For mocking Apex class dependencies using System.StubProvider:
@IsTest
public class MyServiceMock implements System.StubProvider {
public Object handleMethodCall(
Object stubbedObject,
String stubbedMethodName,
Type returnType,
List<Type> paramTypes,
List<String> paramNames,
List<Object> args
) {
if (stubbedMethodName == 'getAccountData') {
return new AccountData('Mock Account', 'Active');
}
return null;
}
}
// Usage in test:
@IsTest
private static void shouldUseAccountData() {
MyServiceMock mockProvider = new MyServiceMock();
IMyService mockService = (IMyService)Test.createStub(IMyService.class, mockProvider);
// Inject mock into class under test
MyController controller = new MyController(mockService);
Test.startTest();
String result = controller.displayAccountInfo();
Test.stopTest();
System.Assert.isTrue(result.contains('Mock Account'), 'Should use mocked data');
}Email Mocking
Apex sends real emails by default. Use limits to verify:
@IsTest
private static void shouldSendEmail_WhenTriggered() {
Integer emailsBefore = Limits.getEmailInvocations();
Test.startTest();
MyService.sendNotification(testContact);
Test.stopTest();
// Verify email was queued (not actually sent in tests)
System.Assert.areEqual(
emailsBefore + 1,
Limits.getEmailInvocations(),
'One email should be sent'
);
}Platform Event Testing
@IsTest
private static void shouldPublishEvent_WhenRecordCreated() {
Test.startTest();
// Enable event delivery in test context
Test.enableChangeDataCapture();
Account acc = TestDataFactory.createAccount(true);
// Deliver events
Test.getEventBus().deliver();
Test.stopTest();
// Query platform event trigger results
List<EventLog__c> logs = [SELECT Id FROM EventLog__c WHERE AccountId__c = :acc.Id];
System.Assert.areEqual(1, logs.size(), 'Event handler should create log record');
}TestDataFactory Patterns
Overview
TestDataFactory is a centralized utility class for creating test records with sensible defaults. It ensures consistent test data across all test classes and reduces duplication.
Base Template
@IsTest
public class TestDataFactory {
// ============ ACCOUNTS ============
public static List<Account> createAccounts(Integer count, Boolean doInsert) {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < count; i++) {
accounts.add(new Account(
Name = 'Test Account ' + i,
BillingStreet = '123 Test St',
BillingCity = 'San Francisco',
BillingState = 'CA',
BillingPostalCode = '94105',
BillingCountry = 'USA',
Industry = 'Technology',
Type = 'Customer'
));
}
if (doInsert) insert accounts;
return accounts;
}
public static Account createAccount(Boolean doInsert) {
return createAccounts(1, doInsert)[0];
}
// ============ CONTACTS ============
public static List<Contact> createContacts(List<Account> accounts, Integer countPerAccount, Boolean doInsert) {
List<Contact> contacts = new List<Contact>();
Integer index = 0;
for (Account acc : accounts) {
for (Integer i = 0; i < countPerAccount; i++) {
contacts.add(new Contact(
FirstName = 'Test',
LastName = 'Contact ' + index,
Email = 'test.contact' + index + '@example.com',
Phone = '555-000-' + String.valueOf(index).leftPad(4, '0'),
AccountId = acc.Id
));
index++;
}
}
if (doInsert) insert contacts;
return contacts;
}
// ============ OPPORTUNITIES ============
public static List<Opportunity> createOpportunities(List<Account> accounts, Integer countPerAccount, Boolean doInsert) {
List<Opportunity> opps = new List<Opportunity>();
Integer index = 0;
for (Account acc : accounts) {
for (Integer i = 0; i < countPerAccount; i++) {
opps.add(new Opportunity(
Name = 'Test Opportunity ' + index,
AccountId = acc.Id,
StageName = 'Prospecting',
CloseDate = Date.today().addDays(30),
Amount = 10000 + (index * 1000)
));
index++;
}
}
if (doInsert) insert opps;
return opps;
}
// ============ USERS ============
public static User createUser(String profileName, Boolean doInsert) {
Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
String uniqueKey = String.valueOf(DateTime.now().getTime());
User u = new User(
FirstName = 'Test',
LastName = 'User ' + uniqueKey,
Email = 'testuser' + uniqueKey + '@example.com',
Username = 'testuser' + uniqueKey + '@example.com.test',
Alias = 'tuser',
TimeZoneSidKey = 'America/Los_Angeles',
LocaleSidKey = 'en_US',
EmailEncodingKey = 'UTF-8',
LanguageLocaleKey = 'en_US',
ProfileId = p.Id
);
if (doInsert) insert u;
return u;
}
// ============ CUSTOM OBJECTS ============
// Add methods for your custom objects following the same pattern:
// public static List<MyObject__c> createMyObjects(Integer count, Boolean doInsert) { ... }
}Field Override Pattern
Allow callers to override default values:
public static Account createAccount(Map<String, Object> fieldOverrides, Boolean doInsert) {
Account acc = new Account(
Name = 'Test Account',
Industry = 'Technology'
);
// Apply overrides
for (String fieldName : fieldOverrides.keySet()) {
acc.put(fieldName, fieldOverrides.get(fieldName));
}
if (doInsert) insert acc;
return acc;
}
// Usage:
Account acc = TestDataFactory.createAccount(new Map<String, Object>{
'Name' => 'Custom Name',
'Industry' => 'Healthcare'
}, true);Handling Required Fields and Validation Rules
public static Account createAccountWithRequiredFields(Boolean doInsert) {
Account acc = new Account(
Name = 'Test Account',
// Required custom fields
External_Id__c = 'EXT-' + String.valueOf(DateTime.now().getTime()),
// Fields required by validation rules
Phone = '555-123-4567',
Website = 'https://example.com'
);
if (doInsert) insert acc;
return acc;
}Record Type Support
public static Account createAccountByRecordType(String recordTypeName, Boolean doInsert) {
Id recordTypeId = Schema.SObjectType.Account
.getRecordTypeInfosByDeveloperName()
.get(recordTypeName)
.getRecordTypeId();
Account acc = new Account(
Name = 'Test Account',
RecordTypeId = recordTypeId
);
if (doInsert) insert acc;
return acc;
}Best Practices
1. Always include doInsert parameter - Allows flexibility for tests that need to modify records before insert 2. Use unique identifiers - Include index or timestamp in Name/Email fields to avoid duplicates 3. Set all required fields - Include all fields required by validation rules 4. Return the created records - Enables chaining and further manipulation 5. Create bulk methods first - Single record methods should call bulk methods with count=1