Apex Stem · Data Access
v3.5.0·Release notes

ApexEloquent

AI writes the code. AI writes the tests.One wall is left, and it's made of physics: running them.

Requirements stay human. Everything after that keeps getting cheaper. But executing a test is bound to DML — to real disk — and no model generation shaves a millisecond off it.ApexEloquent removes that wall structurally. With one injection.

PriceBand_T.cls — unit test · no database
// A unit test that never touches the databaseIEntry opp = MockEntry.of(Opportunity.class)    .autoId(1)    .set('Amount', 50000000); MockEloquent mock = (new MockEloquent())    .attach('fetch', new List<IEntry>{ opp }); new PriceBandUsecase(mock).invoke(); Assert.areEqual('Large',    ((Opportunity) mock        .upsertedRecordsAt('update')[0]).PriceBand__c);

Feed the mock, call the Usecase exactly as production does, then read back what it wrote. The database never moves. That's why the cost of running tests stops piling up, however many AI writes.

The wall AI can't move

A test creates a record, updates it, and triggers, flows and rollups fire in turn. Those are things you built on the platform. What is actually slow is underneath them: the reads and writes to the database, a physical round trip to disk. No amount of model progress takes a millisecond off that. And here is the awkward part — the closer the cost of writing tests gets to zero, the more the cost of running them stands alone as the one thing setting the pace.

Number of tests

More features, more tests. AI is on the side that accelerates this.

Weight of each one

Add one required field or validation rule and every existing test that touches the object is taxed.

Whole org, every deploy

RunLocalTests bites as total tests × deploy frequency.

The three multiply, so execution time does not grow linearly — it growssuperlinearly as the org matures. ApexEloquent uses DI to take the dominant term, the database round trip, out of unit tests entirely and run them in memory.This is not "a bit faster". The single largest factor inflating your execution time is gone.

Fast, and not blind

DB-less tests are fast.They're usually blind, too. Not here.

Speed is usually sold together with blindness. Not here. If we're going to claim speed, we should also show that the fast testsstill catch real bugs.

Speed

Run time of the same
test method (median)

DB-less (MockEloquent)

15ms

vs

Real DML

272–1,062ms

Measured across two orgs in production use. The unit layer runs no DML, so as validation rules, related objects and flows pile up,only the real-DML side grows.

Bug-catching

Share of 334 planted
bugs actually caught

DB-less (MockEloquent)

83.4%

vs

Real DML

79.3%

Measured with mutation testing — deliberately planting bugs and checking whether the tests notice. Trading detection away for speeddid not happen.

Both are real measurements, but not large ones (one class, one run each). Read them as "this is what we measured", not as proof.

Even with the numbers in hand, two things tend to stay unresolved. They bothered us too, so they're written out below.

"Doesn't dropping the DB drop the real bugs with it?"

The constraint that catches real bugs — the SELECT clause — is rebuilt inside the mock layer. The blueprint a Scribe declares (fields, relations, aliases) is enforced on the mock, so touching a field you never selected throws in the unit test. False positives die before shipping. The SELECT-omission net →

"If it's slow, why not just run the tests that matter (RunSpecifiedTests)?"

In a mature org, what "matters" isn't decided by code alone. Every insert and update sets off triggers, flows and rollups, so the blast radius is decided at runtime by code plus metadata plus data. The set of "relevant tests" cannot be enumerated in principle. Narrowing the target doesn't remove risk — ittrades visible slowness for invisible misses, quietly shrinking what "green" means. ApexEloquent goes the other way:run all of them, and make that cheap, so "every class green" keeps meaning "nothing is broken".

Batteries included

Everything you need to write a test lives in one library — the ORM that builds the query, the test data, the execution mock, the spy, the failure injection. Nothing left over to fill in with another library or a helper of your own.

Query

Scribe builds SOQL as a typed chain. SELECT, WHERE and ordering, plus parent fields, child subqueries, many-to-many, aggregates and dynamic conditions — all in the same shape.

Test data

