ApexEloquent API Reference: IEntry / Entry / MockEntry

Apex Stem Docs
Apex StemApexEloquentAPI ReferenceIEntryMockEntrySalesforceApex
API reference for the IEntry interface and its implementations Entry (production) and MockEntry (test). Field / relationship access plus MockEntry extras (set, template, setParent / setChildren, autoId, alias, asAggregateResult).

IEntry is ApexEloquent's record wrapper (the interface). The return value of Eloquent.get(scribe) and similar is List<IEntry>. Production uses Entry; tests use MockEntry.

For usage and typical scenarios, see Data Access, DML, IEntry, and Mock and Parent Fields, Child Subqueries, and Many-to-Many.

The Three Relationships

CODE
IEntry  (interface)        ← the type handled by Usecases / business logic
   ↑
   ├── Entry               ← production. Wraps SObject or AggregateResult
   └── MockEntry           ← tests. Not bound to an SObject type; any field can be set

IEntry is an SObject wrapper, and via MockEntry.set it can express test data unreachable from raw SObject (writes to formula fields, rollups, parent relationships, auto-number). AggregateResult also works through the same IEntry interface, so the consumer-side code stays uniform between SObject queries and aggregate queries.

IEntry (Interface)

Field Access

SignatureReturnPurpose
get(String fieldName)ObjectRead any field's value (cast required)
put(String fieldName, Object value)voidWrite a value
getId()IdDedicated Id getter (no cast)
getName()StringDedicated Name getter (no cast)
getRecord()SObjectExtract the wrapped SObject (last resort)
setRecord(SObject record)IEntryInternal use. Swap the SObject
setFieldStructure(FieldStructure fs)IEntryInternal use. Set the SELECT-omission detection schema
setDescribeResult(Schema.DescribeSObjectResult)voidInternal use
APEX
IEntry oppEntry = eloquent.first(oppScribe);
Id oppId = oppEntry.getId();
String name = oppEntry.getName();
String industry = (String) oppEntry.get('Industry__c');
oppEntry.put('Status__c', 'Active');

Relations (reading)

SignatureReturnPurpose
getParent(String parentIdFieldName)IEntryGet the parent record (e.g. 'AccountId')
getChildren(String name)List<IEntry>Child record list
getThrough(String junctionName, String relatedKey)List<IEntry>Many-to-many. Get the related target via a junction object

The first argument of getChildren / getThrough resolves as either an object name or a relationship name. It looks for an object name first and falls back to a relationship name.

APEX
// Both give the same result
List<IEntry> opps = accountEntry.getChildren('Opportunity');    // object name
List<IEntry> opps = accountEntry.getChildren('Opportunities');  // relationship name

If you declared relationName(...) on the Scribe side, that name works too.

Deprecated: *ByRelationName

SignatureUse instead
getChildrenByRelationName(String childRelationName)getChildren(String)
getThroughByRelationName(String junctionRelationName, String relatedKey)getThrough(String, String)

Now that name resolution is unified, a relationship-name-only entry point is unnecessary. They remain for backward compatibility, but do not use them in new code — they may be removed in a future version.

Entry (Production)

The production class implementing IEntry. Supports both SObject-derived and AggregateResult-derived modes, and internally enforces SELECT-omission detection based on the Scribe's selected fields. No additional public methods.

Each element in the return value of Eloquent.get(scribe) is an Entry instance.

Safe to carry on batch state (v3.4.1+)

You can hold IEntry as an instance variable on a Database.Stateful batch.

⚠️ Through v3.4.0 this could throw SerializationException when crossing chunks, because the Schema.DescribeSObjectResult held internally is not serializable. Worse, whether it fired depended on cache warm-up — only the first cache-miss instance for a type carried the value — so identical code passed or failed seemingly at random. v3.4.1 made the field transient and re-derives it on demand.

MockEntry (Mock Extension)

The test-side implementation of IEntry. On top of the IEntry contract, it adds many extension APIs for building test data.

Factories

SignaturePurpose
MockEntry.of(System.Type recordType)Create an entry for a normal SObject type (MockEntry.of(Account.class))
MockEntry.asAggregateResult()Create an entry for aggregate-query results (not bound to an SObject type)
MockEntry.asAggregateResult(Map<String, Object> fieldToValue)Same as above, with initial values

Field Operations

SignatureReturnPurpose
set(String fieldName, Object value)MockEntrySet a field value (non-writable fields are allowed)
template(Map<String, Object> fieldToValue)MockEntrySet multiple fields at once via a Map
APEX
MockEntry accEntry = MockEntry.of(Account.class)
  .template(new Map<String, Object>{
    'Name' => 'Acme Co.',
    'Industry' => 'Technology'
  })
  .set('Active__c', true);

Immediate detection of SObject field-name typos: passing a non-existent SObject field name to set / add / setParent / addParent throws ApexEloquentException immediately.

APEX
MockEntry.of(Account.class).set('Naame', 'foo');
// → ApexEloquentException ("The field 'Naame' does not exist on the SObject.")

