Test Data - Guide
Framework: KernDX Package Type: Managed Package
Target Audience:
- Developers - Building test records, relationships, mock Ids, and bulk data for Apex unit tests
- Architects - Designing a consistent, low-boilerplate test-data strategy across a team
- Business Analysts - Understanding what the test-data layer guarantees and where its limits are
Table of Contents
Expand
- Quick Navigation
- Overview
- Quick Start
- Escape Hatches
- Architecture
- Building Records
- Bulk Creation
- Insertion Modes
- Relationships
- The Factory Pattern
- Extending the Defaults
- Capability Matrix (for Analysts)
- Anti-Patterns
- Best Practices
- Override Only What Matters
- Prefer In-Memory Builds for Pure Logic
- Bulk With One DML
- Use Mock Ids Only When the Code Needs an Id
- Build Relationships With withChildren
- Prefer Token Overloads in Hand-Written Tests
- Centralise Shared Setup in TST_Factory
- Assert on User-Facing Text Through Custom Labels
- Let Salesforce Reset Static State
- Related Documentation
Quick Navigation
| I am a... | I need to... | Go to... |
|---|---|---|
| Developer | Build one record fast | Quick Start |
| Developer | Build records in bulk | Bulk Creation |
| Developer | Build records with no DML | Insertion Modes |
| Developer | Build parent-child graphs | Relationships |
| Architect | Standardise team test data | The Factory Pattern |
| Architect | Customise auto-populated defaults | Extending the Defaults |
| Analyst | Understand guarantees and limits | Capability Matrix |
Overview
The Test Data layer creates SObject records for Apex unit tests with the smallest possible amount of boilerplate. You declare the SObject type and override only the fields your test actually cares about; the framework auto-populates the rest of the required fields, inserts the record (or keeps it in memory), and hands it back.
Managed Package Context:
This framework is exposed as global classes in a managed package. When calling these classes from a subscriber org's Apex, use the kern. namespace prefix — for example, kern.TST_Builder and kern.TST_Factory. The framework source is available to clients who build and manage the package themselves.
The layer has two complementary classes:
TST_Builder— Primary tool — a fluent builder for any SObject. Auto-populates required fields, supports overrides, bulk creation, cycling values, parent-child relationships, and three insertion modes.TST_Factory— Convenience layer — pre-configured builders for common records (users, permission set assignments, feature flags) and the framework's own metadata (trigger settings, validation rules, custom metadata).
> Scope: This Guide is the deep reference for the test-data layer. For DML-free query > interception — registering in-memory records so kern.QRY_Builder returns them without touching > the database — see TST_Mock in the Test Data Fast Start. > TST_Mock shares the same fluent surface as TST_Builder and is covered there end to end.
> Responsibilities: This layer only constructs (and optionally inserts) records for tests. It > does not run production business logic, and most of its surface is meant for @IsTest code. > TST_Builder and the TST_Factory methods run from anonymous Apex and from a subscriber > @IsTest class. To set up framework configuration metadata (trigger settings, validation rules, > masking) for your tests, deploy the relevant CustomMetadata records via XML (see > Framework Metadata).
Key Benefits:
- Less boilerplate — override only what matters; required fields are filled in for you.
- Type safety —
SObjectFieldtoken overloads catch field-name typos at compile time. - Bulk in one statement —
withCount(n).buildList()creates many records with a single DML call. - No-DML modes — build in memory, with or without mock Ids, for fast governor-friendly tests.
- Relationships — build parent-child graphs in a single Unit of Work transaction.
- Consistency —
TST_Factorycentralises the setup every test class repeats.
Quick Start
Use TST_Builder directly. No custom class is required.
> Step-by-step walkthrough: the Test Data Fast Start covers > a full build-and-test cycle, plus TST_Mock for DML-free query testing.
// Build and insert a single Account; required fields are auto-populated
Account account = (Account)kern.TST_Builder.of(Account.SObjectType)
.withOverride(Account.Industry, 'Technology')
.build();
System.debug('Id: ' + account.Id + ', Industry: ' + account.Industry);build() returns one record (inserted by default). buildList() returns a List<SObject> and respects withCount(n). Cast the result to your concrete type.
For deeper coverage, continue reading the sections below.
Escape Hatches
The test-data layer is opt-in. When it doesn't fit, standard Apex test data creation is always available — nothing in the framework prevents it.
| You need | Use | See |
|---|---|---|
| A plain record with no auto-population | new Account(Name = 'X') then insert — standard Apex, no wrapper. | — |
| One field that the builder shouldn't default | withOptionalField(field) to skip a required field, or build the record by hand. | Marking Fields Optional |
DML-free query interception (records flow through kern.QRY_Builder) | kern.TST_Mock — same fluent surface as TST_Builder, auto-registers for query mocking. | Test Data Fast Start |
| Aggregate-query test data | Insert real records via TST_Builder — AggregateResult cannot be mocked (platform limitation). | Anti-Patterns |
| Custom default values for a field type | Extend kern.TST_Builder.DefaultValueProvider and assign kern.TST_Builder.defaultValueProvider. | Extending the Defaults |
The builder is a productivity convenience, not a wall. Reach for it when its features pay off; skip it when they don't.
Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ TEST DATA LAYER ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ @IsTest CODE (and anonymous Apex for TST_Builder) │
│ ================================================== │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ TST_Factory (Convenience — pre-configured builders for common records)│ │
│ │ - newUser, createPermissionSetAssignments, newFeatureFlag │ │
│ │ - newContentVersion, newApiCall, newOutboundApiSetting │ │
│ └───────────────────────────────┬───────────────────────────────────────┘ │
│ │ delegates to │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ TST_Builder (Primary — fluent builder for any SObject) │ │
│ │ - of(type).withOverride(...).withCount(n).build()/buildList() │ │
│ │ - auto-populated required fields, cycling, relationships, mock Ids │ │
│ └───────────────────────────────┬───────────────────────────────────────┘ │
│ │ pluggable │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ DefaultValueProvider (extension point for custom field defaults) │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘KernDX vs OOTB: Test Data Comparison
Salesforce has no built-in test-data factory. The standard approach is to hand-construct records:
Account account = new Account(Name = 'Test', Industry = 'Technology');
insert account;| Feature | KernDX Test Data Layer | Hand-Written Test Data |
|---|---|---|
| Required-field population | ✅ Auto-populated; override only what the test needs | ❌ Every required field set by hand on every record |
| Field type safety | ✅ SObjectField token overloads (withOverride(Account.Name, …)) | ✅ Direct field access is compile-checked too |
| Bulk creation | ✅ withCount(n).buildList() — single DML statement | ⚠️ Manual loop; easy to accidentally DML inside the loop |
| Cycling values | ✅ withCycle(field, values) rotates across records | ❌ Manual index arithmetic |
| In-memory / mock Ids | ✅ withoutInsertion() / withoutInsertion(true) | ⚠️ Manual; mock Ids require a separate helper |
| Parent-child graphs | ✅ withChildren(...) — single Unit of Work transaction | ⚠️ Insert parent, set foreign keys, insert children by hand |
| Consistency across a team | ✅ TST_Factory centralises shared setup | ⚠️ Each test class repeats its own boilerplate |
| Simplicity for one field | ⚠️ Builder API to learn | ✅ A single new expression is familiar to everyone |
Use the builder when its features pay off (bulk, relationships, no-DML, shared setup). For a single throwaway record in one test, a plain new expression is perfectly fine.
Layer 1: TST_Builder
Purpose: A fluent builder that constructs and (optionally) inserts SObject records, populating Salesforce-required fields automatically.
When to Use: All SObject construction in tests. Custom validation rules in a subscriber org may require extra withOverride() calls beyond the Salesforce-required fields the builder fills in.
Entry points:
kern.TST_Builder.of(SObjectType)— start a build for a typed SObject.kern.TST_Builder.of(String)— start a build by SObject API name (resolved via describe).
Configuration (all chainable, all return Builder):
withOverride(field, value)/withOverride(fieldName, value)— set one field.withOverrides(Map<SObjectField, Object>)/withOverrides(Map<String, Object>)— set many.withCycle(field, values)/withCycle(fieldName, values)— rotate values acrossbuildList().withDefaultedField(...)/withDefaultedFields(List<Object>)— also default a non-required field.withOptionalField(...)/withOptionalFields(List<Object>)— skip auto-populating a field.withCount(Integer)— number of records forbuildList().withRecordType(String developerName)— set the record type by developer name.withChildren(...)— attach child records (several overloads).withoutInsertion()/withoutInsertion(Boolean)— control DML and mock Ids.
Terminals:
build()— returns a singleSObject.buildList()— returns aList<SObject>ofwithCount(n)records.
Layer 2: TST_Factory
Purpose: Pre-configured builders for records that nearly every test needs, plus helpers for the framework's own configuration metadata.
When to Use: When a record needs setup that's the same across many tests — a User on a given profile, a permission set assignment, an active feature flag — or when you need to wire up framework metadata (trigger settings, validation rules) for a test.
Subscriber-callable highlights:
kern.TST_Factory.newUser(profileName)/newUser(profileName, companyName)— uncommittedUser.kern.TST_Factory.newUsers(profileName, count)— uncommittedUserlist.kern.TST_Factory.newUserWithPermissionSet(profileName, companyName, permissionSetName)— inserts a user and assigns a permission set (or permission set group).kern.TST_Factory.createPermissionSetAssignments(users, permissionSetOrGroupName)— assign a permission set to existing users.kern.TST_Factory.newFeatureFlag(flagName)— register an active feature flag for the transaction.
To set up framework configuration metadata — trigger settings, trigger actions, and validation rules — for a test, deploy the relevant CustomMetadata records via XML (see Framework Metadata).
Building Records
These examples use Account and Contact, but every method works for any SObject — standard or custom.
A Single Record
// Inserted by default; required fields auto-populated
Account account = (Account)kern.TST_Builder.of(Account.SObjectType).build();
System.debug('Name: ' + account.Name); // randomly generated
System.debug('Id: ' + account.Id); // populated because the record was insertedOverriding Fields
Override only the fields the test cares about. Everything else the test doesn't name is either auto-populated (if required) or left null.
Account account = (Account)kern.TST_Builder.of(Account.SObjectType)
.withOverride(Account.Name, 'Acme Corp')
.withOverride(Account.Industry, 'Technology')
.build();
Assert.areEqual('Acme Corp', account.Name, 'Name override should apply');Token Maps vs String Maps
For many overrides at once, use a map. The SObjectField-token overload is compile-checked; the String overload is for field names known only at runtime.
// Type-safe: field-name typos are caught at compile time
Account tokenForm = (Account)kern.TST_Builder.of(Account.SObjectType)
.withOverrides(new Map<SObjectField, Object>
{
Account.Name => 'Acme Corp',
Account.Industry => 'Technology',
Account.AnnualRevenue => 5000000
})
.build();
// String form: useful when the field name is dynamic
Account stringForm = (Account)kern.TST_Builder.of(Account.SObjectType)
.withOverrides(new Map<String, Object>
{
'Name' => 'Beta Corp',
'Industry' => 'Finance'
})
.build();> Prefer the SObjectField token overloads in hand-written tests so the compiler catches renamed or > deleted fields. Reach for the String overloads only when the field name genuinely varies at > runtime.
Building by API Name
When the SObject type itself is dynamic, start from the API name. An invalid name throws an IllegalArgumentException with a descriptive message.
List<SObject> records = kern.TST_Builder.of('Foobar__c')
.withCount(5)
.buildList();Defaulting Optional Fields
By default the builder only populates Salesforce-required fields. To also auto-populate a field that is not required — for example, to exercise code that reads it — list it with withDefaultedField or withDefaultedFields.
Account account = (Account)kern.TST_Builder.of(Account.SObjectType)
.withDefaultedField(Account.Description)
.withDefaultedField(Account.Industry)
.withoutInsertion()
.build();
Assert.isNotNull(account.Description, 'Description should be auto-populated');Marking Fields Optional
The inverse: tell the builder to leave a normally-required field alone. The resulting field is null.
Account account = (Account)kern.TST_Builder.of(Account.SObjectType)
.withOptionalField(Account.Name)
.withoutInsertion()
.build();
Assert.isNull(account.Name, 'Name should be left unpopulated');> withOptionalField only suppresses auto-population. If the field is genuinely required for DML, > inserting the record will still fail — use this with withoutInsertion() or supply the value > another way.
Record Types
Set a record type by its developer name. An unknown developer name throws an IllegalArgumentException; a blank name is ignored.
Account account = (Account)kern.TST_Builder.of(Account.SObjectType)
.withRecordType('Enterprise_Account')
.withOverride(Account.Name, 'Enterprise Corp')
.build();> In a subscriber org a packaged record type may not be assigned to the running profile. If a build > fails with INVALID_CROSS_REFERENCE_KEY on RecordTypeId, confirm the record type is available > to the running user, or pass a blank developer name to fall back to the master record type.
Bulk Creation
withCount and buildList
withCount(n) followed by buildList() creates n records with a single DML statement — critical for staying within governor limits. build() always returns exactly one record regardless of withCount.
List<Account> accounts = (List<Account>)kern.TST_Builder.of(Account.SObjectType)
.withOverride(Account.Industry, 'Technology')
.withCount(200)
.buildList();
Assert.areEqual(200, accounts.size(), 'Should build 200 accounts in one DML statement');Cycling Values Across Records
withCycle(field, values) rotates a list of values across the records produced by buildList(). When there are more records than values, the values repeat from the start. One call covers several scenarios without writing separate test methods. The value list must not be null or empty.
List<SObject> records = kern.TST_Builder.of(Account.SObjectType)
.withCycle(Account.Industry, new List<Object> {'Technology', 'Finance', 'Healthcare'})
.withCount(6)
.buildList();
// Industries: Technology, Finance, Healthcare, Technology, Finance, HealthcareInsertion Modes
Three modes control whether the record hits the database and whether it has an Id.
Insert (Default)
Without withoutInsertion(), records are inserted and come back with real Ids.
Account account = (Account)kern.TST_Builder.of(Account.SObjectType).build();
Assert.isNotNull(account.Id, 'Inserted record has a real Id');In-Memory, No Id
withoutInsertion() builds the record in memory with no DML and no Id. Use it when the code under test works with the record's fields and never needs an Id or a database round-trip — the fastest, most governor-friendly option.
Account account = (Account)kern.TST_Builder.of(Account.SObjectType)
.withOverride(Account.Name, 'In-Memory Co')
.withoutInsertion()
.build();
Assert.isNull(account.Id, 'No insertion means no Id');In-Memory with Mock Ids
withoutInsertion(true) builds in memory but stamps a valid 18-character mock Id. Use it when the code under test checks record.Id != null, uses the Id as a map key, or registers the record for query mocking.
Account account = (Account)kern.TST_Builder.of(Account.SObjectType)
.withOverride(Account.Name, 'Mock Corp')
.withoutInsertion(true)
.build();
Assert.isNotNull(account.Id, 'Mock Id is generated without DML');Choosing a Mode
| Mode | DML | Id | Use when |
|---|---|---|---|
| (default) | ✅ | Real | The test exercises real persistence, queries, triggers, or sharing. |
withoutInsertion() | ❌ | null | The code under test only reads fields — no Id, no database needed. |
withoutInsertion(true) | ❌ | Mock | The code checks Id != null, keys by Id, or registers a query mock. |
> Custom metadata and read-only fields: custom metadata types (*__mdt) cannot be inserted via > DML, so the framework builds them in memory. Read-only and system fields are applied through a > serialize/deserialize step so they appear populated on an in-memory record even though Apex would > normally reject setting them directly.
Relationships
The builder constructs parent-child graphs. With insertion, parents and children commit atomically in a single Unit of Work transaction; without insertion, the child relationship is populated in memory so it reads back like a subquery result.
Adding Child Records
The simplest overload takes a child type and a count — the relationship name is auto-detected from the schema.
Account account = (Account)kern.TST_Builder.of(Account.SObjectType)
.withOverride(Account.Name, 'ACME Corp')
.withChildren(Contact.SObjectType, 3)
.build();
Assert.areEqual(3, account.Contacts.size(), 'Three child contacts should be attached');Add child field overrides with the map overloads (token or string form):
Account account = (Account)kern.TST_Builder.of(Account.SObjectType)
.withOverride(Account.Name, 'ACME Corp')
.withChildren(Contact.SObjectType, 3, new Map<SObjectField, Object>
{
Contact.LastName => 'Mock Contact'
})
.build();Child Builders
For richer child configuration, pass a fully-configured child Builder. The relationship name is auto-detected from the child's SObject type.
Account account = (Account)kern.TST_Builder.of(Account.SObjectType)
.withChildren(
kern.TST_Builder.of(Opportunity.SObjectType)
.withCount(2)
.withOverride(Opportunity.Name, 'Big Deal')
.withOverride(Opportunity.StageName, 'Prospecting')
)
.build();Explicit Relationship Names
When parent and child share more than one relationship (for example, two lookup fields from the same child to the same parent), auto-detection cannot choose for you and throws an IllegalArgumentException listing the candidates. Pass the relationship name explicitly:
Foobar__c foobar = (Foobar__c)kern.TST_Builder.of(Foobar__c.SObjectType)
.withChildren('PrimaryContacts__r',
kern.TST_Builder.of(Contact.SObjectType).withCount(2))
.withChildren('SecondaryContacts__r',
kern.TST_Builder.of(Contact.SObjectType).withCount(1))
.build();Mock Ids Across a Graph
withoutInsertion(true) on the parent generates mock Ids for the entire graph — parents and children — and wires each child's foreign key to its parent's mock Id. This produces a fully-linked in-memory graph with no DML, ready to register for query mocking.
Account account = (Account)kern.TST_Builder.of(Account.SObjectType)
.withOverride(Account.Name, 'Mock Account')
.withChildren(Contact.SObjectType, 3, new Map<SObjectField, Object>
{
Contact.LastName => 'Mock Contact'
})
.withoutInsertion(true)
.build();
Assert.isNotNull(account.Id, 'Parent has a mock Id');
Assert.isNotNull(account.Contacts[0].Id, 'Children have mock Ids');
Assert.areEqual(account.Id, account.Contacts[0].AccountId, 'Child foreign key references the parent');The Factory Pattern
Why a Factory
Some records need the same multi-field setup in test after test: a User on a specific profile, a permission set assignment, an active feature flag, or the framework's own configuration metadata. TST_Factory wraps TST_Builder with pre-configured builders for these cases so the setup lives in one place instead of being copied across every test class.
Users and Permission Sets
newUser returns an uncommitted User (you control insertion). Remove IsParallel=true from the test class header when inserting User records — Salesforce forbids User DML in parallel tests.
@IsTest
private static void shouldRunAsStandardUser()
{
User testUser = kern.TST_Factory.newUser('Standard User');
insert testUser;
System.runAs(testUser)
{
Account account = (Account)kern.TST_Builder.of(Account.SObjectType)
.withOverride(Account.Industry, 'Technology')
.withoutInsertion()
.build();
Assert.areEqual('Technology', account.Industry, 'Industry should be set');
}
}Assign a permission set (or permission set group) to existing users with createPermissionSetAssignments. It throws an AssertException if the named permission set cannot be resolved.
List<User> users = kern.TST_Factory.newUsers('Standard User', 2);
insert users;
kern.TST_Factory.createPermissionSetAssignments(users, 'My_Permission_Set');For a committed admin user that test methods reach through System.runAs, newUserWithPermissionSet composes the user, the insert, and the assignment in one call — ideal inside @TestSetup.
@TestSetup
private static void setupAdmin()
{
kern.TST_Factory.newUserWithPermissionSet(
'System Administrator',
'MyClass_TEST-admin',
'My_Admin_Permission_Set'
);
}Feature Flags
newFeatureFlag(flagName) registers an active feature flag for the current transaction, so kern.UTIL_FeatureFlag.isEnabled(flagName) returns true afterward. No DML is involved — the flag is injected into the framework's in-memory cache.
@IsTest
private static void shouldRunFlagGatedPath()
{
kern.TST_Factory.newFeatureFlag('MyFeatureFlag');
Assert.isTrue(
kern.UTIL_FeatureFlag.isEnabled('MyFeatureFlag'),
'Flag should report enabled after registration'
);
}Framework Metadata: Triggers and Validations
To exercise framework trigger or validation behaviour in your tests, deploy the relevant framework configuration metadata — trigger settings, trigger actions, validation rules, and validation rule groups — as CustomMetadata records via XML. Those records are in place for every test that runs against the org, so a test simply builds a record and asserts on the resulting trigger or validation behaviour.
> Setting up framework metadata for tests. Deploy the TriggerSetting__mdt, > TriggerAction__mdt, ValidationRule__mdt, and related records your tests rely on as > CustomMetadata via XML. See the > Trigger Actions Fast Start and > Custom Validations Fast Start for that pattern.
Once the trigger action metadata is deployed, a test builds a record and asserts that the action fired:
@IsTest
private static void shouldFireTriggerAction()
{
Account account = (Account)kern.TST_Builder.of(Account.SObjectType).build();
Assert.isNotNull(account.Id, 'Record inserted and trigger action should have fired');
}Custom Metadata Records
Framework configuration that depends on custom metadata — masking targets, for example — is set up by deploying the relevant CustomMetadata records as XML alongside your test classes. Custom metadata types can't be inserted via DML, so the records ship with your source and are active for every test run.
Place the records under force-app/main/default/customMetadata/, one file per record:
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Mask Contact Email</label>
<values>
<field>kern__SObjectType__c</field>
<value xsi:type="xsd:string">Contact</value>
</values>
<values>
<field>kern__Rule__c</field>
<value xsi:type="xsd:string">kern__Mask_Email</value>
</values>
</CustomMetadata>Once the records are deployed, a test builds a record normally and asserts on the masked behaviour:
@IsTest
private static void shouldMaskContactEmail()
{
Contact contact = (Contact)kern.TST_Builder.of(Contact.SObjectType)
.withOverride(Contact.Email, 'real.person@example.com')
.build();
Assert.areNotEqual(
'real.person@example.com',
contact.Email,
'Email should be masked according to the deployed masking target');
}Logger Configuration
The logging framework runs inside tests according to your org's configuration, so logs you generate in test context exercise the same code path as production. By default the framework suppresses log records during tests; set the global kern.LOG_Builder.ignoreTestMode flag to true when a test needs to assert on logged output:
kern.LOG_Builder.ignoreTestMode = true;> The flag is reset between test methods along with all other static state, so enabling it in one > test never leaks into another.
Extending the Defaults
Custom Default Value Provider
The values the builder auto-populates come from a default value provider. To change how defaults are generated — for example, to honour an unusual required-field setup — extend kern.TST_Builder.DefaultValueProvider and assign an instance to kern.TST_Builder.defaultValueProvider. It's a global virtual class (not an interface) so the framework can add methods in future releases without breaking your subclass.
public class MyDefaultProvider extends kern.TST_Builder.DefaultValueProvider
{
public override Map<String, kern.TST_Builder.DefaultFieldValueProvider> getDefaultMapOfValues(
SObjectType sObjectType,
Map<String, Object> overrides)
{
Map<String, kern.TST_Builder.DefaultFieldValueProvider> defaults =
super.getDefaultMapOfValues(sObjectType, overrides);
// add or replace field-level default providers here
return defaults;
}
}
// Apply it for the transaction
kern.TST_Builder.defaultValueProvider = new MyDefaultProvider();> A custom provider is set per transaction. Salesforce resets static state between test methods, so > the assignment does not leak into other tests.
Auto-Default Marker
In an override map, the kern.TST_Builder.autoDefaultFieldValueProvider marker asks the builder to generate a default for a field even when you're supplying other fields explicitly:
Account account = (Account)kern.TST_Builder.of(Account.SObjectType)
.withOverrides(new Map<String, Object>
{
'Name' => 'Test Account',
'Description' => kern.TST_Builder.autoDefaultFieldValueProvider
})
.withoutInsertion()
.build();Capability Matrix (for Analysts)
| Capability | How it is expressed | Notes |
|---|---|---|
| Auto-populate required fields | Default behaviour of build() / buildList() | Custom validation rules may require additional overrides |
| Override specific fields | withOverride / withOverrides | Token overloads are compile-checked |
| Bulk creation, single DML | withCount(n).buildList() | Keeps tests within governor limits |
| Rotate values across records | withCycle(field, values) | Repeats the list when records exceed values |
| Build without DML | withoutInsertion() | No Id; fastest path |
| Build without DML, with an Id | withoutInsertion(true) | Valid mock Id for Id != null logic and query mocking |
| Parent-child graphs | withChildren(...) | Single Unit of Work on insert; in-memory linkage without insert |
| Record types | withRecordType(developerName) | Falls back to master when the packaged type is unavailable |
| Common records (users, flags) | TST_Factory methods | Centralises repeated setup |
| Framework metadata for tests | Deploy CustomMetadata records via XML | Sets up trigger and validation behaviour for your tests |
| Customise auto-populated defaults | Extend DefaultValueProvider | Per-transaction; reset between test methods |
Anti-Patterns
| Anti-Pattern | Why It's Wrong | Instead |
|---|---|---|
Calling build() inside a loop | One DML statement per iteration — hits governor limits fast | withCount(n).buildList() — a single DML statement |
| Setting every required field by hand | Verbose and brittle; renamed required fields break silently | Override only what the test asserts on; let the builder fill the rest |
Using withoutInsertion(true) when no Id is needed | Generates a mock Id the test never uses — noise | withoutInsertion() (no argument) for pure in-memory logic tests |
| Trying to mock aggregate queries | AggregateResult has no public constructor — it cannot be instantiated | Insert real records via TST_Builder, then run the aggregate query |
Forgetting IsParallel=true removal when inserting User | Salesforce forbids User DML in parallel tests | Remove IsParallel=true from the class header for user-inserting tests |
| Hardcoding field names as strings in hand-written tests | Breaks silently when fields are renamed or deleted | Use SObjectField token overloads (withOverride(Account.Name, …)) |
Best Practices
Override Only What Matters
Let the builder handle required fields. A test that names only the fields it asserts on stays focused and survives unrelated schema changes.
// Good — the test cares about Industry; everything else is filled in
Account account = (Account)kern.TST_Builder.of(Account.SObjectType)
.withOverride(Account.Industry, 'Technology')
.build();Prefer In-Memory Builds for Pure Logic
If the code under test only reads fields, withoutInsertion() skips DML entirely — faster tests and no governor cost.
Bulk With One DML
Always create many records with withCount(n).buildList(), never a loop of build() calls.
Use Mock Ids Only When the Code Needs an Id
Reach for withoutInsertion(true) when logic checks Id != null, keys a map by Id, or registers a query mock. Otherwise use withoutInsertion().
Build Relationships With withChildren
Let the builder set foreign keys. On insert it uses a single Unit of Work transaction; in memory it links the graph so children read back like subquery results.
Prefer Token Overloads in Hand-Written Tests
withOverride(Account.Name, …) is compile-checked. Use the String overloads only when the field name is genuinely dynamic.
Centralise Shared Setup in TST_Factory
If many test classes build the same kind of record (a user on a profile, an active flag), use the existing TST_Factory method — or, for subscriber-specific records, follow the same factory pattern in your own test helper.
Assert on User-Facing Text Through Custom Labels
When a test asserts on text that surfaces to an end user, compare against the Custom Label rather than a hardcoded literal — in Apex via System.Label.X, in LWC via an import from @salesforce/label/c.X. This keeps the test aligned with translatable, overridable copy.
Let Salesforce Reset Static State
Salesforce resets static state (including any custom default provider you assigned) between test methods. You do not need manual cleanup between methods for the test-data layer.
Related Documentation
- Test Data Fast Start - Build-and-test walkthrough, plus
TST_Mockfor DML-free query interception - Selectors - Guide - Querying records and mocking query results in tests
- DML - Guide - DML operations and Unit of Work transactions
- Triggers - Guide - Trigger actions tested with factory-built metadata
- Validation - Guide - Validation rules tested with factory-built metadata