MockEntry puts values into fields you cannot normally write — formula, rollup, auto-number. Parent-child graphs and AggregateResult take the same form.

Execution mock

Four read methods, four DML methods and external-ID upsert all land onMockEloquent. Label your call sites and one mock serves them all, kept apart.

Spy

Read back which records were saved or deleted, under which label. You get to see what happened without running a single DML.

Failure injection

Throw an exception at exactly one call site. Retry-then-succeed paths and per-record partial save failures are reproducible without touching org configuration.

False-positive guards

Touch a field you never selected and the unit test throws. Feed the mock under the wrong key and it throws.Green while verifying nothing is closed off structurally.

On the production side, v3 defaults execution to user mode(respecting the running user's field and object permissions). Only work that must complete no matter who triggers it — aggregation, stamping, migration — opts out explicitly with systemMode().

800+

tests on the library itself

A library for testing is worth little if it is itself broken, so ApexEloquent is held down by over 800 of its own tests. On top of that, it is deployed and running in production across more than ten client projects. It is a library its author uses every working day.

How it reads

The production code and its unit test, side by side. Same IEloquent — real SOQL in production, in-memory in the test. The tinted lines are the seams where the two meet.

Data in

The test attaches mock records to 'fetch' — the exact label production reads from.

Data out

The test pulls whatever was written to 'update' from the spy, and asserts on it.

The only change

eloquent arrives by DI — real in production, mock in the test. The Usecase code itself never changes.

PriceBandUsecase.clsProduction
Scribe scribe = Scribe.of(Opportunity.class)    .field('Name')    .field('Amount')    .whereEqual('StageName', 'Prospecting'); List<IEntry> opps = this.eloquent.label('fetch').get(scribe); for (IEntry opp : opps) {  Decimal amount = (Decimal) opp.get('Amount');  opp.put('PriceBand__c', amount >= 10000000 ? 'Large' : 'Standard');} this.eloquent.label('update').doUpdate(opps);
PriceBand_T.clsUnit test · no DB
IEntry opp = MockEntry.of(Opportunity.class)    .autoId(1)    .set('Name', 'Acme Renewal')    .set('Amount', 50000000); MockEloquent mock = (new MockEloquent())    .attach('fetch', new List<IEntry>{ opp }); new PriceBandUsecase(mock).invoke(); List<SObject> updated = mock.upsertedRecordsAt('update');Assert.areEqual('Large',    ((Opportunity) updated[0]).PriceBand__c);

Why it's one library

A query builder, an execution mock, a test-data factory. Different responsibilities — normally you would split them into separate libraries. There is a reason they are kept together.

SELECT-omission detection only exists if all three points connect.

Declare

Scribe builds up what this query selects

Carry

Eloquent /MockEloquent hands that declaration back with the result

Check

IEntry checks every access against it and throws on anything not declared

The third one is the key. The check runs at the moment a field is accessed, and the only way to get your own code in there is to own the type the caller touches. Returning IEntry putsget() in the path, and that is where the comparison happens. Return a bare SObject andrecord.Industry becomes a language primitive — not one line of library code runs.It isn't a feature left unbuilt; there is no seam to build it in.

A guarantee reaches only as far as the type you own.

Hand back a plain SObject and it connects to existing code and standard APIs with no conversion, and there is no new type to learn. Hand back a wrapper and you can put a guarantee on the moment of access instead. Neither is better — it is a trade. ApexEloquent takes the second, and keeps declaration through checking in one place to do it.

Position in Apex Stem

ApexEloquent is the Data Access piece of Apex Stem. It lives in the Usecase layer of the Handler-Usecase Architecture and carries the "Usecase unit test" half of the test strategy.

ApexEloquent
Data Access: SOQL / DML + Mock
ApexBlueprint
Test Data Factory: real-DML data for integration tests
ApexTrace
Lifecycle Logging: Usecase path tracing and test verification
ApexTools
Foundation: TriggerHandler base + HTTP DI wrapper
Related Documents

Start with the Developer Guide

Three usage paths — building queries with Scribe, fetching data and running DML, and working with relations — all in real code.