ApexEloquent: MockEntry Deep Dive
When you try to write business-logic tests in Apex, you hit a wall almost immediately: "I can't build the test data". Formula fields and rollup summaries can't be assigned directly on an SObject; building parent-child relationships from code requires DML; child relationship names can't be written back onto an SObject; and so on. The platform is full of fields that you can't fill in until you actually run something.
ApexEloquent's MockEntry is the core feature for assembling test data — including those non-writable fields — without going through the DB. Formula fields, rollups, parent relationships, and auto-numbers all accept direct value assignment, and parent-child hierarchies can be expressed in code while preserving structure.
This document covers "what MockEntry solves" and "what the writing style feels like", following typical scenarios. For comprehensive API coverage, see API Reference: IEntry / Entry / MockEntry.
Why Writing Apex Test Data Is Hard
Apex's SObjects have many fields that business logic tests care about but can't be assigned from code.
- Formula fields (
Formula) - Roll-Up Summary fields (
Roll-Up Summary) - Auto-number fields (
Auto Number) - System-managed fields (
CreatedDate/LastModifiedDate/Idand the like) - Parent-child relationship names (e.g.
Account'sContacts, lookup'sParent__r)
These are calculated or assigned by the Salesforce platform, so a pure SObject instance (new Opportunity(...)) can't have them set by hand. As a result, testing business logic that depends on these values requires:
- Actually
inserting parent / child records and letting the platform compute formulas - Re-querying until rollups reflect
- Going back through DML and re-querying to confirm what comes back through a relationship name
— in other words, preparation work unrelated to the actual logic verification piles up. Tests get slow, heavy, and become things you write with one eye on governor limits.
The Conventional Workaround: JSON Serialization Hack and Its Limits
A common technique in the Apex community for filling in non-writable fields is the JSON.deserialize hack: build the SObject as a JSON string, deserialize it, and you can put values into fields that are normally inaccessible.
// Build a Map<String, Object>, serialize to JSON, then deserialize into an SObject
Map<String, Object> oppMap = new Map<String, Object>{
'Id' => '006000000000001AAA',
'Name' => 'Opportunity A',
'NameWithAccountName__c' => 'Opportunity A_Test Account'
};
Opportunity opp = (Opportunity) JSON.deserialize(JSON.serialize(oppMap), Opportunity.class);
// → an SObject instance with a value in the formula field NameWithAccountName__c
In simple cases this does work. Writing things as a Map sidesteps the string-concatenation and escaping pain, but the hack is fundamentally dependent on "going through a JSON layer", so the following costs remain:
- The cost of round-tripping through JSON: working with an Apex SObject takes three hops — Map →
JSON.serialize→JSON.deserialize. For a flat field set this is no big deal, but the cost shows up the moment you start expressing parent-child or child-subquery structures - Parent-child expression is Salesforce-specific: building parent fields or child-subquery-like data as a Map means you have to understand Salesforce's bespoke JSON structure (child subqueries live under a
'records'key, parent and child records need anattributeskey, etc.) — a separate convention from plain Apex SObject handling - Template management reverts to method proliferation: similar Map structures end up scattered across tests, and consolidating leads back to "let's add another test-data factory method" — landing in the same Selector Pattern method explosion
In short, the JSON hack can produce "working tests" but it's structurally hard to produce "maintainable tests".
MockEntry is the core feature that achieves what the JSON hack was trying to do (= free assignment to non-writable fields), through dedicated factory method chains that stay inside Apex, without the round-trip through Map and JSON.
// No round-trip through Map and JSON — structurally written via dedicated factory methods
MockEntry oppEntry = MockEntry.of(Opportunity.class)
.alias('opp').autoId(1)
.set('Name', 'Opportunity A')
.set('NameWithAccountName__c', 'Opportunity A_Test Account');
Starting from MockEntry.of(...), you build test data with the structure intact via dedicated methods like .set / .setParent / .setChildren / .times / .alias / .autoId. Parent-child hierarchies, bulk generation patterns, and retrieving generated Ids are all handled through a typed API.
On top of that, in ApexEloquent typos in field names are caught twice via the coordination with Scribe.
- Scribe level: if you specify a non-existent field name like
Scribe.of(Account.class).field('TypoField__c'), the path to.toSoql()throwsThe field TypoField__c does not exist on the SObject Account. Caught at test execution time - MockEntry level: accessing a field not in
Scribe's SELECT viaentry.get('XXX')throws immediately, just like a realEntry(see "SELECT Omission Detection" under How MockEntry Works)
The risk that comes with string keys gets surfaced at the test stage, the moment you combine with Scribe.
How MockEntry Works
MockEntry implements the IEntry interface, following the same contract as the production Entry, but designed so that any field can be assigned a value in tests. Internally, it runs along two axes.
1. Value Override (override map)
MockEntry checks an internal override map first when reading a value, and returns whatever is there if present. Otherwise it falls through to the wrapped SObject. Formula / rollup / auto-number fields can't be written on the SObject, but they're free to write into the override map — so any field value can be returned as a result.
// MockEntry.get() behavior (simplified internal implementation)
public override Object get(String fieldName) {
// 1. prefer the override map if it has the value
if(fieldToValue.containsKey(fieldName)) {
return fieldToValue.get(fieldName);
}
// 2. otherwise fall through to the wrapped SObject
return record.get(fieldName);
}
2. SELECT Omission Detection
MockEntry has another strong safety device. It remembers the SELECT clause of the Scribe query, and if entry.get('FieldName') tries to pull a field that wasn't in Scribe's SELECT, it throws immediately, just like a real Entry.
In other words, the classic bug "tests pass because the value was set, but production fails because the field was forgotten in the SOQL" can be caught at the unit-test stage. The background of this property is also touched on in Query Delegation Pattern.
📘 Detailed: see the Deep Dive Catching Mock-Test False Positives: A Safety Net for SELECT Omissions, which digs into four practical cases (primary object / parent relation / child subquery / aggregate alias) with code examples showing "the happy-path test catching a buggy Usecase".
Because production uses standard Entry and tests inject MockEntry, the structure stays intact — no risk of drift between production and test behavior.
Writing Tests Including Non-Writable Fields
A real-world example: business logic that depends on a formula field. Opportunity has a formula field PriceBand__c that returns 'Small' / 'Medium' / 'Large' based on Amount. When PriceBand__c is 'Large', the logic prepends [Approval Required] to Description.
Production Code
public with sharing class FlagLargeOppForApprovalUsecase {
private final Id oppId;
private final IEloquent eloquent;
public FlagLargeOppForApprovalUsecase(Id oppId, IEloquent eloquent) {
this.oppId = oppId;
this.eloquent = eloquent ?? new Eloquent();
}
public Opportunity invoke() {
Scribe oppScribe = Scribe.of(Opportunity.class)
.fields(new List<String>{ 'Id', 'Description', 'PriceBand__c' })
.whereEqual('Id', this.oppId);
IEntry oppEntry = this.eloquent.first(oppScribe);
if(oppEntry == null) {
return null;
}
String priceBand = (String) oppEntry.get('PriceBand__c');
if(priceBand != 'Large') {
return(Opportunity) oppEntry.getRecord();
}
Opportunity opp = (Opportunity) oppEntry.getRecord();
String currentDescription = opp.Description != null ? opp.Description : '';
opp.Description = '[Approval Required] ' + currentDescription;
return(Opportunity) this.eloquent.doUpdate(opp);
}
}
Test Code
@isTest
static void testInvoke_WhenPriceBandIsLarge_ThenDescriptionIsPrefixed() {
Trace t = Trace.of('Happy path: when PriceBand is Large, Description is prefixed with [Approval Required]');
t.start();
// Arrange: assemble a MockEntry with a direct value in the formula field PriceBand__c
MockEntry oppEntry = MockEntry.of(Opportunity.class)
.alias('opp').autoId(1)
.set('Description', 'Large deal')
.set('PriceBand__c', 'Large');
IEloquent mockEloquent = new MockEloquent(oppEntry);
// Act
FlagLargeOppForApprovalUsecase usecase =
new FlagLargeOppForApprovalUsecase(oppEntry.getAliasId('opp'), mockEloquent);
Opportunity updatedOpp = usecase.invoke();
// Assert
Assert.areEqual('[Approval Required] Large deal', updatedOpp.Description);
t.finish();
}
The formula field PriceBand__c gets 'Large' set directly. Normally, setting a real Amount like new Opportunity(Amount = 10000000) doesn't help — formula fields are computed by the platform, so values don't actually land in code (you'd need a real insert). MockEntry.set writes into the override map, so the formula field's value itself can be specified arbitrarily. This lets you verify "the logic when PriceBand__c is 'Large'" without touching the DB and without worrying about Amount's boundary conditions.
The same approach works for rollup summary fields, auto-number fields, and system-managed fields (CreatedDate etc.).
Feature Catalog of MockEntry
MockEntry offers several features beyond .set for non-writable fields that make test-data construction easier. This section only gives an overview of "what's possible"; concrete code examples and usage are consolidated in Data Access, DML, IEntry, and Mock.
- Parent-child hierarchical structure:
setParent/setChildrenhangs a parent record and child-record lists with structure preserved. The code's indentation directly mirrors the data's relation structure, so "what child records hang under this Account" is readable at a glance. The Id linkage between parent and child is also handled internally byMockEntry. Details in Parent Fields, Child Subqueries, and Many-to-Many - Pattern generation at scale:
times()plus the{#}/{A}/{a}placeholders expand from one template into N sequential / uppercase / lowercase records.times(count, startAt, interval)lets you specify the start and increment (⚠️ nested "N parents × M children each" multiplicative expansion is not supported) - Named retrieval of generated Ids: the 18-character Id auto-generated by
autoIdcan be named withalias('opp')and pulled out later viagetAliasId('opp'). Useful when asserting "the Id of an upserted Opportunity matches the one set in the mock" - Mocking aggregate-query results:
MockEntry.asAggregateResult()produces anIEntrynot bound to anAggregateResulttype. MockCOUNT/SUM/GROUP BYresults as one entry per group
"Non-writable fields", "parent-child", "bulk generation", and "aggregation" all flow through method chains on the same MockEntry API — no JSON serialization, no hand-rolled factories. That's the core of MockEntry's design.
Summary
The reason Apex test-data construction is hard boils down to two things: "many non-writable fields" and "building parent-child requires DML". The traditional workaround — the JSON serialization hack — handles the former, but you're left carrying the burden of string assembly and Salesforce-specific JSON structure. MockEntry resolves both, with structure intact, through dedicated factory method chains.
- Non-writable fields: return arbitrary field values via the override map (no JSON serialization needed)
- Parent-child relationships: build with
setParent/setChildrenin structural form - Bulk data: expand from one template via
times()and placeholders - Consistency with production: the
IEntryinterface plus SELECT-omission detection guarantees the same behavior as production - Generated Ids:
autoId+aliasmakes Ids retrievable by name for assertions - Aggregate queries:
AggregateResultis mocked through the same interface
This brings Apex testing down to "verifying business logic itself, without needing a DB".
Related Documents
- API Reference: IEntry / Entry / MockEntry: comprehensive coverage of
MockEntry's full API - Parent Fields, Child Subqueries, and Many-to-Many: the usage guide for relation operations
- Data Access, DML, IEntry, and Mock: integration with
MockEloquent(including Spy / failOn) - Query Delegation Pattern: the design philosophy behind SELECT-omission detection
- ApexEloquent Guide: the entry point to the whole ApexEloquent guide