ApexEloquent v2: What's New

8 min read
ApexEloquentv2Release NotesScribeMockEntryTestingSalesforceApex
A tour of the new features in ApexEloquent v2 — Scribe.of(), MockEntry's alias and template, IEloquent now accepting raw SOQL, and more. Testing got noticeably easier.

A while back I shipped ApexEloquent v2.0.0! 🎉

(There was a feature I urgently wanted to add, so the version has already moved on to v2.0.x.)

In this post I want to walk through the new features that landed in this release!

Unified Error Messages

The library used to throw the standard QueryException, but I changed that to throw ApexEloquentException instead.

This ApexEloquentException bundles into one log message:

  • where it happened
  • what kind of error it was
  • the reason
  • what action to take next

It's easy to read for humans, of course, but also clear enough for an AI to know what to do next, so you're less likely to get stuck on how to use ApexEloquent!

Scribe

A New of Factory Method

The Scribe entry point used to look like this:

APEX
Scribe oppScribe = Scribe.source(Opportunity.getSObjectType());

— a bit long and hard on the eyes, so to align with ApexBlueprint, you can now declare it like:

APEX
Scribe oppScribe = Scribe.of(Opportunity.class);

Readability gets noticeably better, especially in nested queries!


Faster Child-Relationship Resolution

When you write a child relationship in Scribe like:

APEX
Scribe scribe = Scribe.of(Account.class)
    .field('Name')
    .withChildren(
        Scribe.asChild(Contact.class)
            .fields(new List<String>{'Id', 'Email'})
            .whereNotNull('Email')
    );

it generates this SOQL:

SOQL
SELECT name, (SELECT id, email FROM Contacts WHERE Email != NULL) FROM Account

The asChild(Contact.class) part dynamically figures out the relationship name by inspecting the parent Account — and I switched this resolution to use binary search.

I'll go deeper in another post, but since the child object names on a parent are already sorted in Unicode order, I dropped a binary search in there.

The number of calls to getDescribe (a heavy operation) dropped substantially, so test execution time went down with v2!

This binary search is robust as your org grows and the number of relationships increases, so it should keep paying back even more for larger orgs.

IEntry

getChildrenByRelationName Merged into getChildren

When you specified Scribe.relationName('xxx__r'), you used to have to retrieve children via getChildrenByRelationName('xxx__r') rather than getChildren('xxx__r') — and Claude kept tripping on this, so I made getChildren('xxx__r') work too.

AI-friendly! 🙌

MockEntry

I added a lot of mock-data features. Writing tests should be more comfortable, and more fun!


A New of Factory Method

Just like Scribe, you can now write:

APEX
MockEntry.of(Opportunity.class)

Cleaner, right?


Turning Off SELECT-Omission Detection

Scribe lets you list fields to SELECT with field / fields, and if you try to access a field that wasn't in that list, the mock throws an error — that's the "SELECT-omission detection". This new feature is the switch to turn that off.

Concretely:

APEX
MockEntry.withoutFieldValidation()

Just drop withoutFieldValidation() into the chain!

There must be a corner case somewhere where this comes in handy!


Expressing Aggregate-Result Mocks with asAggregateResult

MockEntry can mock AggregateResult too, but there used to be a knack to declaring it, so now you can express "this is an aggregate mock" clearly like:

APEX
MockEntry.asAggregateResult().set('count', 10);

Field Setting via set

Field setting used to go through add, and now I added set.

add and set behave identically, but add is scheduled for removal in v3.

Writing tests myself, I kept reaching for set to set a field — that made me suspect add wasn't the more conventional verb, so I added set!


Aliases, and Pulling Things Out by Alias

When you set an id or a value like this, there used to be no way to retrieve it back:

APEX
MockEntry oppEntry = MockEntry.of(Opportunity.class).autoId(1).set('Name', 'TestOpp');

But while writing tests, I kept running into situations where I wanted the Id of the oppEntry I'd just built. So I added these APIs:

  • alias
  • getByAlias
  • getAliasId

How to use them:

  • Attach an alias to a MockEntry with alias
  • Use getByAlias to retrieve that MockEntry (it walks parent and child links automatically too!)
  • Use getAliasId if you just want the Id
APEX
MockEntry oppEntry = MockEntry.of(Opportunity.class)
    .alias('opp')       // attach alias 'opp'
    .autoId(1)
    .set('Name', 'TestOpp');
  
// pull the Id directly
Id mockOppId = oppEntry.getAliasId('opp');
 
// pull the MockEntry itself
MockEntry mockOpp = oppEntry.getByAlias('opp');
String mockOppName = mockOpp.getName();

This is incredibly handy when writing Asserts! Highly recommended!


The template Method

When you're setting up test data and the same set of fields keeps showing up, that's where template shines!

APEX
private static Map<String, Object> getTemplate() {
    return new Map<String, Object>{
        'Name' => 'testOpp'
        ...
    }
}

Prepare a method that returns frequently-used fields like the above, then in your test class:

APEX
MockEntry oppEntry = MockEntry.of(Opportunity.class)
    .template(xxxTest.getTemplate())
    .set(...)

— and the common setup lands in one line! DRY up your tests cleanly!

IEloquent

getAggregate Merged into get

It was confusingly subtle — even I got confused — so I just merged them!


Now Accepts Raw SOQL

Where previously you had to go through Scribe, you can now hand a raw SOQL string instead!

APEX
String soql = 'SELECT Id, Name FROM Opportunity LIMIT 10';
 
List<IEntry> oppEntries = (new Eloquent()).rawSoql(soql);

Note: when you take this path, the "SELECT-omission detection" is forcibly turned off, so be careful!

MockEloquent

Adding the failOnXxx Family

MockEloquent could already throw errors intentionally on get, doUpdate and friends, but you had to pass the error as the second constructor argument, which was awkward — so I added methods like:

  • failOnGet
  • failOnFirst
  • failOnDoInsert
  • failOnDoUpsert

… in the failOnXxx family!

APEX
IEloquent eloquent = (new MockEloquent(mockEntry)).failOnGet();

Define it like this, DI it in, and the moment get is called, an error is thrown!

That makes verifying catch blocks and retry logic even smoother!


Adding deletedCount

MockEloquent now records how many records were passed to doDelete. With that:

APEX
// Production code
this.eloquent.doDelete(deleteTargets);
 
// Test class
Assert.areEqual(10, this.eloquent.deletedCount);

— delete-related asserts are now possible!