Skip to content

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
  1. Quick Navigation
  2. Overview
  3. Quick Start
  4. Escape Hatches
  5. Architecture
  6. Building Records
  7. Bulk Creation
  8. Insertion Modes
  9. Relationships
  10. The Factory Pattern
  11. Extending the Defaults
  12. Capability Matrix (for Analysts)
  13. Anti-Patterns
  14. Best Practices
  15. Related Documentation

Quick Navigation

I am a...I need to...Go to...
DeveloperBuild one record fastQuick Start
DeveloperBuild records in bulkBulk Creation
DeveloperBuild records with no DMLInsertion Modes
DeveloperBuild parent-child graphsRelationships
ArchitectStandardise team test dataThe Factory Pattern
ArchitectCustomise auto-populated defaultsExtending the Defaults
AnalystUnderstand guarantees and limitsCapability 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:

  1. TST_BuilderPrimary tool — a fluent builder for any SObject. Auto-populates required fields, supports overrides, bulk creation, cycling values, parent-child relationships, and three insertion modes.
  2. TST_FactoryConvenience 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 safetySObjectField token overloads catch field-name typos at compile time.
  • Bulk in one statementwithCount(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.
  • ConsistencyTST_Factory centralises 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.

apex
// 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 needUseSee
A plain record with no auto-populationnew Account(Name = 'X') then insert — standard Apex, no wrapper.
One field that the builder shouldn't defaultwithOptionalField(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 dataInsert real records via TST_BuilderAggregateResult cannot be mocked (platform limitation).Anti-Patterns
Custom default values for a field typeExtend 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

text
┌─────────────────────────────────────────────────────────────────────────────┐
│                          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:

apex
Account account = new Account(Name = 'Test', Industry = 'Technology');
insert account;
FeatureKernDX Test Data LayerHand-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 safetySObjectField token overloads (withOverride(Account.Name, …))✅ Direct field access is compile-checked too
Bulk creationwithCount(n).buildList() — single DML statement⚠️ Manual loop; easy to accidentally DML inside the loop
Cycling valueswithCycle(field, values) rotates across records❌ Manual index arithmetic
In-memory / mock IdswithoutInsertion() / withoutInsertion(true)⚠️ Manual; mock Ids require a separate helper
Parent-child graphswithChildren(...) — single Unit of Work transaction⚠️ Insert parent, set foreign keys, insert children by hand
Consistency across a teamTST_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 across buildList().
  • withDefaultedField(...) / withDefaultedFields(List<Object>) — also default a non-required field.
  • withOptionalField(...) / withOptionalFields(List<Object>) — skip auto-populating a field.
  • withCount(Integer) — number of records for buildList().
  • 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 single SObject.
  • buildList() — returns a List<SObject> of withCount(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) — uncommitted User.
  • kern.TST_Factory.newUsers(profileName, count) — uncommitted User list.
  • 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

apex
// 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 inserted

Overriding 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.

apex
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.

apex
// 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.

apex
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.

apex
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.

apex
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.

apex
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.

apex
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.

apex
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, Healthcare

Insertion 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.

apex
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.

apex
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.

apex
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

ModeDMLIdUse when
(default)RealThe test exercises real persistence, queries, triggers, or sharing.
withoutInsertion()nullThe code under test only reads fields — no Id, no database needed.
withoutInsertion(true)MockThe 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.

apex
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):

apex
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.

apex
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:

apex
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.

apex
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.

apex
@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.

apex
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.

apex
@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.

apex
@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:

apex
@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
<?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:

apex
@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:

apex
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.

apex
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:

apex
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)

CapabilityHow it is expressedNotes
Auto-populate required fieldsDefault behaviour of build() / buildList()Custom validation rules may require additional overrides
Override specific fieldswithOverride / withOverridesToken overloads are compile-checked
Bulk creation, single DMLwithCount(n).buildList()Keeps tests within governor limits
Rotate values across recordswithCycle(field, values)Repeats the list when records exceed values
Build without DMLwithoutInsertion()No Id; fastest path
Build without DML, with an IdwithoutInsertion(true)Valid mock Id for Id != null logic and query mocking
Parent-child graphswithChildren(...)Single Unit of Work on insert; in-memory linkage without insert
Record typeswithRecordType(developerName)Falls back to master when the packaged type is unavailable
Common records (users, flags)TST_Factory methodsCentralises repeated setup
Framework metadata for testsDeploy CustomMetadata records via XMLSets up trigger and validation behaviour for your tests
Customise auto-populated defaultsExtend DefaultValueProviderPer-transaction; reset between test methods

Anti-Patterns

Anti-PatternWhy It's WrongInstead
Calling build() inside a loopOne DML statement per iteration — hits governor limits fastwithCount(n).buildList() — a single DML statement
Setting every required field by handVerbose and brittle; renamed required fields break silentlyOverride only what the test asserts on; let the builder fill the rest
Using withoutInsertion(true) when no Id is neededGenerates a mock Id the test never uses — noisewithoutInsertion() (no argument) for pure in-memory logic tests
Trying to mock aggregate queriesAggregateResult has no public constructor — it cannot be instantiatedInsert real records via TST_Builder, then run the aggregate query
Forgetting IsParallel=true removal when inserting UserSalesforce forbids User DML in parallel testsRemove IsParallel=true from the class header for user-inserting tests
Hardcoding field names as strings in hand-written testsBreaks silently when fields are renamed or deletedUse 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.

apex
// 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.