Previously, the typo would slip silently into fieldToValue; when the SUT later asked for the correct name (get('Name')), it received null and followed the null branch — passing the test while failing in production. False positives like this had a structural home. Now they are caught the moment you write the test setup, closing off setup-origin false positives by structure.

Note that put already runs field-name validation through standard SObject.put, so typos are caught there too — no behavior change for put.

set has a synonym, add(String, Object). They behave identically (both return a new MockEntry), so keep new code on set. setParent / setChildren likewise have addParent / addChildren.

Baking in the SELECT contract: fetchedBy

SignatureReturnPurpose
fetchedBy(Scribe scribe)MockEntryBake that Scribe's SELECT clause onto this entry as its contract

Entries returned through MockEloquent pick up the contract automatically from the Scribe you passed to get(scribe). That is the mechanism that throws when you touch a field you never selected.

Entries handed straight to the SUT have no contract, though. They never went through a Scribe, so there is no way for them to know which fields count as selected. fetchedBy attaches the contract after the fact.

APEX
MockEntry card = MockEntry.of(BusinessCard__c.class)
  .autoId(1)
  .set('CompanyName__c', 'Acme')
  .fetchedBy(RematchCompanyCardsHandler.scope());

Where this earns its keep is mainly batches. Records arriving at execute(bc, scope) never pass through IEloquent, so production is covered by the platform (they are real query results) while tests are not checked at all — you built those entries yourself. Pull the query construction into a @TestVisible method and the test can hand over exactly the same Scribe production uses.

See "Extract the Scribe and SELECT-omission detection works in tests too" in API Reference: Scribe for the full pattern.

Production Entry needs no fetchedBy. A real SOQL row already throws from the platform the moment you touch an unqueried field. This is a strictness tool the mock side alone requires.

Relations (building)

SignatureReturnPurpose
setParent(String parentIdFieldName, MockEntry parent)MockEntryHang a parent record under the entry
setChildren(String name, List<MockEntry> children)MockEntryHang a child-record list under the entry

For the first argument of setChildren, use the name you specified in Scribe via relationName if you did; otherwise use the object name (e.g. 'Opportunity' — singular, no __r).

⚠️ Keep the Scribe key and the setChildren key aligned.

MockEntry.getChildren checks against the child relation name the Scribe registered (the object name unless relationName was set). If they diverge, an ApexEloquentException is thrown. Production Entry resolves relationship names too, but in mocks you must call with the key you declared on the Scribe. It never quietly returns an empty list, so a mismatched key fails the test on the spot.

Auto-Generated Id

SignatureReturnPurpose
autoId(Integer suffix)MockEntryAuto-generate an 18-character Id (numeric suffix)
autoId(String suffix)MockEntryAuto-generate an 18-character Id (string suffix; supports placeholder expansion like {#})

Bulk Generation (times)

SignatureReturnPurpose
times(Integer count)List<MockEntry>Expand count records from the template. Replaces placeholders {#} {A} {a} with sequential values
times(Integer count, Integer startAt)List<MockEntry>Specify the start of the sequence
times(Integer count, Integer startAt, Integer interval)List<MockEntry>Specify the start and increment
APEX
List<MockEntry> contacts = MockEntry.of(Contact.class)
  .autoId('{#}')
  .set('LastName', 'Contact-{#}')
  .alias('con_{#}')
  .times(3);  // con_1, con_2, con_3

Nested multiplication is not supported: you cannot expand "2 parents × 4 children each" hierarchically by having MockEntry.of(Account.class).times(2) contain times(4) on the child side. Build the child side by enumerating individual entries inside setChildren.

alias (Retrieving Generated Ids)

SignatureReturnPurpose
alias(String name)MockEntryAttach a name to this entry
getByAlias(String name)MockEntryRetrieve a MockEntry by alias via recursive search
getAliasId(String name)IdRetrieve the auto-generated Id of an alias (handy for assertions)
APEX
MockEntry oppEntry = MockEntry.of(Opportunity.class)
  .alias('opp').autoId(1);
Id oppId = oppEntry.getAliasId('opp');
 
// In the test:
Opportunity updated = (Opportunity) mock.upsertedRecordsAt(MyUsecase.LBL_UPDATE)[0];
Assert.areEqual(oppId, updated.Id);

Disabling Detection (Two Layers)

MockEntry has detection in two independent layers, each with its own escape hatch. Neither should be used as a rule — both safety nets exist to close off false positives.

SignatureReturnWhat It Disables
withoutFieldValidation()MockEntryScribe FieldStructure check (permits get(...) access to fields not SELECTed in Scribe)
withoutSObjectFieldValidation()MockEntrySObject field-name check (permits non-existent field names in set / add / setParent / addParent)
APEX
// Example: a rare case where you want to store data under a name that isn't an SObject field
MockEntry.of(Account.class)
  .withoutSObjectFieldValidation()
  .set('Synthetic__c', 'value');  // not an Account field, but permitted

These two are separate flags. Relaxing the Scribe check via withoutFieldValidation() keeps the SObject typo detector intact (a clear set('Naame', ...) typo still throws). The reverse holds as well. Each escape hatch disables only its own responsibility — the other safety net stays on.