# KrileWorks — All documentation (full text) > Full documentation for **Apex Stem**, an OSS stack for Salesforce Apex development > (ApexEloquent / ApexBlueprint / ApexTrace / ApexTools). This is the content itself, not an index. Everything you need to reach API signatures and code examples is in this single file. Human-facing pages live at https://krileworks.com/apex-stem/docs/{slug}. If you only need the index, see https://krileworks.com/llms.txt. Per-library files are also available, sized to be read in a single fetch: - ApexEloquent: https://krileworks.com/llms/apexeloquent.txt (13 docs) - ApexBlueprint: https://krileworks.com/llms/apexblueprint.txt (10 docs) - ApexTrace: https://krileworks.com/llms/apextrace.txt (4 docs) - ApexTools: https://krileworks.com/llms/apextools.txt (2 docs) - Apex Stem: https://krileworks.com/llms/apex-stem.txt (2 docs) - Architecture & Concepts: https://krileworks.com/llms/concepts.txt (3 docs) The text below is the same source Markdown the site renders (only in-page links are expanded to absolute URLs). ============================================================================== Source: https://krileworks.com/document/apex-eloquent-prerequisites.md Page: https://krileworks.com/apex-stem/docs/apex-eloquent-prerequisites ============================================================================== # ApexEloquent Installation Prerequisites Before introducing ApexEloquent, please ensure that the following tools and environments are properly set up. ## ✅ Salesforce CLI Setup ApexEloquent deployment uses the Salesforce CLI. If you haven't installed it yet, please install it from the official website. - Installation verification: ```bash $ sf -v ``` If version information is displayed, you're all set. ## ✅ Target Organization Verification In your project root directory, run the following command to verify organization information: ```bash $ sf org list ``` Confirm that the organization you want to target has the 🍁`Default Org` indicator. Example: ``` ┌────┬─────────┬────────────┬────────────────────────────────────────┬────────────────────┬───────────┐ │ │ Type │ Alias │ Username │ Org Id │ Status │ ├────┼─────────┼────────────┼────────────────────────────────────────┼────────────────────┼───────────┤ │ 🌳 │ DevHub │ devhub │ example-user@example.com │ 00DxxxxxxxxxxxxXXX │ Connected │ │ 🍁 │ Sandbox │ sandbox │ example-user@example.com.sandbox │ 00DyyyyyyyyyyyyYYY │ Connected │ └────┴─────────┴────────────┴────────────────────────────────────────┴────────────────────┴───────────┘ ``` ## ⚙️ Default Organization Configuration If the 🍁 indicator is not present, log in and configure as follows: ```bash # Login to organization (replace alias and URL as appropriate) $ sf org login web --alias my-sandbox --instance-url https://orgfarm-xxxxxxx-dev-ed.develop.my.salesforce.com # Set as default organization $ sf config set target-org=my-sandbox ``` After configuration, run `sf org list` again to confirm the 🍁 mark is present. ## 🛠️ Make Command Setup ApexEloquent uses a `Makefile` to simplify some deployment operations. Please verify that the `make` command is available. ```bash $ make -v ``` If you see output like the following, you're ready: ``` GNU Make 4.3 ``` ## 🔗 Back to Guide ← [Back to ApexEloquent Developer Guide](https://krileworks.com/apex-stem/docs/apex-eloquent-guide) ============================================================================== Source: https://krileworks.com/document/apex-eloquent-installation-guide.md Page: https://krileworks.com/apex-stem/docs/apex-eloquent-installation-guide ============================================================================== # 🚀 Getting Started This guide walks you through the complete installation process for ApexEloquent in your Salesforce project. ## 📥 Package Acquisition (First Time Only) Use Git Submodule to acquire `ApexEloquent`: ```bash $ cd force-app/main/default/classes $ git submodule add https://github.com/krile136/ApexEloquent.git ApexEloquent ``` This will add the `ApexEloquent` directory to your repository and incorporate it into source control. If you are cloning a repository that already has it, don't forget `git submodule update --init --recursive`. ### Choosing the v2 line The command above tracks the default branch (`main`), which gives you the **v3 line**. On v3, SOQL and DML default to **user mode**, so dropping it into an existing org can start failing on missing field permissions. If you want to start on the v2 line, point at the maintenance branch. ```bash $ git submodule add -b v2.2.x https://github.com/krile136/ApexEloquent.git ApexEloquent ``` Feature-wise it matches the v3 line; the only difference is the execution mode. See the top of the [ApexEloquent guide](https://krileworks.com/apex-stem/docs/apex-eloquent-guide) for what to weigh. ## 🚀 Deploy to Organization Deploy the acquired classes with the following command: ```bash $ make install ``` The `make install` command internally calls Salesforce CLI deploy commands. ApexEloquent ships with its own test classes that contribute to the 75% coverage requirement. However, **if test failures occur due to organization settings or disabled standard fields**, please adjust the `*_T.cls` test classes accordingly. ## 🔄 Updating ApexEloquent To update ApexEloquent, run the following commands from your project root: ```bash $ cd force-app/main/default/classes/ApexEloquent $ git pull $ make install ``` :::warning `git pull` takes the tip of whatever branch you are tracking. **If you are on the v2 line (`v2.2.x`), switching to `main` and pulling will jump you to the v3 line.** That changes the default execution mode — a breaking change — so check what you are tracking with `git branch --show-current` before updating. ::: :::warning Submodules maintain information (pointers) about which commit the parent repository references. To ensure consistency in production environments, we recommend using `git submodule update --remote` to automatically update the reference to the latest version, and then committing in the parent repository as well. ::: ## 🔗 Back to Guide ← [Back to ApexEloquent Developer Guide](https://krileworks.com/apex-stem/docs/apex-eloquent-guide) ============================================================================== Source: https://krileworks.com/document/apex-eloquent-scribe-guide.md Page: https://krileworks.com/apex-stem/docs/apex-eloquent-scribe-guide ============================================================================== # Building Queries with Scribe This document is a usage guide focused on **how to write `Scribe` in production code**, ApexEloquent's query builder. For the full API signatures, see [API Reference: Scribe](https://krileworks.com/apex-stem/docs/apex-eloquent-api-scribe). For an index of the other ApexEloquent topics, see the [ApexEloquent Guide](https://krileworks.com/apex-stem/docs/apex-eloquent-guide). ## What Scribe Is `Scribe` is a query builder that assembles SOQL through typed method chains. Starting from `Scribe.of(Account.class)` and stacking `field`, `whereEqual`, `orderBy`, and friends, you end up with a complete SOQL string. ```apex Scribe accountScribe = Scribe.of(Account.class) .field('Id') .field('Name') .field('Industry') .whereEqual('Industry', 'Technology') .orderBy('Name', 'ASC') .take(10); // → SELECT id, name, industry FROM Account WHERE Industry = 'Technology' ORDER BY Name ASC LIMIT 10 List accounts = new Eloquent().get(accountScribe); ``` Three key points: - **Immutable**: each method returns a new `Scribe` instance. You can branch midway to derive condition-varied queries without altering the original `Scribe`. - **Query execution is delegated to `IEloquent`**: when you hand a finished `Scribe` to `IEloquent.get(scribe)`, the SOQL gets issued and the data is fetched. This **separation of query construction from execution** is the heart of ApexEloquent — laid out in detail in [Query Delegation Pattern](https://krileworks.com/apex-stem/docs/query-delegation-pattern). - **Mockable via DI**: `IEloquent` is an interface, with `Eloquent` (issues real SOQL) in production and `MockEloquent` (no DB; returns the injected `IEntry` list as-is) in tests, swapped via the Layered Constructor Pattern. The Usecase-side code depends on a field of type `IEloquent`, so the calling shape stays identical in production and tests — only tests get to run logic without touching the DB. Details in [Data Access, DML, IEntry, and Mock](https://krileworks.com/apex-stem/docs/apex-eloquent-data-access). Field names are passed as **strings** rather than `SObjectField` (e.g. `'Id'` / `'Industry__c'`). This frees the builder from Apex's type system and lets queries be assembled dynamically at runtime. ## Choosing What to SELECT ### Single Field vs Multiple Fields Add one at a time with `field(String fieldName)`, or hand over many at once via `fields(List)`. ```apex Scribe scribe = Scribe.of(Opportunity.class) .field('Id') .field('Name') .field('StageName'); // → SELECT id, name, stagename FROM Opportunity ``` `fields(List)` also accepts a pre-declared `List` directly. ```apex List opportunityFields = new List{ 'Id', 'Name', 'StageName', 'CloseDate', 'Amount' }; Scribe scribe = Scribe.of(Opportunity.class) .fields(opportunityFields) .whereEqual('StageName', 'Prospecting'); // → SELECT id, name, stagename, closedate, amount FROM Opportunity WHERE StageName = 'Prospecting' ``` ### Pulling in Parent Fields with parentField Pass `Scribe.asParent('AccountId').field(...)` to `parentField`, and the parent object's fields land in the SELECT clause. ```apex Scribe scribe = Scribe.of(Opportunity.class) .field('Id') .field('Name') .parentField(Scribe.asParent('AccountId').field('Name').field('Industry')) .whereEqual('StageName', 'Closed Won'); // → SELECT id, name, Account.name, Account.industry FROM Opportunity WHERE StageName = 'Closed Won' ``` Child subqueries (`withChildren`) and many-to-many (`through`) are covered in [Parent Fields, Child Subqueries, and Many-to-Many](https://krileworks.com/apex-stem/docs/apex-eloquent-relations). ### allFields for Selecting Everything `allFields()` selects every accessible field on the target object. In production, explicit fields are safer, but it's handy for building test data or for investigation. ## Filtering with WHERE ### Basic WHERE Equality (`whereEqual` / `whereNotEqual`), comparison (`whereGreaterThan` family / `whereLessThan` family), pattern (`whereLike` / `whereNotLike`), list (`whereIn` / `whereNotIn`), multi-select (`whereIncludes` / `whereExcludes`), and null check (`whereNull` / `whereNotNull`) are all available. For signatures and behavior, see [API Reference: Scribe](https://krileworks.com/apex-stem/docs/apex-eloquent-api-scribe). ```apex Scribe scribe = Scribe.of(Opportunity.class) .field('Id') .whereEqual('StageName', 'Prospecting') .whereGreaterThan('Amount', 1000) .whereIn('OwnerId', ownerIds); // Set can be passed as-is // → SELECT id FROM Opportunity WHERE StageName = 'Prospecting' AND Amount > 1000 AND OwnerId IN (...) ``` Successive `whereXxx` calls are joined by **AND** by default. ### Mixing AND and OR To slip in an OR, insert `orCondition()` **before the next where**. ```apex Scribe scribe = Scribe.of(Account.class) .field('Id') .whereEqual('Name', 'A') .orCondition() .whereEqual('Name', 'B') .orCondition() .whereEqual('Name', 'C'); // → SELECT id FROM Account WHERE Name = 'A' OR Name = 'B' OR Name = 'C' ``` **Once you switch to OR, every subsequent where must also be joined by OR**. You can't switch back to AND midway. When you want to mix OR and AND, **put the AND conditions first and the OR conditions at the end**, or use `whereGroup` + `Scribe.asGroup()` to wrap one side in parentheses. ```apex Scribe scribe = Scribe.of(Account.class) .field('Id') .whereGroup( Scribe.asGroup() .whereEqual('Industry', 'Tech') .whereEqual('Name', 'X') ) .orCondition() .whereEqual('BillingCity', 'Tokyo'); // → SELECT id FROM Account WHERE (Industry = 'Tech' AND Name = 'X') OR BillingCity = 'Tokyo' ``` Note: when `whereIn` receives an **empty list**, the SOQL converts it to `Id = null` (a condition guaranteed to be false). This is by design — "if the filter target has 0 items, the result is also 0" — so you don't need an `isEmpty()` check on the caller side. ⚠️ `whereNotIn` goes the other way: given an empty collection it **drops the condition entirely** (no filtering — the all-rows side). Writing `whereIn` while meaning "don't filter when empty" gives you zero rows, so when you want to state which side you mean, use `ignoreWhen` (below). ### Subquery IN The second argument of `whereIn` can take **another `Scribe`**. The two-step SOQL pattern of "first fetch IDs, then use them in the next query" collapses into one. ```apex // Take only opportunities tied to "accounts the current user follows" Scribe followedAccountIds = Scribe.of(AccountShare.class) .field('AccountId') .whereEqual('UserOrGroupId', UserInfo.getUserId()); Scribe oppScribe = Scribe.of(Opportunity.class) .field('Id') .field('Name') .whereIn('AccountId', followedAccountIds); // → SELECT id, name FROM Opportunity WHERE AccountId IN (SELECT accountid FROM AccountShare WHERE UserOrGroupId = '...') ``` This is essentially SOQL's `IN (SELECT ...)` syntax made native. `whereNotIn` also accepts a `Scribe` the same way. ### whereLike Auto-Escapes Against SQL Injection Single quotes (`'`) inside the `pattern` argument to `whereLike(field, pattern)` are auto-escaped. You can pass user input through and not introduce a SOQL injection. ```apex String userInput = "O'Brien"; // a suspicious-looking input Scribe scribe = Scribe.of(Contact.class) .field('Id') .whereLike('LastName', '%' + userInput + '%'); // → ...WHERE LastName LIKE '%O\'Brien%' ``` ## Sorting, LIMIT, FOR UPDATE `orderBy` / `take` / `offset` / `forUpdate` handle sorting, limits, and row locking. ```apex Scribe scribe = Scribe.of(Opportunity.class) .field('Id') .field('CloseDate') .whereEqual('StageName', 'Prospecting') .orderBy('CloseDate', 'DESC') .take(20); // → SELECT id, closedate FROM Opportunity WHERE StageName = 'Prospecting' ORDER BY CloseDate DESC LIMIT 20 ``` Combining `forUpdate` with `orderBy` / `offset` throws an exception; `offset` is capped at 2000. SOQL's constraints get mirrored as build-time checks. Details in [API Reference: Scribe](https://krileworks.com/apex-stem/docs/apex-eloquent-api-scribe). ## Aggregate Queries For cases like "aggregate child-record counts per parent Id", use aggregate queries — they keep Apex SOQL row counts and heap usage in check. ```apex Scribe eventScribe = Scribe.of(Event.class) .field('WhatId') // GROUP BY fields must also appear in SELECT .count('Id', 'eventCount') // COUNT with alias .whereIn('WhatId', opportunityIds) .groupByField('WhatId'); // → SELECT whatid, COUNT(Id) eventCount FROM Event WHERE WhatId IN (...) GROUP BY WhatId List aggregateEntries = new Eloquent().get(eventScribe); for (IEntry aggregateEntry : aggregateEntries) { Id whatId = (Id) aggregateEntry.get('WhatId'); Integer cnt = ((Decimal) aggregateEntry.get('eventCount')).intValue(); } ``` The six aggregate functions are `count` / `countDistinct` / `sum` / `average` / `max` / `min`. GROUP BY uses `groupByField` / `groupByFields` / `groupByParent`; HAVING is assembled with `havingCondition(Scribe.asHaving()...)`. For signatures, see [API Reference: Scribe](https://krileworks.com/apex-stem/docs/apex-eloquent-api-scribe). **Aliases are required**. Each aggregate function takes `count(field, alias)` — the `alias` cannot be omitted — and you retrieve the value via `aggregateEntry.get('alias')`. Salesforce's standard `AggregateResult` defaults to field names like `expr0` / `expr1` / ... (in declaration order), which is a classic beginner trap; ApexEloquent enforces aliases at the method signature level to dodge that pitfall. ### Aggregating and Grouping by Parent Fields When you want to aggregate or GROUP BY a **parent object's field**, you **cannot write `field('Account.Industry')` directly**. Everything parent-related has to go through `Scribe.asParent(...)`. Example: aggregate per Product × Opportunity — total line-item sum per product, plus the maximum amount on the opportunity each line item belongs to. ```apex Scribe scribe = Scribe.of(OpportunityLineItem.class) .field('Product2Id') .sum('TotalPrice', 'totalPrice') .parentField( Scribe.asParent('OpportunityId') .field('Id') .max('Amount', 'maxAmount') ) .groupByField('Product2Id') .groupByParent( Scribe.asParent('OpportunityId').groupByField('Id') ); // → SELECT product2id, SUM(TotalPrice) totalPrice, Opportunity.id, MAX(Opportunity.Amount) maxAmount FROM OpportunityLineItem GROUP BY Product2Id, Opportunity.Id ``` Points: - Parent SELECT fields and aggregate functions are bundled inside `parentField(Scribe.asParent('OpportunityId').field(...).max(...))` - GROUP BY on a parent field is expressed as `groupByParent(Scribe.asParent('OpportunityId').groupByField('Id'))` - A HAVING clause referring to a parent-derived alias (`maxAmount`) uses the same alias: `Scribe.asHaving().whereGreaterThan('maxAmount', 1000)` ### Notes on Aggregate Queries - **GROUP BY fields must also appear in SELECT** (don't forget `field('Product2Id')`) - **Aggregate results come back as `Decimal`**. To put them into an `Integer`, cast like `((Decimal) aggregateEntry.get('alias')).intValue()` - The retrieval API stays as the normal `get(scribe)`. **Whether the query is aggregate or normal is auto-detected by the framework** - Reusing the same alias across multiple aggregate functions throws (don't duplicate `total` between `sum('A','total')` and `max('B','total')`) - Combining child subqueries (`withChildren`) with aggregate functions is forbidden by SOQL constraints ## Building Queries Dynamically Queries like search filters — "only add the condition when there is input" — come out as **a single chain with no branching**, thanks to `ignoreWhen`. ```apex Scribe scribe = Scribe.of(Opportunity.class) .field('Id') .field('Name') .whereEqual('Industry', industry).ignoreWhen(industry == null) .whereIn('StageName', stages).ignoreWhen(stages.isEmpty()) .whereGreaterThan('CloseDate', closeAfter).ignoreWhen(closeAfter == null); List opps = this.fetchEloquent.get(scribe); // When all three are provided: // → SELECT id, name FROM Opportunity WHERE Industry = 'Technology' AND StageName IN (...) AND CloseDate > 2026-01-01 // When none are: // → SELECT id, name FROM Opportunity ``` `ignoreWhen(true)` **retracts the immediately preceding condition**. It also sidesteps the trap where an empty `whereIn` collapses to zero rows (see [API Reference: Scribe](https://krileworks.com/apex-stem/docs/apex-eloquent-api-scribe)). > ⚠️ **The `null` case needs v3.5.0 or later.** The **12 methods that reject null** — `whereGreaterThan`, `whereIn` and friends — used to throw the moment you chained them, so a following `ignoreWhen` was never reached. From v3.5.0 the error is **deferred to build time**, which lets `ignoreWhen` retract it. The `closeAfter == null` line above is exactly this case. ### Stacking with if On versions without `ignoreWhen`, you stack conditions with branches. `Scribe` is immutable, so **reassignment** (`scribe = scribe.whereXxx(...)`) is required. ```apex Scribe scribe = Scribe.of(Opportunity.class) .field('Id') .field('Name'); if (industry != null) { scribe = scribe.whereEqual('Industry', industry); } if (stages != null && !stages.isEmpty()) { scribe = scribe.whereIn('StageName', stages); } ``` This still works, but as conditions pile up the *shape of the query* gets scattered across branches and stops being readable. Prefer `ignoreWhen` for new code. ### Deriving queries Since each method returns a **new `Scribe` instance**, you can derive from a single query without changing the original — for example, building both a count query and a list query against the same set of opportunities. ```apex Scribe baseScribe = Scribe.of(Opportunity.class) .whereEqual('StageName', 'Prospecting') .whereGreaterThan('Amount', 1000); // Just the count Scribe countScribe = baseScribe.count('Id', 'cnt'); // → SELECT COUNT(Id) cnt FROM Opportunity WHERE StageName = 'Prospecting' AND Amount > 1000 // The list itself Scribe listScribe = baseScribe .field('Id') .field('Name') .orderBy('CloseDate', 'ASC') .take(50); // → SELECT id, name FROM Opportunity WHERE StageName = 'Prospecting' AND Amount > 1000 ORDER BY CloseDate ASC LIMIT 50 ``` `baseScribe` stays unchanged and is reusable as the source for both derivations. ## Read Next - [Data Access, DML, IEntry, and Mock](https://krileworks.com/apex-stem/docs/apex-eloquent-data-access): execute the assembled `Scribe` with `IEloquent` and work with `IEntry` - [Parent Fields, Child Subqueries, and Many-to-Many](https://krileworks.com/apex-stem/docs/apex-eloquent-relations): queries using relations, and mocking parent-child structures with `MockEntry` - [ApexEloquent Guide](https://krileworks.com/apex-stem/docs/apex-eloquent-guide): back to the ApexEloquent Guide index ============================================================================== Source: https://krileworks.com/document/apex-eloquent-data-access.md Page: https://krileworks.com/apex-stem/docs/apex-eloquent-data-access ============================================================================== # Data Access, DML, IEntry, and Mock This document is a usage guide focused on **how to write ApexEloquent's data-access layer in production code**. For the full API signatures, see [API Reference: IEloquent / Eloquent / MockEloquent](https://krileworks.com/apex-stem/docs/apex-eloquent-api-eloquent) and [API Reference: IEntry / Entry / MockEntry](https://krileworks.com/apex-stem/docs/apex-eloquent-api-entry). For how to build queries, see [Building Queries with Scribe](https://krileworks.com/apex-stem/docs/apex-eloquent-scribe-guide). For the rest of the ApexEloquent topics, see the [ApexEloquent Guide](https://krileworks.com/apex-stem/docs/apex-eloquent-guide). ## What IEloquent Is `IEloquent` is the interface that abstracts data access (SOQL / DML). Production uses `Eloquent` (a wrapper over standard SOQL / DML); tests use `MockEloquent` (a behavior mock without a DB) — both are swapped in without changing the calling code. ```apex public with sharing class FindActiveOpportunitiesUsecase { private final IEloquent fetchEloquent; public FindActiveOpportunitiesUsecase() { this(null); } @TestVisible private FindActiveOpportunitiesUsecase(IEloquent fetchEloquent) { this.fetchEloquent = fetchEloquent ?? new Eloquent(); } public List invoke() { Scribe scribe = Scribe.of(Opportunity.class) .field('Id') .field('Name') .whereEqual('IsClosed', false); return this.fetchEloquent.get(scribe); } } ``` For "two constructors that simultaneously support a production default and test-side DI", see [Layered Constructor Pattern](https://krileworks.com/apex-stem/docs/layered-constructor-pattern). ## Data Retrieval ### Retrieval Methods In business logic, the default is **`get(scribe)` returning `List`**. 0 hits yields an empty list. When you want just one record, `first` (returns `null` on 0 hits) or `firstOrFail` (throws on 0 hits) is handy. `getAsSObject` conversion is a last resort — use it only when you have a concrete reason to hand off an `SObject` instance (e.g. directly passing it to an external API like `Messaging.SingleEmailMessage`). Similarly, `rawSoql(soql)` is the last resort for SOQL that `Scribe` can't express; it disables the `MockEntry` SELECT-omission detection. For the full list of signatures, see [API Reference: IEloquent](https://krileworks.com/apex-stem/docs/apex-eloquent-api-eloquent). ### The Benefits of Staying on IEntry `IEntry` wraps `SObject` and gives you four benefits unique to ApexEloquent. 1. **False-positive detection for unselected fields**: if production code accesses a field that wasn't called out with `field()` in the `Scribe`, an exception fires at test time (via `MockEntry`). Early conversion to `SObject` strips that protection, and the classic "tests pass but production reads empty" incident creeps back in. 2. **Freedom in building mock data**: `MockEntry.set()` accepts non-writable fields — relationship fields, formula fields, rollups, auto-number — that you can't normally write. When the logic depends on these, test data you can't build with raw `SObject` becomes trivial via `IEntry`. 3. **Retrieve → edit → update flows stay on IEntry**: mutate with `entry.put('Industry__c', value)`, hand it straight to `eloquent.doUpdate(List)`. No conversion to `SObject` needed. 4. **No distinction between SObject and AggregateResult**: results of normal queries and aggregate queries (`AggregateResult`) both come back as `IEntry`. `entry.get('Industry')` for **an object field**, `entry.get('totalAmount')` for **an aggregate alias** — same shape. The consumer doesn't have to care whether it's an `SObject` or `AggregateResult`. ## Working with IEntry ### Reading and Writing Fields ```apex List accountEntries = eloquent.get(accountScribe); for (IEntry accountEntry : accountEntries) { // Id and Name have dedicated getters (no cast needed) Id accountId = accountEntry.getId(); String name = accountEntry.getName(); // Other fields require a cast String industry = (String) accountEntry.get('Industry'); Boolean isActive = (Boolean) accountEntry.get('Active__c'); // Writing accountEntry.put('Industry', 'Technology'); } ``` `getId` / `getName` are dedicated getters that don't need casting. Other fields go through `get(fieldName)` and require a cast on the return. Writes are `put(fieldName, value)`. For parent and child record access see [Parent Fields, Child Subqueries, and Many-to-Many](https://krileworks.com/apex-stem/docs/apex-eloquent-relations); for signatures see [API Reference: IEntry](https://krileworks.com/apex-stem/docs/apex-eloquent-api-entry). ### Variable Naming Tips When using abstracted types (`IEntry` / `Scribe` / `IEloquent`), **let the variable name make the underlying SObject explicit** — it reads better. ```apex // Bad: short names like r or e force the reader to chase the type for (IEntry e : eloquent.get(scribe)) { ... } // Good: consistent {SObjectName}Entry form for (IEntry accountEntry : eloquent.get(accountScribe)) { ... } ``` ## Running DML `IEloquent` also wraps standard DML. The four methods `doInsert` / `doUpdate` / `doUpsert` / `doDelete` each have `SObject` / `IEntry` / `List` / `List` overloads. ```apex // IEntry retrieved via Scribe can be updated as-is List oppEntries = this.eloquent.label(LBL_FETCH).get(oppScribe); for (IEntry oppEntry : oppEntries) { oppEntry.put('Industry__c', 'Technology'); } this.eloquent.label(LBL_UPDATE).doUpdate(oppEntries); ``` Default to the `List<>` versions to keep things bulk-aligned. Single-record versions are for cases where exactly one record is guaranteed. For each overload's signature, see [API Reference: IEloquent](https://krileworks.com/apex-stem/docs/apex-eloquent-api-eloquent). ### Execution Mode (v3 line) In the v3 line, SOQL / DML default to **user mode** — honouring the running user's field and object permissions. Only work that must complete no matter who triggered it (aggregation, stamping, data migration) opts out explicitly with `systemMode()`. ```apex this.eloquent.systemMode().label(LBL_UPDATE).doUpdate(entries); ``` Calling it once applies to every subsequent operation on that instance (sticky). On `MockEloquent` it is a no-op, so **unit tests can be written without thinking about the mode**. ⚠️ `systemMode()` only lifts **field and object permissions**. **Sharing (record visibility) is a separate axis** — to lift that, the calling class must be declared `without sharing`. ## Testing with MockEloquent `MockEloquent` is the test implementation of `IEloquent`. By DI-ing it into a Usecase via the Layered Constructor Pattern, you can verify behavior without ever touching the DB. ### Constructors `new MockEloquent()` for empty, `new MockEloquent(entry)` to return one record, `new MockEloquent(List)` to return multiple. ```apex MockEntry oppEntry = MockEntry.of(Opportunity.class) .autoId(1) .set('Name', 'Test Opp') .set('Amount', 1000); IEloquent fetchEloquent = new MockEloquent(new List{ oppEntry }); ``` ### Verifying DML with the Spy Instead of really running `doInsert` / `doUpdate` / `doUpsert` / `doDelete`, `MockEloquent` **records the records it was handed**. Tests can then check after the fact that the DML you expected actually happened. | Method | Content | |---|---| | `upsertedRecordsAt(String label)` | Records passed to `doInsert` / `doUpdate` / `doUpsert` under that label (`List`) | | `deletedCountAt(String label)` | `doDelete` count under that label (`Integer`) | > In tests that don't use labels (lenient), pass `'default'`. The `upsertedRecords` / `deletedCount` **fields** remain for backward compatibility but are `@deprecated`; new code should use the `*At(...)` methods. ```apex MockEloquent mock = (new MockEloquent()) .attach(CopyAccountIndustryToOpportunityUsecase.LBL_FETCH, new List{ oppEntry }); (new CopyAccountIndustryToOpportunityUsecase(oppIds, mock)).invoke(); // Did exactly 1 update happen as expected? List updated = mock.upsertedRecordsAt(CopyAccountIndustryToOpportunityUsecase.LBL_UPDATE); Assert.areEqual(1, updated.size()); // Did the expected value land? Assert.areEqual('Technology', ((Opportunity) updated[0]).Industry__c); ``` ### Mocking failure paths In Salesforce, **reproducing a failure costs an order of magnitude more than reproducing success**. To make real DML actually fail you have to add a validation rule, strip a field permission, contend for a row lock. It is slow, and because it leans on org state, it breaks easily. `MockEloquent` lets you **declare the failure by name** instead. #### Failure is specified along four axes The `failOn*` family looks like a lot of methods, but it is really a combination of four axes. Once you see them, reading and writing gets much easier. | Axis | What you specify | Default | |---|---|---| | **What** fails | `failOnDoUpdate()` / `failOnGet()` / … one per method | — | | **Why** it fails | the `Exception` you pass | a generic test exception | | **Where** it fails | `.whenLabel(label)` | aimed at label-less calls (`'default'`) | | **How often** it fails | stack `failOn*` / `.repeat()` | just the next call | The four are orthogonal; add only what you need. ```apex MockEloquent mock = (new MockEloquent()) .failOnDoUpdate(new DmlException('Simulated failure')) // what + why .whenLabel(YourUsecase.LBL_UPDATE); // where try { (new YourUsecase(input, mock)).invoke(); Assert.fail('An exception should have been thrown'); } catch (DmlException e) { Assert.isTrue(TraceFlow.isLastAbort()); } ``` #### Why narrowing "where" matters With `whenLabel` you drop **just that one call site** and let the rest run normally. "Aggregation succeeded, only the final save failed" writes out directly. In a Usecase firing several DMLs, without this a test can't tell you which one fell over. A configuration without `whenLabel` is **aimed at label-less calls (`'default'`)**. If production code uses labels, `whenLabel` is effectively mandatory (forgetting it is detected — see below). A call whose label doesn't match **does not consume the configuration**. A later matching call picks it up, so ordering doesn't matter when you plant them. #### "How often" stacks `failOn*` entries **queue up**. Plant several against the same method and they fail in order. ```apex // The 1st and 2nd doUpdate fail; the 3rd succeeds MockEloquent mock = (new MockEloquent()) .failOnDoUpdate(new DmlException('first')) .failOnDoUpdate(new DmlException('second')); ``` That is how you write the success side of a retry. Queues are independent per label, so entries carrying `whenLabel` stack the same way. `.repeat()`, by contrast, repeats the entry you just planted **forever** (no count). ```apex // Fails no matter how many times it's called MockEloquent mock = (new MockEloquent()) .failOnDoUpdate(new DmlException('Always fails')) .whenLabel(YourUsecase.LBL_UPDATE) .repeat(); ``` Use it for the **abort side** of "retry up to 3 times, then give up". Not having to count calls makes the intent clearer. #### Queues are per label Failure configurations queue up **per label** (an entry without `whenLabel` is aimed at label-less calls, `'default'`). So you can plant **different failures against different labels** on the same method, in any order. ```apex // Fail the fetch for one reason and the save for another MockEloquent mock = (new MockEloquent()) .failOnGet(new QueryException('fetch failed')).whenLabel(YourUsecase.LBL_FETCH) .failOnDoUpdate(new DmlException('save failed')).whenLabel(YourUsecase.LBL_UPDATE); ``` #### Retries can keep the same label The "once only" rule on labels counts **successful** operations. A **failed operation releases its label**, so production code that catches and retries under the same label tests as-is. ```apex // Fails the first time, succeeds the second — same label throughout MockEloquent mock = (new MockEloquent()) .attach(YourUsecase.LBL_UPDATE, entries) .failOnDoUpdate(new DmlException('first attempt')).whenLabel(YourUsecase.LBL_UPDATE); ``` Add `.repeat()` and it keeps failing however many times you retry, which is how you check the "give up at the limit" path. #### Pitfall: keep the return value The test-setup builders (`attach` / `failOn*` / `whenLabel` / `failSave` / `repeat`) all **return a new instance rather than mutating `this`**. Drop the return value and the configuration disappears. ```apex // ❌ Does nothing — mock itself has no configuration on it MockEloquent mock = new MockEloquent(); mock.failOnDoUpdate(new DmlException('...')); // ✅ Keep the chain, or reassign MockEloquent mock = (new MockEloquent()) .attach(YourUsecase.LBL_FETCH, entries) .failOnDoUpdate(new DmlException('...')) .whenLabel(YourUsecase.LBL_UPDATE); ``` #### Pitfall: a forgotten whenLabel is detected In a test that uses labels (strict mode), forgetting `whenLabel` aims the configuration at label-less calls. But strict mode requires a label on every operation, so **that configuration can never fire**. Silently doing nothing would fail in a baffling way — the exception never arrives, so your `Assert.fail` trips instead. So it throws a diagnostic exception on the spot, naming the label you should have used. #### failSave: not an exception, just "some of it wasn't saved" Where `failOn*` means "calling it throws", `failSave` is a different kind of failure: **the call itself succeeds but the named record is not saved**. It reproduces the partial failure of an `allOrNone` DML. ```apex Id badId = MockEntry.of(Account.class).autoId(2).getId(); MockEloquent mock = (new MockEloquent()) .failSave(badId, 'Rejected by a validation rule'); ``` - `allOrNone = false` — that record's `SaveResult` carries `success = false` and your message, and it is **not recorded by the spy** (it was never saved) - `allOrNone = true` — as with real all-or-nothing DML, **the whole operation throws** before anything is recorded The target is named by **record Id** rather than index, so pair it with `MockEntry.autoId()`. Because it doesn't depend on list order, it survives records being added later. You can verify ETL / incremental-sync paths that "log the failed records and carry on" without touching org configuration. > For the full list of `failOn*` methods, see [API Reference: MockEloquent](https://krileworks.com/apex-stem/docs/apex-eloquent-api-eloquent). ### Distinguishing Multiple Queries on the Same IEloquent: Label Multiplexing (Important) `MockEloquent` **does not evaluate** the `Scribe`'s WHERE conditions. It returns the injected `IEntry` list as-is. This means cases like "I want one `IEloquent` to distinguish between `last month's query` and `this year's query`" **can't be handled out of the box**. **`IEloquent.label(String)`**, added in v2.1, resolves this. By **multiplexing** a single `IEloquent` with labels, you can use the same instance — sorted by label — instead of DI-ing a separate `IEloquent` per purpose. ```apex public with sharing class AggregateAccountActivityUsecase { @TestVisible static final String LBL_LAST_MONTH = 'lastMonthEvent'; @TestVisible static final String LBL_THIS_YEAR = 'thisYearEvent'; @TestVisible static final String LBL_ACCOUNT_UPDATE = 'accountUpdate'; private final Set accountIds; private final IEloquent eloquent; public AggregateAccountActivityUsecase(Set accountIds) { this(accountIds, null); } @TestVisible private AggregateAccountActivityUsecase( Set accountIds, IEloquent eloquent ) { this.accountIds = accountIds; this.eloquent = eloquent ?? new Eloquent(); } public void invoke() { List lastMonthEvents = this.eloquent.label(LBL_LAST_MONTH).get(lastMonthScribe); List thisYearEvents = this.eloquent.label(LBL_THIS_YEAR).get(thisYearScribe); // ... aggregate ... this.eloquent.label(LBL_ACCOUNT_UPDATE).doUpdate(updatedAccounts); } } ``` In tests, preload each label's query result on a single `MockEloquent` via `.attach(LBL_LAST_MONTH, ...)` / `.attach(LBL_THIS_YEAR, ...)`, and pull DML outcomes via `mock.upsertedRecordsAt(LBL_ACCOUNT_UPDATE)`. You preserve the same per-purpose isolation as separate-DI, but the constructor signature collapses to a single dependency. Note: the first time `.label()` is called, **opt-in strict mode** turns on for that instance, and every subsequent operation must carry a label (the "forgot to label" footgun is closed at runtime). Details in [API Reference: IEloquent](https://krileworks.com/apex-stem/docs/apex-eloquent-api-eloquent). ## Building MockEntry `MockEntry` is the test-side implementation of `IEntry`. It lets you build test data that you can't build with raw `SObject`. ### Basics ```apex MockEntry oppEntry = MockEntry.of(Opportunity.class) .autoId(1) .set('Name', 'Test Opp') .set('Amount', 1000); ``` `MockEntry.of(Type)` creates the entry. `.set` writes to a field (non-writable fields are allowed). `.autoId` auto-generates an 18-character Id. `.alias` names the entry so you can retrieve the Id later. For the signature list, see [API Reference: MockEntry](https://krileworks.com/apex-stem/docs/apex-eloquent-api-entry). ### Baking in the SELECT contract with fetchedBy Entries returned through `MockEloquent`'s `attach` **pick up a SELECT contract automatically** from the `Scribe` passed to `get(scribe)` — 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 one after the fact. ```apex MockEntry card = MockEntry.of(BusinessCard__c.class) .autoId(1) .set('CompanyName__c', 'Acme') .fetchedBy(RematchCompanyCardsHandler.scope()); ``` This matters mostly for **batches**. Records reaching `execute(bc, scope)` never pass through `IEloquent`, so production is covered by the platform (they are real query results) while tests check nothing — you built those entries yourself. Pull the query construction into a `@TestVisible` method and the test can hand over the same `Scribe` production uses (see [API Reference: Scribe](https://krileworks.com/apex-stem/docs/apex-eloquent-api-scribe)). ### Retrieving Generated Ids with alias Sometimes you want to use the Id generated by `autoId` in a test assertion. ```apex MockEntry oppEntry = MockEntry.of(Opportunity.class) .alias('opp') .autoId(1); Id oppId = oppEntry.getAliasId('opp'); // pull out the generated Id // In the test: Opportunity updated = (Opportunity) mock.upsertedRecordsAt(MyUsecase.LBL_UPDATE)[0]; Assert.areEqual(oppId, updated.Id); ``` ### Bulk-Generating with times() ```apex List contactEntries = MockEntry.of(Contact.class) .autoId('{#}') .set('LastName', 'Contact-{#}') .alias('con_{#}') .times(3); // → 3 records: con_1 / con_2 / con_3, each with Id and LastName expanded ``` ### Setting Multiple Fields at Once with template When you have "default values to reuse across multiple tests", hand them over as a `Map` to avoid repeating `.set` in every test. ```apex Map defaults = new Map{ 'StageName' => 'Prospecting', 'CloseDate' => Date.today().addDays(30), 'Amount' => 1000 }; MockEntry oppEntry = MockEntry.of(Opportunity.class) .template(defaults) .set('Name', 'Specific name for this test'); // override or add per-field ``` ### Mocking Aggregate Results When mocking aggregate-query results, use `MockEntry.asAggregateResult()`. This reflects the property that "aggregate results aren't tied to an SObject type", removing the need to pick a specific SObject type via `MockEntry.of(SomeType.class)` (and reducing cognitive noise for the reader). ```apex MockEloquent eventEloquent = new MockEloquent( new List{ MockEntry.asAggregateResult() .set('WhatId', oppAId) .set('eventCount', 3), MockEntry.asAggregateResult() .set('WhatId', oppBId) .set('eventCount', 1) } ); ``` Passing `Decimal` values to `set` (same as real SOQL aggregate results) means the logic-side `((Decimal) entry.get('eventCount')).intValue()` cast keeps working as-is. ### Mocking Parent-Child Structures To view parent from a child, use `setParent`. To view children from a parent, use `setChildren`. The code's indentation directly mirrors the relation structure — re-reading later, "what children hang off this parent" is visible at a glance. ```apex // Hang multiple Contact / Opportunity under a parent Account MockEntry accountEntry = MockEntry.of(Account.class) .alias('acc').autoId(1) .set('Name', 'Acme Corporation') .setChildren('Contacts', new List{ MockEntry.of(Contact.class).autoId(1).set('FirstName', 'John'), MockEntry.of(Contact.class).autoId(2).set('FirstName', 'Jane') }) .setChildren('Opportunities', new List{ MockEntry.of(Opportunity.class).autoId(1).set('Name', 'Deal 1').set('Amount', 100000), MockEntry.of(Opportunity.class).autoId(2).set('Name', 'Deal 2').set('Amount', 150000) }); ``` The reverse (viewing parent Account from child Opportunity) uses `setParent`: ```apex MockEntry oppEntry = MockEntry.of(Opportunity.class) .autoId(1) .set('Name', 'Major Deal') .setParent('AccountId', MockEntry.of(Account.class).set('Name', 'Acme Corporation').set('Type', 'Customer') ); ``` The Id linkage between parent and child (e.g. filling `Contact.AccountId` with the parent's Id) is handled internally by `MockEntry` — you don't fill it by hand. ⚠️ If you specified the child relationship name in `Scribe` via `relationName('CustomOpportunities__r')`, use the same string as the first argument of `setChildren`. For the full picture of relation operations (including many-to-many and junction objects), see [Parent Fields, Child Subqueries, and Many-to-Many](https://krileworks.com/apex-stem/docs/apex-eloquent-relations). ## Read Next - [Parent Fields, Child Subqueries, and Many-to-Many](https://krileworks.com/apex-stem/docs/apex-eloquent-relations): retrieving and mocking relations - [Building Queries with Scribe](https://krileworks.com/apex-stem/docs/apex-eloquent-scribe-guide): the full picture of the query builder - [Layered Constructor Pattern](https://krileworks.com/apex-stem/docs/layered-constructor-pattern): the design for DI'ing `IEloquent` by purpose - [Test Strategy](https://krileworks.com/apex-stem/docs/test-strategy): where Usecase unit tests centered on `MockEloquent` sit - [ApexEloquent Guide](https://krileworks.com/apex-stem/docs/apex-eloquent-guide): back to the ApexEloquent Guide index ============================================================================== Source: https://krileworks.com/document/apex-eloquent-relations.md Page: https://krileworks.com/apex-stem/docs/apex-eloquent-relations ============================================================================== # Parent Fields, Child Subqueries, and Many-to-Many This document covers how to work with relations in ApexEloquent. For query assembly via `Scribe`, see [Building Queries with Scribe](https://krileworks.com/apex-stem/docs/apex-eloquent-scribe-guide). For the basics of `IEloquent` / `IEntry`, see [Data Access, DML, IEntry, and Mock](https://krileworks.com/apex-stem/docs/apex-eloquent-data-access). ## Retrieving Parent Fields To pull a parent object's field into the SELECT — like "from an Opportunity, fetch the parent Account's industry" — use `parentField`. ```apex Scribe oppScribe = Scribe.of(Opportunity.class) .field('Id') .field('Name') .parentField(Scribe.asParent('AccountId').field('Name').field('Industry')) .whereEqual('StageName', 'Closed Won'); // → SELECT id, name, Account.name, Account.industry FROM Opportunity WHERE StageName = 'Closed Won' List oppEntries = this.fetchEloquent.get(oppScribe); for (IEntry oppEntry : oppEntries) { IEntry accountEntry = oppEntry.getParent('AccountId'); String accountName = accountEntry.getName(); String industry = (String) accountEntry.get('Industry'); } ``` - Create a parent-relation Scribe with `Scribe.asParent('AccountId')` and specify the parent fields via `.field(...)` - Pass it to `parentField(...)`, and the SOQL SELECT clause expands to `Account.Name` and friends - After fetching, retrieve the parent `IEntry` via `IEntry.getParent('AccountId')` ### Accessing a Non-parentField'd Parent Throws Calling `IEntry.getParent('AccountId')` on a parent that wasn't declared with `parentField` in `Scribe` throws at test time (via `MockEntry`). This is the mechanism that surfaces "parent-field SELECT omissions" in tests rather than in production. ## Retrieving Child Subqueries To pull child records via a subquery — like "from an Account, fetch its list of child Opportunities" — use `withChildren`. ```apex Scribe accountScribe = Scribe.of(Account.class) .field('Id') .field('Name') .withChildren( Scribe.asChild(Opportunity.class) .field('Id') .field('Name') .field('StageName') ) .whereEqual('Type', 'Customer'); // → SELECT id, name, (SELECT id, name, stagename FROM Opportunities) FROM Account WHERE Type = 'Customer' List accountEntries = this.fetchEloquent.get(accountScribe); for (IEntry accountEntry : accountEntries) { List oppEntries = accountEntry.getChildren('Opportunity'); for (IEntry oppEntry : oppEntries) { // ... } } ``` - Create a child-subquery `Scribe` with `Scribe.asChild(Opportunity.class)` - Pass it to `withChildren(...)`, and the SOQL SELECT clause expands as a subquery - After fetching, retrieve the child `IEntry` list via `IEntry.getChildren('Opportunity')` In production, `Entry` resolves the `getChildren` argument as **either an object name or a relationship name** (`'Opportunity'` / `'Opportunities'`) — object name first, falling back to relationship name. That said, **`MockEntry` checks against the key you declared on the `Scribe`**, so call it with the name you used there if you want the test to pass (see "Mocking child records" below). **Anything that passes in tests will pass in production**; the reverse is not guaranteed. ### Fetching Multiple Child Subqueries in Parallel Multiple child objects can be fetched **in parallel** under the same parent. Chain `withChildren` twice and each becomes an independent child subquery. ```apex Scribe scribe = Scribe.of(Account.class) .field('Id') .withChildren( Scribe.asChild(Opportunity.class).field('Id').whereNotNull('Name') ) .withChildren( Scribe.asChild(Contact.class).field('Id').whereNotNull('Email') ) .whereEqual('Name', 'Test Account'); // → SELECT id, (SELECT id FROM Opportunities WHERE Name != NULL), (SELECT id FROM Contacts WHERE Email != NULL) FROM Account WHERE Name = 'Test Account' ``` ### Nested Child Subqueries (Children of Children) Calling `withChildren(...)` inside a `Scribe.asChild(...)` gives you nested subqueries that include grandchildren (up to four levels deep). ```apex Scribe scribe = Scribe.of(Account.class) .field('Id') .withChildren( Scribe.asChild(Contact.class) .field('Id') .whereNotNull('Name') .withChildren( Scribe.asChild(Opportunity.class).field('Id').whereNotNull('Email') ) ) .whereEqual('Name', 'Test Account'); // → SELECT id, (SELECT id, (SELECT id FROM Opportunities WHERE Email != NULL) FROM Contacts WHERE Name != NULL) FROM Account WHERE Name = 'Test Account' ``` On the consumer side, walk through nested `getChildren` calls. ```apex for (IEntry accountEntry : accountEntries) { for (IEntry contactEntry : accountEntry.getChildren('Contact')) { for (IEntry oppEntry : contactEntry.getChildren('Opportunity')) { // ... } } } ``` ### Make relationName Explicit When the Child Relation Is Ambiguous When multiple lookup fields reference the same object (e.g. `Opportunity.AccountId` and `Opportunity.CustomAccount__c` both pointing at `Account`), the child subquery becomes **ambiguous about which relation to return**, and `Scribe` errors out as-is. In that case, use `relationName(...)` to disambiguate. ```apex Scribe accountScribe = Scribe.of(Account.class) .field('Id') .withChildren( Scribe.asChild(Opportunity.class) .relationName('CustomOpportunities__r') // ← Custom Relationship Name .field('Id') .field('Name') ); // → SELECT id, (SELECT id, name FROM CustomOpportunities__r) FROM Account // Use the same relation name on the consumer side accountEntry.getChildren('CustomOpportunities__r'); ``` ## Filtering by Parent Conditions To filter a child object based on the **parent's** condition, use `parentCondition`. ```apex // Fetch OpportunityLineItem where the parent Opportunity's Name starts with "Test%" Scribe scribe = Scribe.of(OpportunityLineItem.class) .field('Id') .field('Quantity') .parentCondition( Scribe.asParent('OpportunityId').whereLike('Name', 'Test%') ); // → SELECT id, quantity FROM OpportunityLineItem WHERE Opportunity.Name LIKE 'Test%' ``` Chain WHERE-family methods onto `Scribe.asParent('OpportunityId')` and embed it via `parentCondition`. `parentCondition` **does not include parent fields in the SELECT — it uses the parent only as a condition**. If you also want parent fields in the SELECT, combine with `parentField`. ### Joining Parent Conditions with OR `orCondition()` is usable inside the `Scribe.asParent(...)` of a `parentCondition`, so conditions like "the parent's Name or the parent's Type matches" work fine. ```apex Scribe scribe = Scribe.of(Opportunity.class) .field('Id') .parentField(Scribe.asParent('AccountId').field('Name').field('Id')) .whereEqual('Name', 'Test Opportunity') .parentCondition( Scribe.asParent('AccountId') .whereEqual('Name', 'Test Account') .orCondition() .whereEqual('Type', 'Test Type') ); // → SELECT id, Account.name, Account.id FROM Opportunity WHERE Name = 'Test Opportunity' AND (Account.Name = 'Test Account' OR Account.Type = 'Test Type') ``` A `parentCondition` holding two or more conditions is **wrapped in parentheses as a single structural unit**. Without them the SOQL would read `A AND B OR C` — AND and OR mixed at one nesting level — which SOQL rejects with `unexpected token: OR`. > ⚠️ **Before v3.5.0 the parentheses were missing and the query failed at run time.** `toSoql()` succeeded while only the real query died, which made it hard to spot. Use v3.5.0 or later if you combine a multi-condition `parentCondition` with any other condition. `parentField` and `parentCondition` coexist and reflect independently in the SELECT and WHERE clauses. ## Many-to-Many (Junction Object) For retrieval through a junction object — Salesforce's way of expressing many-to-many — use `asThrough` + `through`. A clear standard-object example: the relationship between an Order (`Order`) and a Product (`Product2`). Between them sits the order item (`OrderItem`) as a junction, referencing `Product2` via `OrderItem.Product2Id` and `Order` via `OrderItem.OrderId`. "Fetch the products an order handles": ```apex Scribe scribe = Scribe.of(Order.class) .field('Id') .through( Scribe.asThrough(OrderItem.class, 'Product2Id') .field('Name') .field('ProductCode') .whereEqual('IsActive', true) ); // → SELECT id, (SELECT product2id, Product2.name, Product2.productcode FROM OrderItems WHERE Product2.IsActive = true) FROM Order ``` Points: - `Scribe.asThrough(OrderItem.class, 'Product2Id')` declares "through `OrderItem`, fetch what `Product2Id` points to (= `Product2`)" - `.field('Name')` / `.field('ProductCode')` — **write field names of the destination (`Product2`)**. The generated SOQL auto-expands them to `Product2.Name` / `Product2.ProductCode` - `.whereEqual('IsActive', true)` etc. are also based on the destination (`Product2`). The SOQL becomes `WHERE Product2.IsActive = true` On the consumer side, retrieve via `IEntry.getThrough(junctionName, relatedKey)`. As with `getChildren`, the first argument resolves as **either the junction's object name or its relationship name**. ```apex List orderEntries = this.fetchEloquent.get(scribe); for (IEntry orderEntry : orderEntries) { List productEntries = orderEntry.getThrough('OrderItem', 'Product2Id'); for (IEntry productEntry : productEntries) { String name = (String) productEntry.get('Name'); String code = (String) productEntry.get('ProductCode'); } } ``` Even via a junction, you can use `relationName(...)` to disambiguate when the relation is ambiguous — same idea as with child subqueries. Useful when the same parent has multiple lookups. ## Mocking Parent-Child with MockEntry To assemble parent-child relationships as test data, use `MockEntry.setParent` and `MockEntry.setChildren`. ### Mocking the Parent ```apex MockEntry oppEntry = MockEntry.of(Opportunity.class) .alias('opp').autoId(1) .set('Name', 'Test Opp') .setParent('AccountId', MockEntry.of(Account.class) .set('Name', 'Parent Account') .set('Industry', 'Technology') ); // In the test, hand it off to the consumer code and access via getParent IEntry accountEntry = oppEntry.getParent('AccountId'); Assert.areEqual('Technology', (String) accountEntry.get('Industry')); ``` `setParent('AccountId', ...)` hangs an Account `MockEntry` as "the parent via the Opportunity's `AccountId`". ### Mocking Children ```apex MockEntry accountEntry = MockEntry.of(Account.class) .alias('acc').autoId(1) .set('Name', 'Acc Co.') .setChildren('Opportunity', MockEntry.of(Opportunity.class) .autoId('{#}') .set('Name', 'Opp-{#}') .set('StageName', 'Prospecting') .times(3) ); // In the test, access via getChildren List oppEntries = accountEntry.getChildren('Opportunity'); Assert.areEqual(3, oppEntries.size()); ``` For the first argument of `setChildren`, use the same name you specified in `Scribe` with `relationName` if you did; otherwise use the **object name** as-is (`'Opportunity'` — no pluralization, 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). Diverge and **an `ApexEloquentException` is thrown**. > > ``` > The specified child Object Name `Opportunities` is not set in Scribe. parent object name: Account > ``` > > So while production `Entry` also resolves relationship names, **in mocks you must call with the key declared on the `Scribe`**. It never quietly returns an empty list — a mismatched key fails the test right there. ### Define Child MockEntry Inline Inside the setChildren Argument If you extract the child `MockEntry` into a variable, the reader has to anticipate "is this variable used somewhere else later?". Unless there's a clear reason to reuse it via `getAliasId(...)` or similar, **inline the definition inside the `setChildren` argument** for better readability. ```apex // Good: inline (the structure is visually obvious) MockEntry accountEntry = MockEntry.of(Account.class).alias('acc').autoId(1) .set('Name', 'Acc Co.') .setChildren('Opportunity', new List{ MockEntry.of(Opportunity.class).autoId(1).set('Name', 'Opp A'), MockEntry.of(Opportunity.class).autoId(2).set('Name', 'Opp B') }); ``` ## Read Next - [Building Queries with Scribe](https://krileworks.com/apex-stem/docs/apex-eloquent-scribe-guide): the full picture of the query builder - [Data Access, DML, IEntry, and Mock](https://krileworks.com/apex-stem/docs/apex-eloquent-data-access): how to use `IEloquent` and `MockEloquent` - [ApexEloquent Guide](https://krileworks.com/apex-stem/docs/apex-eloquent-guide): back to the ApexEloquent Guide index ============================================================================== Source: https://krileworks.com/document/apex-eloquent-api-scribe.md Page: https://krileworks.com/apex-stem/docs/apex-eloquent-api-scribe ============================================================================== # API Reference: Scribe `Scribe` is ApexEloquent's query builder. It's an **immutable** class that assembles SOQL through type hints and method chains. Each method returns a new `Scribe` instance. For usage and typical scenarios, see [Building Queries with Scribe](https://krileworks.com/apex-stem/docs/apex-eloquent-scribe-guide). ## Static Factories | Method | Purpose | |---|---| | `Scribe.of(System.Type recordType)` | Starting point for normal queries. `Scribe.of(Account.class)` | | `Scribe.source(Schema.SObjectType sObjectType)` | Also an entry point, taking an `SObjectType`. `Scribe.source(Account.getSObjectType())` | | `Scribe.asParent(String parentRelationIdFieldName)` | Parent-relation Scribe. Pass to `parentField` / `parentCondition` / `groupByParent` | | `Scribe.asChild(System.Type childRecordType)` | Child-subquery Scribe. Pass to `withChildren` | | `Scribe.asGroup()` | Scribe for wrapping WHERE clauses in parentheses. Pass to `whereGroup` | | `Scribe.asHaving()` | HAVING-clause Scribe. Pass to `havingCondition` | | `Scribe.asThrough(System.Type junctionType, String relatedKey)` | Many-to-many via junction object Scribe. Pass to `through` | ## SELECT Family | Method | Purpose | |---|---| | `field(String fieldName)` | SELECT a single field | | `fields(List fieldNames)` | SELECT multiple fields | | `allFields()` | SELECT every field on the target SObject | | `parentField(Scribe parentScribe)` | Bring parent-object fields into the SELECT | | `withChildren(Scribe childScribe)` | Add a child subquery | | `through(Scribe throughScribe)` | Add a many-to-many relation via a junction object | | `relationName(String relationName)` | Disambiguate child-subquery / many-to-many by explicit relationship name | ```apex List oppFields = new List{ 'Id', 'Name', 'StageName' }; Scribe scribe = Scribe.of(Opportunity.class) .fields(oppFields) .parentField(Scribe.asParent('AccountId').field('Name')); ``` ```soql SELECT id, name, stagename, Account.name FROM Opportunity ``` Plain field names in the SELECT clause are normalised to lower case, and parent fields are **prefixed with the relationship name** (own fields first, then parent fields). Field names in the WHERE clause keep their original casing. ## WHERE Family ### Comparison, Inclusion, Pattern Matching | Method | SOQL Output | |---|---| | `whereEqual(String f, Object v)` | `f = v` (null → `f = NULL`) | | `whereNotEqual(String f, Object v)` | `f != v` | | `whereGreaterThan(String f, Object v)` | `f > v` (null not allowed) | | `whereGreaterThanOrEqual(String f, Object v)` | `f >= v` | | `whereLessThan(String f, Object v)` | `f < v` | | `whereLessThanOrEqual(String f, Object v)` | `f <= v` | | `whereLike(String f, String pattern)` | `f LIKE '...'` (`'` is auto-escaped) | | `whereNotLike(String f, String pattern)` | `f NOT LIKE '...'` | | `whereIn(String f, Object values)` | `f IN (...)`. Takes a `List` or `Set` as-is (no repacking). See below for empty collections | | `whereIn(String f, Scribe subQuery)` | Subquery form: `f IN (SELECT ...)` | | `whereNotIn(String f, Object values)` | `f NOT IN (...)` | | `whereNotIn(String f, Scribe subQuery)` | `f NOT IN (SELECT ...)` | | `whereIncludes(String f, List values)` | `INCLUDES` for multi-select picklists | | `whereExcludes(String f, List values)` | `EXCLUDES` for multi-select picklists | | `whereNull(String f)` | `f = NULL` | | `whereNotNull(String f)` | `f != NULL` | ### Empty Collections (whereIn and whereNotIn differ) Passing an **empty collection** behaves differently between the two. | Method | Given an empty collection | Result | |---|---|---| | `whereIn(f, empty)` | builds a condition that is **always false** | **zero rows** | | `whereNotIn(f, empty)` | **drops the condition entirely** | no filtering — falls to the **all rows** side | Both are reasonable as SOQL, but **writing `whereIn` while meaning "don't filter when empty" gives you zero rows**. To state the intent, use `ignoreWhen` below. ### Logical Joins and Grouping | Method | Purpose | |---|---| | `orCondition()` | Join the next where with OR (once set, every subsequent join must be OR) | | `whereGroup(Scribe groupScribe)` | Wrap a group of conditions in parentheses (built from `Scribe.asGroup()`) | | `parentCondition(Scribe parentScribe)` | Filter by a parent-object condition | ```apex // (Industry = 'Tech' AND Name = 'X') OR BillingCity = 'Tokyo' Scribe scribe = Scribe.of(Account.class) .field('Id') .whereGroup( Scribe.asGroup() .whereEqual('Industry', 'Tech') .whereEqual('Name', 'X') ) .orCondition() .whereEqual('BillingCity', 'Tokyo'); ``` ```soql SELECT id FROM Account WHERE (Industry = 'Tech' AND Name = 'X') OR BillingCity = 'Tokyo' ``` #### Mixing AND and OR bare fails at build time (v3.5.0+) SOQL does not allow AND and OR to mix **at one nesting level without parentheses**. Either direction now raises `ApexEloquentException`, and the message **points you at `whereGroup`**. ```apex // ❌ OR after ANDs .whereEqual('Industry', 'Tech').whereEqual('Name', 'X').orCondition().whereEqual('BillingCity', 'Tokyo') ``` > ⚠️ **Before v3.5.0, OR-after-AND slipped through.** `toSoql()` succeeded and the query then died at run time with the unhelpful `unexpected token: OR`. > > The check runs **at build time, not chain time**, so that a mix legitimately resolved by an `ignoreWhen()` retraction — or by a semantic skip such as an empty `whereNotIn` — still passes. ### Retracting a condition dynamically: ignoreWhen | Method | Purpose | |---|---| | `ignoreWhen(Boolean shouldIgnore)` | When `true`, **retracts the immediately preceding `where...()` condition** | "Skip the condition when the input is empty" becomes a single chain, with no `if` branches and no reassignment. ```apex Scribe scribe = Scribe.of(Opportunity.class) .field('Id') .whereIn('Id', ids).ignoreWhen(ids.isEmpty()) .whereLike('Name', keyword).ignoreWhen(String.isBlank(keyword)); ``` The SOQL that comes out depends on the values at run time. ```soql -- ids has values, keyword is blank SELECT id FROM Opportunity WHERE Id IN ('006000000000000AAA', '006000000000000AAB') -- both empty (no WHERE clause at all) SELECT id FROM Opportunity ``` This also avoids the trap from "Empty Collections" above, where an empty `whereIn` yields zero rows. **It must be chained directly onto a `where...()`.** Calling it first, after `orderBy()`, or twice in a row throws `ApexEloquentException` — the constraint keeps it unambiguous which condition an `ignoreWhen()` applies to. Placed right after `whereGroup(...)`, it retracts **the whole group**. #### null values can be retracted too (v3.5.0+) `whereGreaterThan` / `whereGreaterThanOrEqual` / `whereLessThan` / `whereLessThanOrEqual` / `whereLike` / `whereNotLike` / `whereIn` / `whereNotIn` / `whereIncludes` / `whereExcludes` — **10 methods (12 counting overloads)** — do not accept `null`. From v3.5.0 that **null error is deferred to build time (`toSoql()`)**. Chaining only records an invalid condition, so a following **`ignoreWhen(true)` retracts it**. ```apex // Through v3.4.x: whereGreaterThan threw on the spot — ignoreWhen was never reached // From v3.5.0: retracted, and it never appears in the WHERE clause .whereGreaterThan('CloseDate', closeAfter).ignoreWhen(closeAfter == null) ``` If it survives un-retracted to `toSoql()`, the exception names **which method and which field** originated it, and mentions `ignoreWhen` as the escape hatch. > ⚠️ `whereEqual` / `whereNotEqual` are not among the 12. `X = null` is valid SOQL, so null becomes the condition as written. #### Combining with orCondition Calling `orCondition()` while no condition exists yet is **a no-op**. So a chain does not break when the leading condition is retracted; whatever survives simply becomes the sole condition. ```apex // Both present → OR. One retracted → the other stands alone. Both gone → no WHERE. Scribe scribe = Scribe.of(Opportunity.class) .field('Id') .whereIn('StageName', stages).ignoreWhen(stages.isEmpty()) .orCondition() .whereIn('OwnerId', ownerIds).ignoreWhen(ownerIds.isEmpty()); ``` ```soql -- both present SELECT id FROM Opportunity WHERE StageName IN ('Prospecting') OR OwnerId IN ('005000000000000AAA') -- only stages (the OR disappears and it becomes a single condition) SELECT id FROM Opportunity WHERE StageName IN ('Prospecting') -- both empty SELECT id FROM Opportunity ``` When the retracted condition was the OR one, **OR-only mode is lifted along with it**. The OR marker never outlives the condition it was attached to, so a plain condition after it is not rejected. ## ORDER / LIMIT / OFFSET / forUpdate | Method | Purpose | |---|---| | `orderBy(String field)` | ASC sort | | `orderBy(String field, String order)` | `ASC` / `DESC` specification | | `orderBy(String field, String order, String nullsOperator)` | `NULLS FIRST` / `NULLS LAST` specification | | `take(Integer limitNumber)` | LIMIT clause | | `offset(Integer offsetNumber)` | OFFSET clause (max 2000; throws if exceeded) | | `forUpdate()` | FOR UPDATE clause | **Constraints**: `forUpdate` cannot be combined with `orderBy` or `offset`. An exception is thrown at build time. ## Aggregate Functions | Method | SOQL | |---|---| | `count(String field, String alias)` | `COUNT(field) alias` | | `countDistinct(String field, String alias)` | `COUNT_DISTINCT(field) alias` | | `sum(String field, String alias)` | `SUM(field) alias` | | `average(String field, String alias)` | `AVG(field) alias` | | `max(String field, String alias)` | `MAX(field) alias` | | `min(String field, String alias)` | `MIN(field) alias` | **The `alias` argument is required**. Salesforce's standard `AggregateResult` defaults to field names like `expr0` / `expr1` / ... (in declaration order) when the alias is omitted — a classic beginner trap. ApexEloquent enforces aliases at the method signature level to sidestep this. Retrieve the result via `aggregateEntry.get('alias')` — the name you assigned. **Other notes**: - Reusing the same alias across multiple aggregate functions throws. - Combining child subqueries (`withChildren`) with aggregate functions is forbidden. ## GROUP BY / HAVING | Method | Purpose | |---|---| | `groupByField(String fieldName)` | GROUP BY a single field | | `groupByFields(List fieldNames)` | GROUP BY multiple fields | | `groupByParent(Scribe parentScribe)` | GROUP BY a parent-object field (pass `Scribe.asParent(...).groupByField(...)`) | | `havingCondition(Scribe havingScribe)` | HAVING clause (pass `Scribe.asHaving().whereGreaterThan(alias, value)`) | ```apex Scribe scribe = Scribe.of(OpportunityLineItem.class) .field('Product2Id') .sum('TotalPrice', 'totalPrice') .parentField( Scribe.asParent('OpportunityId').field('Id').max('Amount', 'maxAmount') ) .groupByField('Product2Id') .groupByParent(Scribe.asParent('OpportunityId').groupByField('Id')) .havingCondition( Scribe.asHaving().whereGreaterThan('totalPrice', 1000) ); ``` ```soql SELECT product2id, SUM(TotalPrice) totalPrice, Opportunity.id, MAX(Opportunity.Amount) maxAmount FROM OpportunityLineItem GROUP BY Product2Id, Opportunity.Id HAVING SUM(TotalPrice) > 1000 ``` In HAVING, **the alias expands back into the aggregate expression**. Writing `whereGreaterThan('totalPrice', 1000)` yields `SUM(TotalPrice) > 1000`, so you never write the aggregate twice. ## Inspection / Output | Method | Return | Purpose | |---|---|---| | `toSoql()` | `String` | Return the assembled SOQL string | | `isAggregate()` | `Boolean` | Whether this is an aggregate query (Eloquent uses this internally to route `get`) | | `buildFieldStructure()` | `FieldStructure` | Build the SELECT-clause field structure (used internally by MockEntry's SELECT-omission detection) | | `buildAggregateFieldStructure()` | `FieldStructure` | Build the field structure for aggregate queries | | `getSelectedFields(Map)` | `List` | List of fields targeted by SELECT | `toSoql()` is handy for debugging and learning, but **its real job is a batch `start()`**. ### Building a batch QueryLocator with toSoql() `Database.getQueryLocator()` demands SOQL as a **string**, so this is the one place a `Scribe` cannot be handed over directly. Build it with `Scribe`, then turn it into a string at the very end. ```apex public Database.QueryLocator start(Database.BatchableContext bc) { return Database.getQueryLocator(scope().toSoql()); } ``` ### Extract the Scribe and SELECT-omission detection works in tests too Batches have a structural hole. The records handed to `execute(bc, scope)` come **straight from the platform** — they never pass through `IEloquent`. So the contract of "what this query selected" never reaches the Usecase. - **Production**: `scope` is a real query result, so touching an unselected field throws from the platform - **Tests**: `scope` is a `MockEntry` you built yourself, so **nothing is checked** `fetchedBy(scribe)` closes that gap. Pull the query construction out into a `@TestVisible` method and your test can hand over **exactly the same `Scribe`** production uses. ```apex public with sharing class RematchCompanyCardsHandler implements Database.Batchable { public Database.QueryLocator start(Database.BatchableContext bc) { return Database.getQueryLocator(scope().toSoql()); } // One definition, shared by production and tests @TestVisible private static Scribe scope() { List cardFields = new List{ 'Id', 'CompanyName__c', 'MatchStatus__c' }; return Scribe.of(BusinessCard__c.class) .fields(cardFields) .whereEqual('MatchStatus__c', 'Unprocessed'); } } ``` ```soql SELECT id, companyname__c, matchstatus__c FROM BusinessCard__c WHERE MatchStatus__c = 'Unprocessed' ``` ```apex // In the test: bake production's SELECT contract onto the mock MockEntry card = MockEntry.of(BusinessCard__c.class) .autoId(1) .set('CompanyName__c', 'Acme') .fetchedBy(RematchCompanyCardsHandler.scope()); ``` Now the moment the Usecase reads a field that `scope()` does not select, **the unit test fails**. Adding a field on the Usecase side while forgetting to add it to the batch's SELECT gets caught before it ships. > See [API Reference: MockEntry](https://krileworks.com/apex-stem/docs/apex-eloquent-api-entry) for `fetchedBy`. ## Read Next - [Building Queries with Scribe](https://krileworks.com/apex-stem/docs/apex-eloquent-scribe-guide): the usage guide - [Parent Fields, Child Subqueries, and Many-to-Many](https://krileworks.com/apex-stem/docs/apex-eloquent-relations): typical relation-operation examples - [API Reference: IEloquent / Eloquent / MockEloquent](https://krileworks.com/apex-stem/docs/apex-eloquent-api-eloquent): execute the assembled Scribe - [ApexEloquent Guide](https://krileworks.com/apex-stem/docs/apex-eloquent-guide): back to the guide index ============================================================================== Source: https://krileworks.com/document/apex-eloquent-api-eloquent.md Page: https://krileworks.com/apex-stem/docs/apex-eloquent-api-eloquent ============================================================================== # API Reference: IEloquent / Eloquent / MockEloquent `IEloquent` is ApexEloquent's data-access contract (the interface). Production uses `Eloquent`; tests use `MockEloquent`, both DI'd via the Layered Constructor Pattern. For usage and typical scenarios, see [Data Access, DML, IEntry, and Mock](https://krileworks.com/apex-stem/docs/apex-eloquent-data-access). > This page covers both the v3 and v2 lines. The only difference between them is the execution mode, so that part is called out separately under "Execution Mode (v3 line)". ## The Three Relationships ``` IEloquent (interface) ← the contract Usecase depends on ↑ ├── Eloquent ← production. Hits standard SOQL / DML directly └── MockEloquent ← tests. No DB; provides Spy + failOn* ``` The Usecase holds an `IEloquent`-typed field, into which it receives `new Eloquent()` in production and `new MockEloquent(...)` in tests via the Layered Constructor. ## IEloquent (Interface) Every contract method is implemented by both `Eloquent` and `MockEloquent`. ### Labeling | Signature | Return | Purpose | |---|---|---| | `label(String labelName)` | `IEloquent` | Tags the next operation with a label so the chain stays composable. A single `IEloquent` can be **multiplexed by label** — fold "last-month / this-year / Account update" purposes onto one instance instead of DI-ing a separate `IEloquent` per purpose | ```apex // Distinguish two SOQLs and one DML on the same IEloquent IEntry job = this.eloquent.label('jobLoad').firstOrFail(jobScribe); List details = this.eloquent.label('detailLoad').get(detailScribe); this.eloquent.label('finalDml').doUpdate(toUpdate); ``` Passing `null` or blank to `label(...)` throws. `'default'` is the name **assigned internally to label-less operations**. `label('default')` itself goes through, but it points at the same slot as an unlabelled call, so the intent doesn't read. Use a different name. (`whenLabel('default')`, on the other hand, is explicitly rejected as redundant.) **Opt-in strict mode**: the first `.label(...)` call on an `IEloquent` instance switches it into **strict mode** (sticky for the rest of the instance's lifetime): - Every subsequent operation must be preceded by `.label(...)` (label-less operations throw) - Each label can be consumed at most once per instance (reusing throws) - Instances that never call `.label()` stay in lenient mode (full backward compatibility) This closes the "I added a new DML and forgot to label it" footgun at runtime: the offending call throws instead of silently landing in the default bucket. ### Execution Mode (v3 line only) | Signature | Returns | Purpose | |---|---|---| | `userMode()` | `IEloquent` | Run subsequent SOQL / DML as `AccessLevel.USER_MODE` (the v3 default) | | `systemMode()` | `IEloquent` | Run subsequent SOQL / DML as `AccessLevel.SYSTEM_MODE`, ignoring FLS and object permissions | In the v3 line `Eloquent` became `inherited sharing` and SOQL / DML default to **user mode** (honouring the running user's field and object permissions). Only system processes — aggregation, stamping, data migration, anything that must complete no matter who triggered it — opt out explicitly with `systemMode()`. ```apex // A system process: mark the calling class without sharing, then be explicit here this.eloquent.systemMode().label(LBL_UPDATE).doUpdate(entries); ``` - **Sticky**: calling it once applies to every subsequent operation on that instance (unlike `label()`, which resets per operation) - **A no-op on `MockEloquent`**: it simply returns itself, so unit tests can ignore the mode entirely - ⚠️ `systemMode()` only lifts **FLS and object permissions**. **Sharing (record visibility) is a separate axis** — to lift that, the calling class must be declared `without sharing` ### Query Methods | Signature | Return | Purpose | |---|---|---| | `get(Scribe scribe)` | `List` | Execute the query. Empty list on 0 hits | | `first(Scribe scribe)` | `IEntry` | The first record. `null` on 0 hits | | `firstOrFail(Scribe scribe)` | `IEntry` | The first record. Throws `ApexEloquentException` on 0 hits | | `firstOrFail(Scribe scribe, Exception orFail)` | `IEntry` | The first record. On 0 hits, throws **the exception you passed**, as-is | | `getAsSObject(Scribe scribe)` | `List` | `SObject` list (last resort) | | `firstAsSObject(Scribe scribe)` | `SObject` | SObject version of the first record | | `firstOrFailAsSObject(Scribe scribe)` | `SObject` | SObject version of the first record; throws on 0 hits | | `rawSoql(String soql)` | `List` | Bypass `Scribe` and run raw SOQL (last resort; SELECT-omission detection is disabled) | **When `firstOrFail(scribe, orFail)` earns its keep**: when zero rows is a business error you want to surface to the screen. The exception you hand it is thrown verbatim, so you skip `first` + null check + your own throw. What matters is **where it gets caught**. Hand it a business exception and it lands in your `catch (UsecaseException)`. ```apex IEntry job; try { job = this.eloquent.label(LBL_JOB).firstOrFail( jobScribe, new UsecaseException('The requested job was not found.') ); } catch (UsecaseException ex) { this.t.skip('Business error: ' + ex.getMessage()); } ``` The no-argument `firstOrFail(scribe)` throws `ApexEloquentException`, which never reaches that `catch`. ### DML Methods | Signature | Return | |---|---| | `doInsert(SObject record)` | `SObject` | | `doInsert(List records)` | `List` | | `doUpdate(SObject record)` | `SObject` | | `doUpdate(IEntry entry)` | `IEntry` | | `doUpdate(List records)` | `List` | | `doUpdate(List entries)` | `List` | | `doUpdate(SObject record, Boolean allOrNone)` | `Database.SaveResult` | | `doUpdate(IEntry entry, Boolean allOrNone)` | `Database.SaveResult` | | `doUpdate(List records, Boolean allOrNone)` | `List` | | `doUpdate(List entries, Boolean allOrNone)` | `List` | | `doUpsert(SObject record)` | `SObject` | | `doUpsert(IEntry entry)` | `IEntry` | | `doUpsert(List records)` | `List` | | `doUpsert(List entries)` | `List` | | `doUpsertByExternalId(SObject record, Schema.SObjectField externalIdField, Boolean allOrNone)` | `Database.UpsertResult` | | `doUpsertByExternalId(IEntry entry, Schema.SObjectField externalIdField, Boolean allOrNone)` | `Database.UpsertResult` | | `doUpsertByExternalId(List records, Schema.SObjectField externalIdField, Boolean allOrNone)` | `List` | | `doUpsertByExternalId(List entries, Schema.SObjectField externalIdField, Boolean allOrNone)` | `List` | | `doDelete(SObject record)` | `void` | | `doDelete(IEntry entry)` | `void` | | `doDelete(List records)` | `void` | | `doDelete(List entries)` | `void` | Default to bulk; use the single-record versions only when exactly one record is guaranteed. **`doUpdate` with `allOrNone`**: the return type is `Database.SaveResult` / `List`, so partial-success per-record outcomes can be inspected through the `IEloquent` abstraction. Useful in ETL / incremental sync / batch migration paths that log failed records and continue. **`doUpsertByExternalId`**: brings the standard `Database.upsert(records, externalIdField, allOrNone)` API under the `IEloquent` abstraction. Essential for "upsert by external Id key" in ETL / incremental sync / batch migration, handled through a shared production / Mock contract. ## Eloquent (Production) The production class implementing `IEloquent`. It hits standard SOQL / DML directly. **No additional public methods** (interface-only). ```apex IEloquent eloquent = new Eloquent(); List opps = eloquent.get(scribe); eloquent.doUpdate(opps); ``` ## MockEloquent (Mock Extension) On top of the `IEloquent` contract, it adds test-side **Spy properties** and the **failOn series**. ### Constructors | Signature | Behavior | |---|---| | `new MockEloquent()` | Empty. If you use labels, feed data with `attach(...)` | | `new MockEloquent(IEntry entry)` | Preloads one record as the label-less (`'default'`) result | | `new MockEloquent(List entries)` | Same, list version | Data passed to the constructor only feeds **label-less (`'default'`) operations**. If production code uses `label(...)`, supply it with `attach(label, ...)` instead. `MockEloquent` **does not evaluate `Scribe`'s WHERE conditions** — it hands back the list you gave it, as-is. To tell condition-varied queries apart, **label each query and feed them separately with `attach(label, ...)`** (below). ### An unattached label throws Call `get` / `first` / `firstOrFail` under `label('X')` without a matching `attach('X', ...)` and **the test throws**. The error lists the labels that were attached, so a typo shows up immediately. This guards a specific false positive: mistype a label, get zero rows, fall into the "nothing to do, skip" branch, and **the test goes green while verifying nothing**. When you genuinely want to test the zero-row path, **attach an empty list** to say so. ```apex MockEloquent mock = (new MockEloquent()) .attach(MyUsecase.LBL_FETCH, new List()); ``` > Upgrading from an older version can turn some tests red here. Those are the tests that were green while verifying nothing. Rather than mechanically adding empty attaches, check what data should have been injected in the first place. ### Spy Properties and Per-Label Accessors | Property / Method | Type | Content | |---|---|---| | `upsertedRecords` | `List` | Records passed to `doInsert` / `doUpdate` / `doUpsert` / `doUpsertByExternalId` for the **`'default'` bucket** (※ `@deprecated`: new code should prefer `upsertedRecordsAt('default')`) | | `deletedCount` | `Integer` | Total `doDelete` invocations for the **`'default'` bucket** (※ `@deprecated`: new code should prefer `deletedCountAt('default')`) | | `upsertedRecordsAt(String label)` | `List` | Accumulated DML records for the given label | | `deletedCountAt(String label)` | `Integer` | Delete invocation count for the given label | | `attach(String label, IEntry entry)` | `MockEloquent` | Preload a single record as the given label's query data (chainable; re-attaching the same label overwrites) | | `attach(String label, List entries)` | `MockEloquent` | Same, list version | | `failSave(Id recordId, String errorMessage)` | `MockEloquent` | Make **only the named record fail to save** (chainable) | ```apex // Label-less, traditional usage (backward compatible) MockEloquent updateEloquent = new MockEloquent(); (new MyUsecase(input, fetchEloquent, updateEloquent)).invoke(); Assert.areEqual(1, updateEloquent.upsertedRecords.size()); Opportunity updated = (Opportunity) updateEloquent.upsertedRecords[0]; Assert.areEqual('Technology', updated.Industry__c); ``` ```apex // Label-multiplexed — one MockEloquent serves query preloading and DML verification, sorted by label MockEloquent mock = (new MockEloquent()) .attach('jobLoad', jobEntry); (new FinalizeJobUsecase(jobId, mock)).invoke(); Assert.areEqual(1, mock.upsertedRecordsAt('finalDml').size()); ``` **Backward compatibility note**: the existing `upsertedRecords` / `deletedCount` public fields continue to work and internally reflect the `'default'` bucket. They are JSDoc-marked `@deprecated`; migrate to `*At('default')` over time. The legacy constructor `new MockEloquent(List)` also still works — the data it accepts feeds the `'default'` source. ### failOn Series (Exception Simulation) Each method has two overloads: no-argument and `Exception`-accepting. Without an `Exception`, a default exception is thrown. | Method | Corresponding Contract | |---|---| | `failOnGet()` / `failOnGet(Exception e)` | `get(scribe)` | | `failOnFirst()` / `failOnFirst(Exception e)` | `first(scribe)` | | `failOnFirstOrFail()` / `failOnFirstOrFail(Exception e)` | `firstOrFail(scribe)` | | `failOnGetAsSObject()` / `failOnGetAsSObject(Exception e)` | `getAsSObject(scribe)` | | `failOnFirstAsSObject()` / `failOnFirstAsSObject(Exception e)` | `firstAsSObject(scribe)` | | `failOnFirstOrFailAsSObject()` / `failOnFirstOrFailAsSObject(Exception e)` | `firstOrFailAsSObject(scribe)` | | `failOnRawSoql()` / `failOnRawSoql(Exception e)` | `rawSoql(soql)` | | `failOnDoInsert()` / `failOnDoInsert(Exception e)` | `doInsert(*)` | | `failOnDoUpdate()` / `failOnDoUpdate(Exception e)` | `doUpdate(*)` | | `failOnDoUpsert()` / `failOnDoUpsert(Exception e)` | `doUpsert(*)` | | `failOnDoUpsertByExternalId()` / `failOnDoUpsertByExternalId(Exception e)` | `doUpsertByExternalId(*)` | | `failOnDoDelete()` / `failOnDoDelete(Exception e)` | `doDelete(*)` | ### Scoping Failures to a Label with whenLabel() | Signature | Purpose | |---|---| | `whenLabel(String label)` | Scopes the most recent `failOn*()` to **fire only for a specific label** (chainable) | ```apex // Only the DML labeled 'finalDml' fails; the others run normally MockEloquent mock = (new MockEloquent()) .failOnDoUpdate(new DmlException('Simulated finalDml failure')) .whenLabel('finalDml'); ``` Reads as "fail on get when label is X" — useful in label-multiplexed Usecases where you want exactly one side effect to fail. ```apex MockEloquent mock = (new MockEloquent()) .failOnDoUpdate(new DmlException('Simulated DML failure')) .whenLabel(MyUsecase.LBL_UPDATE); try { (new MyUsecase(input, mock)).invoke(); Assert.fail('Expected exception'); } catch (DmlException e) { Assert.isTrue(TraceFlow.isLastAbort()); } ``` ### Expressing "Always Fails" with repeat() | Signature | Purpose | |---|---| | `repeat()` | Call after a `failOn*` to **repeat the failure forever afterward** (count cannot be specified) | `failOn*()` alone fails just the next call. `failOn*` entries **queue up per method**, so putting two in a row fails the first and second call and lets the third succeed (the retry-then-succeed path). Add `.repeat()` and the entry you just planted **fails on every subsequent invocation**. That is how you check the *abort* side of "retry up to 3 times, then give up" — without counting calls. Queues are **kept per label**. A configuration without `whenLabel` targets label-less calls (`'default'`). ⚠️ Every test-setup builder (`attach` / `failOn*` / `whenLabel` / `failSave` / `repeat`) **returns a new instance rather than mutating `this`**. Drop the return value and the configuration is lost, so keep the chain or reassign. 💡 The once-per-label rule counts **successful** operations. A failed operation releases its label, so production code that catches and retries under the same label tests just fine. ⚠️ In a test that uses labels, forgetting `whenLabel` leaves the configuration aimed at `'default'`, where it can never fire. That is detected and reported as an exception, naming the label you should have used. ### The failure patterns you can express Combining `failOn*` / `whenLabel` / `repeat` gives five shapes. | Written as | What happens | |---|---| | `failOnGet(exA).failOnGet(exB)` | Fails with `exA`, then `exB`, and **succeeds on the third call** | | `failOnGet(ex).repeat()` | **Fails every time** | | `failOnGet(ex).whenLabel('opp')` | `'opp'` fails once; **a retry succeeds** | | `failOnGet(exA).whenLabel('opp')`
`.failOnGet(exB).whenLabel('opp')` | `'opp'` fails twice in a row, **succeeds on the third** | | `failOnGet(ex).whenLabel('opp').repeat()` | `'opp'` **fails every time** | ```apex // "fail once, succeed on retry" MockEloquent mock = (new MockEloquent()) .attach('opp', new List{ oppEntry }) .failOnGet(new QueryException('boom')) .whenLabel('opp'); // 1st call: throws // 2nd call: the failure released the label, so the attached data comes back ``` Because queues are per label, failures aimed at different labels on the same method never interfere. ```apex MockEloquent mock = (new MockEloquent()) .failOnGet(new QueryException('fetch failed')).whenLabel('fetch') .failOnDoUpdate(new DmlException('save failed')).whenLabel('update'); ``` Add `failSave` — where the call succeeds but a specific record is not saved — and that is the whole of what `MockEloquent` can express. ### failSave(): partial save failure `failSave` differs in kind from `failOn*`. Rather than throwing, **the call succeeds while the named record is left unsaved** — the partial failure of an `allOrNone` DML. ```apex Id badId = MockEntry.of(Account.class).autoId(2).getId(); MockEloquent mock = (new MockEloquent()) .failSave(badId, 'Rejected by a validation rule'); ``` - `allOrNone = false` — that record's `SaveResult` carries `success = false` and your message, and it is **not recorded by the spy** (it was never saved) - `allOrNone = true` — as with real all-or-nothing DML, **the whole operation throws** before anything is recorded The target is named by **record Id**, not index, so pair it with `MockEntry.autoId()`. It survives records being added later. ## Read Next - [Data Access, DML, IEntry, and Mock](https://krileworks.com/apex-stem/docs/apex-eloquent-data-access): the usage guide - [API Reference: Scribe](https://krileworks.com/apex-stem/docs/apex-eloquent-api-scribe): the query-assembly side - [API Reference: IEntry / Entry / MockEntry](https://krileworks.com/apex-stem/docs/apex-eloquent-api-entry): the `IEntry` side that's returned - [Layered Constructor Pattern](https://krileworks.com/apex-stem/docs/layered-constructor-pattern): the design for DI'ing `IEloquent` into a Usecase - [ApexEloquent Guide](https://krileworks.com/apex-stem/docs/apex-eloquent-guide): back to the guide index ============================================================================== Source: https://krileworks.com/document/apex-eloquent-api-entry.md Page: https://krileworks.com/apex-stem/docs/apex-eloquent-api-entry ============================================================================== # API Reference: IEntry / Entry / MockEntry `IEntry` is ApexEloquent's record wrapper (the interface). The return value of `Eloquent.get(scribe)` and similar is `List`. Production uses `Entry`; tests use `MockEntry`. For usage and typical scenarios, see [Data Access, DML, IEntry, and Mock](https://krileworks.com/apex-stem/docs/apex-eloquent-data-access) and [Parent Fields, Child Subqueries, and Many-to-Many](https://krileworks.com/apex-stem/docs/apex-eloquent-relations). ## The Three Relationships ``` 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 | Signature | Return | Purpose | |---|---|---| | `get(String fieldName)` | `Object` | Read any field's value (cast required) | | `put(String fieldName, Object value)` | `void` | Write a value | | `getId()` | `Id` | Dedicated Id getter (no cast) | | `getName()` | `String` | Dedicated Name getter (no cast) | | `getRecord()` | `SObject` | Extract the wrapped `SObject` (last resort) | | `setRecord(SObject record)` | `IEntry` | Internal use. Swap the SObject | | `setFieldStructure(FieldStructure fs)` | `IEntry` | Internal use. Set the SELECT-omission detection schema | | `setDescribeResult(Schema.DescribeSObjectResult)` | `void` | Internal 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) | Signature | Return | Purpose | |---|---|---| | `getParent(String parentIdFieldName)` | `IEntry` | Get the parent record (e.g. `'AccountId'`) | | `getChildren(String name)` | `List` | Child record list | | `getThrough(String junctionName, String relatedKey)` | `List` | 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 opps = accountEntry.getChildren('Opportunity'); // object name List opps = accountEntry.getChildren('Opportunities'); // relationship name ``` If you declared `relationName(...)` on the `Scribe` side, that name works too. #### Deprecated: `*ByRelationName` | Signature | Use 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 | Signature | Purpose | |---|---| | `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 fieldToValue)` | Same as above, with initial values | ### Field Operations | Signature | Return | Purpose | |---|---|---| | `set(String fieldName, Object value)` | `MockEntry` | Set a field value (non-writable fields are allowed) | | `template(Map fieldToValue)` | `MockEntry` | Set multiple fields at once via a Map | ```apex MockEntry accEntry = MockEntry.of(Account.class) .template(new Map{ '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 | Signature | Return | Purpose | |---|---|---| | `fetchedBy(Scribe scribe)` | `MockEntry` | Bake 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](https://krileworks.com/apex-stem/docs/apex-eloquent-api-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) | Signature | Return | Purpose | |---|---|---| | `setParent(String parentIdFieldName, MockEntry parent)` | `MockEntry` | Hang a parent record under the entry | | `setChildren(String name, List children)` | `MockEntry` | Hang 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 | Signature | Return | Purpose | |---|---|---| | `autoId(Integer suffix)` | `MockEntry` | Auto-generate an 18-character Id (numeric suffix) | | `autoId(String suffix)` | `MockEntry` | Auto-generate an 18-character Id (string suffix; supports placeholder expansion like `{#}`) | ### Bulk Generation (times) | Signature | Return | Purpose | |---|---|---| | `times(Integer count)` | `List` | Expand `count` records from the template. Replaces placeholders `{#}` `{A}` `{a}` with sequential values | | `times(Integer count, Integer startAt)` | `List` | Specify the start of the sequence | | `times(Integer count, Integer startAt, Integer interval)` | `List` | Specify the start and increment | ```apex List 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) | Signature | Return | Purpose | |---|---|---| | `alias(String name)` | `MockEntry` | Attach a name to this entry | | `getByAlias(String name)` | `MockEntry` | Retrieve a MockEntry by alias via recursive search | | `getAliasId(String name)` | `Id` | Retrieve 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. | Signature | Return | What It Disables | |---|---|---| | `withoutFieldValidation()` | `MockEntry` | **Scribe FieldStructure** check (permits `get(...)` access to fields not SELECTed in `Scribe`) | | `withoutSObjectFieldValidation()` | `MockEntry` | **SObject 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. ## Read Next - [Data Access, DML, IEntry, and Mock](https://krileworks.com/apex-stem/docs/apex-eloquent-data-access): the usage guide - [Parent Fields, Child Subqueries, and Many-to-Many](https://krileworks.com/apex-stem/docs/apex-eloquent-relations): typical relation-operation examples - [API Reference: IEloquent / Eloquent / MockEloquent](https://krileworks.com/apex-stem/docs/apex-eloquent-api-eloquent): the side that returns `IEntry` - [API Reference: Scribe](https://krileworks.com/apex-stem/docs/apex-eloquent-api-scribe): the side that builds the source data for SELECT-omission detection - [ApexEloquent Guide](https://krileworks.com/apex-stem/docs/apex-eloquent-guide): back to the guide index ============================================================================== Source: https://krileworks.com/document/query-delegation-pattern.md Page: https://krileworks.com/apex-stem/docs/query-delegation-pattern ============================================================================== # Query Delegation Pattern This document explains ApexEloquent's design philosophy of separating query "construction" from query "execution". Starting from the long-term operational challenges of the Selector Pattern, widely adopted in Salesforce development, the article lays out why the Query Delegation Pattern resolves them, and how ApexEloquent embodies it. ## Introduction In Salesforce development, the **Selector Pattern** has long been a popular way to organize data access. Concentrating queries in one place keeps raw SOQL out of business logic and lets the consumer side fetch data through method calls — a clear, simple win. But run a Selector Pattern long enough on a mid-to-large project, and the request "I want a slightly different query" comes up over and over. Methods and arguments accumulate as patches. The Selector class swells, and the original simplicity erodes bit by bit. On top of that, since tests themselves require a database, you're stuck writing only integration tests. ### What Happens to a Selector Pattern Over Time - Each new use case wants a slightly different query, and methods / arguments keep getting tacked on — the Selector bloats - Splitting Selectors per use case generates a swarm of look-alike Selector classes and interfaces - Flag arguments and conditional branches pile up; method readability and maintainability drop - A query change made for one use case quietly affects another - Tests skew toward "implement mock classes for Selectors" and DB-backed integration tests ### Why Separation Started to Feel Necessary While dealing with this kind of erosion, one question arrived: > "Are query construction and query execution really the same responsibility?" Deciding which object to query, and with what conditions, is **part of the business logic** — deeply rooted in the domain's rules and use cases. Actually issuing the assembled query against the DB and returning the result, by contrast, is pure **I/O**. Rather than have these two coexist inside a Selector, the idea was to draw a clear line between them. The thinking that followed landed on what we now call the Query Delegation Pattern. ## Problems with Traditional Patterns ### What Goes Wrong When Construction and I/O Mix A typical Selector holds both responsibilities — "what data, and how to retrieve it" (query construction) and "actually hit the DB and return the result" (execution). Construction lives close to business logic; execution belongs to the infrastructure layer. Sorted that way, the two are **fundamentally distinct responsibilities**. When they mix inside a Selector, the situation degrades over time: - Query assembly is locked inside the Selector, and from the domain side it becomes hard to see "what are we going after, and how" - Modifying a query risks rippling into every feature that uses that Selector - Tests need mock implementations against the Selector interface, and preparing per-test variations becomes overhead - When one Selector concentrates the logic, the operational pattern devolves into "keep adding methods", dragging down continuous feature development - Splitting Selectors per use case produces rows of look-alike interfaces and implementation classes, increasing the psychological cost of writing tests Ideally, "what data do I need" should be expressed explicitly on the use-case (domain) side, and the Selector should be responsible only for "execution". That's the starting point of the Query Delegation Pattern. ### The Cost of Reusability: Lost Context One attraction of the Selector Pattern is **reusability**. A single method that "filters records under a specific condition" can be shared across multiple use cases. But this reusability is often traded for **blurred business intent**. A Selector method designed for general reuse tends to lose the information "why is this data needed?" and "in what context is it used?". > 💭 The more general-purpose a Selector method becomes, the further it drifts from concrete business intent. This matters for testing too. When a method's business intent is vague, writing test cases gets vague along with it — "what exactly should I verify?" — and as a result, tests end up thin. ## What Is the Query Delegation Pattern? ### The Basic Concept The Query Delegation Pattern is the approach of clearly separating the responsibilities of "query construction" and "query execution". | Responsibility | Owner | Nature | |---|---|---| | **Query Construction** | The domain side (Usecase) | Assembles conditions based on business rules | | **Query Execution** | ApexEloquent's built-in `IEloquent` | Receives the assembled query and handles I/O with the DB | Where the traditional Selector held both responsibilities, the Query Delegation Pattern separates them: 1. **The domain side** (Usecase) builds the blueprint of the query 2. **The execution side** (`IEloquent`) receives the blueprint and runs it > 💡 The execution side stops caring about "what to fetch" and cares only about "how to fetch". ### Role Division Inside ApexEloquent ApexEloquent realizes this separation at the framework level with three core pieces. | Role | Class | Description | |---|---|---| | **Query Builder** | `Scribe` | An immutable builder that assembles the query blueprint via type hints and method chains | | **Data Access** | `IEloquent` (`Eloquent` / `MockEloquent`) | The side that takes a blueprint and issues SOQL. Production uses `Eloquent`, tests use `MockEloquent`, swapped via DI | | **Record Wrapper** | `IEntry` (`Entry` / `MockEntry`) | A wrapper around fetched results. Handles SObject and AggregateResult through one interface | ## Query Reusability ### How to Handle Reusable Queries The default stance of the Query Delegation Pattern is to assemble queries individually per use case — that keeps business intent in the code. But the practical reality is that "I want this query shared across multiple use cases" does come up. ApexEloquent answers this with **two options**. Consider team size, domain complexity, and how much commonality the queries have, and consciously pick which way the project leans. ### Option 1: Keep It Inside the Usecase Build the `Scribe` right next to the business logic and preserve context completely. The [Handler-Usecase Architecture](https://krileworks.com/apex-stem/docs/handler-usecase-architecture) of Apex Stem takes this as the default. Benefits: - The context "why this data is needed" stays in the code - Unexpected impact on other use cases is unlikely - Reading the use case alone gives you the full picture of what data it needs ### Option 2: A "Query Vault" — a Selector Derivative Extract the `Scribe`s you want to share into a utility-like class and have each use case explicitly opt in. This is a Selector-Pattern-derived style that offers "query parts". Benefits: - Heavily duplicated queries consolidate in one place - Since the shared query is passed as a `Scribe` object, the use case can append further conditions with chained methods - A gradual migration from the Selector Pattern flows naturally Caller-side feel: ```apex // 1. Pull the base "filter by Id" query from the vault Scribe oppScribe = OpportunityVault.getById(oppId); // 2. Append a use-case-specific condition oppScribe = OpportunityVault.addNameCondition(oppScribe, '%TestName%'); // 3. Add quotes as a child subquery List quoteFields = new List{ 'Id', 'Name', 'GrandTotal' }; oppScribe = OpportunityVault.addQuotes(oppScribe, quoteFields); // 4. Delegate execution to IEloquent (Query Delegation) List entries = this.eloquent.get(oppScribe); ``` We use the word **Vault** here for the Opportunity query vault. Each vault method just returns a `Scribe`; SOQL issuance is concentrated in the final `IEloquent.get(scribe)` call. That's the mechanism that makes the Query Delegation Pattern and the "vault" style coexist. With a traditional Selector — "the method runs SOQL inside and returns the result" — there's no room for the use case to append conditions, so the only escape is to keep adding methods or argument variations. ### Why Both Options Work In either case, what makes both options viable is that `Scribe` — through `.field()` / `.whereEqual()` and friends — has the property of **assembling queries as parts via method chains**. Building from scratch inside a use case, or grabbing a mid-state `Scribe` from a vault and tacking on more `.whereEqual(...)`, feels exactly the same. > 🎯 It's not "one option is correct". Pick what fits your team's situation; you can also migrate from one to the other later. ## Comparison with Traditional Patterns ### What Changes | Aspect | Traditional Selector | Query Delegation Pattern | |---|---|---| | **Responsibility placement** | Query assembly and DB execution live together | The domain assembles the query; `IEloquent` only executes | | **Long-term operation** | Patches accumulate; bloats; loses simplicity | Structure stays stable; easy to add features incrementally | | **Test style** | DB-backed integration tests dominate | Swap in `MockEloquent` and write DB-less unit tests | | **Visibility of intent** | Generalized methods lose context | Assembly lives right next to the use case — why this data matters stays | ## Implementation in ApexEloquent ApexEloquent embodies the Query Delegation Pattern at the framework level. The key implementation features: ### Dynamic Query Construction The domain side builds with `Scribe` and appends conditions to fit the context. `Scribe` is immutable, so deriving multiple queries from a shared `Scribe` ("last month's" / "this year's") doesn't have them step on each other. ### Query Execution Comes Built-In The execution side is handled commonly by ApexEloquent's built-in `IEloquent`. You don't have to mass-produce Selectors yourself — handing over a built `Scribe` issues SOQL. ### Mock Swap for DB-less Unit Tests Because construction and execution are split, swapping `IEloquent` for `MockEloquent` is enough to write DB-less unit tests. `MockEloquent` also exposes Spy properties like `upsertedRecords` / `deletedCount`, so you can assert what was DML'd. ### Mocking Non-Writable Fields Formula fields, rollups, parent relationships, auto-number — fields you can't normally write to — are freely assignable on the `MockEntry` side. Values that production can only obtain through computation can be handled in tests as "verify the logic on the premise that this value is returned". ### Where to Start with the Implementation The entry path is to first read [Building Queries with Scribe](https://krileworks.com/apex-stem/docs/apex-eloquent-scribe-guide), then [Data Access, DML, IEntry, and Mock](https://krileworks.com/apex-stem/docs/apex-eloquent-data-access) — and how Query Delegation lands in real code becomes immediately visible. ## Summary ### What the Query Delegation Pattern Resolves The Query Delegation Pattern is the approach of root-cutting the responsibility blur — "query construction" and "DB execution" — that Selectors tend to carry. Main effects: - Long-term maintainability through clear responsibility separation - Easier test construction without writing complex mock implementations - Healthier domain models through visible query intent - Lower coupling between business logic and the data-access layer > The Query Delegation Pattern isn't just a technical trick — it's a philosophy that pushes how you think about data-access-layer design in a cleaner direction. ### Related Documents - [ApexEloquent](https://krileworks.com/apexeloquent): the full picture of the OSS that adopts the Query Delegation Pattern - [Apex Stem](https://krileworks.com/apex-stem): the combination of the 4 OSS including ApexEloquent and the Handler-Usecase Architecture - [The Repository Pattern in Apex: Trial, Error, and Going Built-In](https://krileworks.com/apex-stem/docs/repository-pattern-challenges-builtin-solution-apex): why the Repository was built into ApexEloquent (and how it relates to the Selector Pattern) - [From Raw SOQL to a Chained-Method ORM in Apex](https://krileworks.com/apex-stem/docs/dynamic-query-creation-apex-eloquent): a migration primer from raw SOQL to Scribe - [MockEntry: How Apex Test Data Construction Becomes Possible](https://krileworks.com/apex-stem/docs/apex-eloquent-mockentry-deep-dive): the mock-side Deep Dive - [Catching Mock-Test False Positives: A Safety Net for SELECT Omissions](https://krileworks.com/apex-stem/docs/false-positive-detection-comprehensive-guide): catching SELECT omissions at unit-test time via coordination with Scribe - [Handler-Usecase Architecture](https://krileworks.com/apex-stem/docs/handler-usecase-architecture): where Query Delegation lands in real code (the Usecase layer) - [ApexEloquent Guide](https://krileworks.com/apex-stem/docs/apex-eloquent-guide): how to use Scribe / IEloquent / IEntry, plus the API reference ============================================================================== Source: https://krileworks.com/document/apex-eloquent-mockentry-deep-dive.md Page: https://krileworks.com/apex-stem/docs/apex-eloquent-mockentry-deep-dive ============================================================================== # MockEntry: How Apex Test Data Construction Becomes Possible 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](https://krileworks.com/apex-stem/docs/apex-eloquent-api-entry). ## 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` / `Id` and the like) - Parent-child relationship names (e.g. `Account`'s `Contacts`, lookup's `Parent__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 `insert`ing 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. ```apex // Build a Map, serialize to JSON, then deserialize into an SObject Map oppMap = new Map{ '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 an `attributes` key, 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**. ```apex // 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()` throws `The 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 via `entry.get('XXX')` throws immediately, just like a real `Entry` (see "SELECT Omission Detection" under [How MockEntry Works](#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. ```apex // 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](https://krileworks.com/apex-stem/docs/query-delegation-pattern). > 📘 Detailed: see the Deep Dive [Catching Mock-Test False Positives: A Safety Net for SELECT Omissions](https://krileworks.com/apex-stem/docs/false-positive-detection-comprehensive-guide), 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 ```apex 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{ '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 ```apex @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](https://krileworks.com/apex-stem/docs/apex-eloquent-data-access#building-mockentry). - **Parent-child hierarchical structure**: `setParent` / `setChildren` hangs 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 by `MockEntry`. Details in [Parent Fields, Child Subqueries, and Many-to-Many](https://krileworks.com/apex-stem/docs/apex-eloquent-relations) - **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 `autoId` can be named with `alias('opp')` and pulled out later via `getAliasId('opp')`. Useful when asserting "the Id of an upserted Opportunity matches the one set in the mock" - **Mocking aggregate-query results**: `MockEntry.asAggregateResult()` produces an `IEntry` not bound to an `AggregateResult` type. Mock `COUNT` / `SUM` / `GROUP BY` results 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` / `setChildren` in structural form - **Bulk data**: expand from one template via `times()` and placeholders - **Consistency with production**: the `IEntry` interface plus SELECT-omission detection guarantees the same behavior as production - **Generated Ids**: `autoId` + `alias` makes Ids retrievable by name for assertions - **Aggregate queries**: `AggregateResult` is 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](https://krileworks.com/apex-stem/docs/apex-eloquent-api-entry): comprehensive coverage of `MockEntry`'s full API - [Parent Fields, Child Subqueries, and Many-to-Many](https://krileworks.com/apex-stem/docs/apex-eloquent-relations): the usage guide for relation operations - [Data Access, DML, IEntry, and Mock](https://krileworks.com/apex-stem/docs/apex-eloquent-data-access): integration with `MockEloquent` (including Spy / failOn) - [Query Delegation Pattern](https://krileworks.com/apex-stem/docs/query-delegation-pattern): the design philosophy behind SELECT-omission detection - [ApexEloquent Guide](https://krileworks.com/apex-stem/docs/apex-eloquent-guide): the entry point to the whole ApexEloquent guide ============================================================================== Source: https://krileworks.com/document/false-positive-detection-comprehensive-guide.md Page: https://krileworks.com/apex-stem/docs/false-positive-detection-comprehensive-guide ============================================================================== # Complete False Positive Detection Guide This comprehensive guide covers ApexEloquent's MockEntry false positive detection system with detailed examples, real-world use cases, and advanced testing patterns. ## The Core Problem Traditional Salesforce testing relies on TestDataFactory and database inserts. When developers move to mock-based testing for performance benefits, they create a dangerous gap: **mock tests pass when production fails**. This happens because mock objects in tests have all fields populated in memory, while production SOQL queries may miss required fields. ApexEloquent's MockEntry closes this gap by validating field access against your actual query structure, ensuring mocks behave like real SOQL results. ## 🧪 Use Case 1: Detecting Omissions on the Main Object Let's see a real-world example with a service class that forgets to select the Name field: ### The Service Class (with a bug) ```apex public with sharing class OppNameUpdater { private final Id oppId; private final IEloquent eloquent; public OppNameUpdater(Id oppId, IEloquent eloquent) { this.oppId = oppId; this.eloquent = eloquent; } public Opportunity execute() { Scribe scribe = Scribe.source(Opportunity.getSObjectType()) .field('Id') // ← Forgetting to select 'Name' .whereEqual('Id', this.oppId); IEntry oppEntry = this.eloquent.first(scribe); // This line will throw an exception because 'Name' was not selected. System.debug('Opportunity Name: ' + oppEntry.getName()); Opportunity opp = (Opportunity) oppEntry.getRecord(); opp.Name = 'Updated Name'; return (Opportunity) this.eloquent.doUpdate(opp); } } ``` ### The Test Class (Catches the Error) ```apex @isTest private class OppNameUpdaterTest { @isTest static void testExecute_WhenNameNotSelected_ThrowsException() { // Arrange IEloquent mockEloquent = new MockEloquent( MockEntry.of(Opportunity.getSObjectType()).autoId('1') ); OppNameUpdater updater = new OppNameUpdater('006000000000001AAA', mockEloquent); // Act & Assert try { updater.execute(); Assert.fail('Expected a QueryException to be thrown.'); } catch (QueryException e) { // Assert that the helpful error message is correct String expectedMessage = 'The specified field is not selected in Scribe. object name: Opportunity, field name: Name'; Assert.areEqual(expectedMessage, e.getMessage()); } } } ``` 👏 **Perfect!** We detected the missing Name field during our unit test, well before deploying to production. ## 🔄 Detailed Field Access Validation MockEntry provides comprehensive validation for all field access patterns: ### 1. Direct Field Access ```apex @isTest static void testDirectFieldAccess() { // Only 'Id' selected, not 'Name' Scribe scribe = Scribe.source(Account.getSObjectType()).field('Id'); MockEntry mockEntry = MockEntry.of(Account.getSObjectType()).autoId(1); mockEntry = (MockEntry) mockEntry.setFieldStructure(scribe.buildFieldStructure()); // ❌ This throws QueryException String name = (String) mockEntry.get('Name'); } ``` ### 2. getId() and getName() Methods ```apex @isTest static void testGetIdMethod() { // Only 'Name' selected, not 'Id' Scribe scribe = Scribe.source(Account.getSObjectType()).field('Name'); MockEntry mockEntry = MockEntry.of(Account.getSObjectType()).add('Name', 'Test Account'); mockEntry = (MockEntry) mockEntry.setFieldStructure(scribe.buildFieldStructure()); try { Id accountId = mockEntry.getId(); // ❌ Id not selected Assert.fail('Expected QueryException'); } catch (QueryException e) { Assert.areEqual( 'The specified field is not selected in Scribe. object name: Account, field name: Id', e.getMessage() ); } } ``` ## 🔗 Use Case 2: Detecting Omissions in Relationships This powerful validation works for parent and child relationships, too. Here, the code forgets to select the parent Account's `Name`: ### The Service Class (with relationship bug) ```apex public Opportunity execute() { Scribe scribe = Scribe.source(Opportunity.getSObjectType()) .field('Id') .parentField( Scribe.asParent('AccountId').field('Id') // ← Forgetting to select Account's 'Name' ) .whereEqual('Id', this.oppId); IEntry oppEntry = this.eloquent.first(scribe); IEntry accountEntry = oppEntry.getParent('AccountId'); // This line will now throw an exception System.debug('Account Name: ' + accountEntry.getName()); // ... } ``` ### The Test Class (Catches the Relationship Error) ```apex @isTest static void testExecute_WhenAccountNameNotSelected_ThrowsException() { // Arrange IEloquent mockEloquent = new MockEloquent( MockEntry.of(Opportunity.getSObjectType()) .autoId(1) .addParent('AccountId', MockEntry.of(Account.getSObjectType()).autoId(1)) ); OppNameUpdater updater = new OppNameUpdater('006000000000001AAA', mockEloquent); // Act & Assert try { updater.execute(); Assert.fail('Expected a QueryException to be thrown.'); } catch (QueryException e) { // Assert that the error message correctly identifies the missing field in the parent String expected = 'The specified field is not selected in Scribe. object name: Account, field name: Name'; Assert.areEqual(expected, e.getMessage()); } } ``` ### 3. Parent Relationship Access Validation ```apex @isTest static void testParentAccess() { // Missing AccountId in field selection Scribe scribe = Scribe.source(Opportunity.getSObjectType()).field('Id'); MockEntry mockEntry = MockEntry.of(Opportunity.getSObjectType()) .autoId(1) .setFieldStructure(scribe.buildFieldStructure()); try { IEntry parent = mockEntry.getParent('AccountId'); // ❌ AccountId not selected Assert.fail('Expected QueryException'); } catch (QueryException e) { Assert.areEqual( 'The specified parentIdFieldName is not set in Scribe. object name: Opportunity, parent Id field name: AccountId', e.getMessage() ); } } ``` ### 4. Child Relationship Access ```apex @isTest static void testChildAccess() { // Child fields not properly selected Scribe scribe = Scribe.source(Account.getSObjectType()) .field('Id') .withChildren( Scribe.asChild(Contract.getSObjectType()).field('Id') // Missing 'Name' field ); MockEntry mockEntry = MockEntry.of(Account.getSObjectType()) .autoId(1) .addChildren( 'Contract', new List{ MockEntry.of(Contract.getSObjectType()).add('Name', 'Mock Contract 1'), // set Name field value MockEntry.of(Contract.getSObjectType()).add('Name', 'Mock Contract 2') } ); mockEntry = (MockEntry) mockEntry.setFieldStructure(scribe.buildFieldStructure()); List contracts = mockEntry.getChildren('Contract'); try { String contractName = (String) contracts[0].get('Name'); // ❌ Name value is set but not selected in scribe! Assert.fail('Expected QueryException'); } catch (QueryException e) { Assert.areEqual( 'The specified field is not selected in Scribe. object name: Contract, field name: Name', e.getMessage() ); } } ``` ## 🧮 Use Case 3: Aggregate Query Validation MockEntry also validates field access for aggregate queries, ensuring that aliases are correctly used: ### The Service Class (with aggregate bug) ```apex public class OpportunityAnalytics { private IEloquent eloquent; public OpportunityAnalytics(IEloquent eloquent) { this.eloquent = eloquent; } public void processAverages() { Scribe scribe = Scribe.source(Opportunity.getSObjectType()) .field('StageName') .average('Amount', 'avgAmount'); List results = this.eloquent.get(scribe); for (IEntry result : results) { Decimal avgAmount = (Decimal) result.get('avgAmount'); // ✅ Correct alias Decimal totalAmount = (Decimal) result.get('totalAmount'); // ❌ Wrong alias System.debug('Average: ' + avgAmount + ', Total: ' + totalAmount); } } } ``` ### The Test Class (Catches the Aggregate Error) ```apex @isTest static void testAnalytics_WhenAccessingWrongAlias_ThrowsException() { // Arrange: Set up the mock aggregate result IEloquent mockEloquent = new MockEloquent( new MockEntry(new Map{ 'StageName' => 'Prospecting', 'avgAmount' => 5000 }) ); OpportunityAnalytics service = new OpportunityAnalytics(mockEloquent); // Act & Assert try { service.processAverages(); Assert.fail('Expected a QueryException to be thrown.'); } catch (QueryException e) { // Assert that the error message correctly identifies the missing alias String expected = 'The specified field or alias is not exist in Scribe. field or alias name: totalAmount'; Assert.areEqual(expected, e.getMessage()); } } ``` ### Additional Aggregate Validation Example ```apex @isTest static void testAggregateValidation() { Scribe scribe = Scribe.source(Opportunity.getSObjectType()) .field('StageName') .average('Amount', 'avgAmount'); MockEntry mockEntry = MockEntry.of(Opportunity.getSObjectType()); mockEntry = (MockEntry) mockEntry.setFieldStructure(scribe.buildFieldStructure()); mockEntry = mockEntry.add('StageName', 'Closed Won') .add('avgAmount', 5000); try { // ❌ Accessing non-existent field in aggregate result Object maxAmount = mockEntry.get('maxAmount'); Assert.fail('Expected QueryException'); } catch (QueryException e) { Assert.areEqual( 'The specified field or alias is not exist in Scribe. field or alias name: maxAmount', e.getMessage() ); } } ``` ✅ This proves that the safety net works for both standard fields and aggregate aliases. ## 🔧 Integration with Business Logic MockEntry validation works seamlessly with your business logic classes: ```apex public class AccountService { private IEloquent eloquent; public AccountService(IEloquent eloquent) { this.eloquent = eloquent; } public List getAccountsWithIndustry() { // ❌ Query missing 'Industry' field Scribe scribe = Scribe.source(Account.getSObjectType()) .fields(new List{'Id', 'Name'}) .whereEqual('Type', 'Customer'); List entries = eloquent.get(scribe); List accounts = new List(); for (IEntry entry : entries) { Account acc = new Account(); acc.Id = entry.getId(); acc.Name = (String) entry.get('Name'); acc.Industry = (String) entry.get('Industry'); // ❌ Will fail here accounts.add(acc); } return accounts; } } @isTest static void testAccountService() { // Arrange MockEloquent mockEloquent = new MockEloquent(); AccountService service = new AccountService(mockEloquent); // Act & Assert - Test catches the missing field selection try { service.getAccountsWithIndustry(); Assert.fail('Expected QueryException for missing Industry field'); } catch (QueryException e) { // Test correctly identifies the production bug Assert.isTrue(e.getMessage().contains('Industry')); } } ``` ## Two Safety Nets Beyond SELECT Omission All four cases above came down to "check the `Scribe`'s SELECT clause against the access". Two other mechanisms close the same "green while verifying nothing" hole. ### Forgetting to feed the mock throws In a test that uses labels, forgetting `attach('X', ...)` while fetching under `label('X')` — or mistyping the label — makes **that query return zero rows**. You fall into the "nothing to do, skip" branch and **the test goes green having verified nothing**. Today, calling `get` / `first` / `firstOrFail` under a label that was never attached throws, and the error lists the labels that were attached. When you genuinely want the zero-row path, **attach an empty list** to declare it. ```apex MockEloquent mock = (new MockEloquent()) .attach(MyUsecase.LBL_FETCH, new List()); ``` The point of the design is that **"not attached" and "attached empty" are distinct states**. ### Entries handed over directly can carry a contract too The four cases above concerned entries returned through `MockEloquent`; they pass through a `Scribe`, so the contract attaches automatically. **Entries handed straight to the SUT have no contract.** The classic case is a batch: records reaching `execute(bc, scope)` never pass through `IEloquent`, so production is covered by the platform (they are real query results) while tests check nothing. `fetchedBy(scribe)` attaches the contract after the fact. ```apex MockEntry card = MockEntry.of(BusinessCard__c.class) .autoId(1) .set('CompanyName__c', 'Acme') .fetchedBy(RematchCompanyCardsHandler.scope()); ``` Pull the query construction into a `@TestVisible` method and your test hands over exactly the same `Scribe` production uses. See [API Reference: Scribe](https://krileworks.com/apex-stem/docs/apex-eloquent-api-scribe). ## 📝 Summary |Feature|Description| |---|---| |**Automatic Validation**|MockEntry throws an exception if you access a field not selected in Scribe.| |**High-Fidelity Mocks**|The mock system reproduces the exact validation behavior of a live query.| |**Full Relationship Support**|This validation works across parent, child, and many-to-many relationships.| |**Aggregate Query Support**|Validates access to aggregate function aliases and prevents wrong field access.| |**Prevents Production Errors**|Gives you confidence that a test that passes will also work in production.| :::info **Database-less tests that don't miss query defects.** This is the core of `ApexEloquent`'s testing philosophy. It provides a robust safety net that elevates your testing strategy to the next level. ::: MockEntry's false positive detection ensures your tests reflect real production behavior, giving you true confidence in your code quality and preventing unexpected runtime failures. By catching field selection errors during development rather than in production, you can deploy with confidence knowing your SOQL queries are complete and correct. ## 🔗 Back to Guide ← [Back to ApexEloquent Developer Guide](https://krileworks.com/apex-stem/docs/apex-eloquent-guide) ============================================================================== Source: https://krileworks.com/document/dynamic-query-creation-apex-eloquent.md Page: https://krileworks.com/apex-stem/docs/dynamic-query-creation-apex-eloquent ============================================================================== # Writing Dynamic Queries in Apex with ApexEloquent ## The Challenge with Traditional SOQL in Apex Verbose SOQL strings, the complexity of relationship queries, and slow, brittle, database-dependent tests... these are challenges every Salesforce developer faces. While powerful, the standard ways of writing queries in Apex often lead to code that is difficult to read, maintain, and, most importantly, test. - ❌ Poor Readability: Long, manually concatenated SOQL strings quickly become unmanageable. - ❌ Complex Dynamic Conditions: Adding conditions within an `if` block requires awkward string manipulation. - ❌ No Type Safety: Field name typos in a string query are only caught at runtime. - ❌ Difficult Relationship Queries: Handling parent and child relationships requires knowledge of specific, often non-intuitive relationship names. - ❌ Difficult to Test: Business logic is tightly coupled to the database, forcing the creation of extensive test data and leading to slow test execution. `ApexEloquent` was born to solve these challenges. ## The Solution: A Fluent, Testable ORM `ApexEloquent` is an ORM framework, inspired by Laravel Eloquent, that provides a fluent and intuitive interface for all data operations. It consists of two core components: - `Scribe`: A powerful, immutable query builder that allows you to construct any SOQL query through a chain of easy-to-understand methods. - `Eloquent`: The execution engine that runs the queries built by `Scribe` against the database. It also includes `MockEloquent` for testing. ## Basic Usage Building a query is simple and readable. You "describe" what you want, then "get" it. ```apex // 1. Arrange: Build the query with Scribe // Get the Id and Name of all Accounts in the 'Technology' industry Scribe scribe = Scribe.source(Account.getSObjectType()) .fields(new List{'Id', 'Name'}) .whereEqual('Industry', 'Technology') .orderBy('Name'); // 2. Act: Execute the query with Eloquent List accounts = (new Eloquent()).get(scribe); // 3. Utilize: Access the results for (IEntry acc : accounts) { System.debug(acc.getName()); } ``` This is far more maintainable than the equivalent SOQL string: ```soql SELECT Id, Name FROM Account WHERE Industry = 'Technology' ORDER BY Name ``` ## Key Features in Action ### Building Dynamic Queries `Scribe`'s immutable design makes it easy to add conditions dynamically without complex string logic. > The `if` form below also adds SELECT fields. If you are only toggling **WHERE conditions**, `ignoreWhen` expresses it as a single chain with no branching (`whereIn('Id', ids).ignoreWhen(ids.isEmpty())`). ```apex Scribe scribe = Scribe.source(Opportunity.getSObjectType()) .fields(new List{'Name', 'Amount'}) .whereIn('StageName', new List{'Prospecting', 'Qualification'}); // Add another condition only if a certain criteria is met if (includeHighValueDeals) { scribe = scribe.whereGreaterThan('Amount', 100000); } List opps = (new Eloquent()).get(scribe); ``` ### Handling Relationships with Ease You no longer need to look up confusing relationship names. ```apex // Get an Account and all of its related Contacts Scribe scribe = Scribe.source(Account.getSObjectType()) .field('Name') .withChildren( Scribe.asChild(Contact.getSObjectType()) .fields(new List{'LastName', 'Email'}) ) .whereEqual('Id', someAccountId); IEntry account = (new Eloquent()).first(scribe); List contacts = account.getChildren('Contact'); ``` ## Liberation from the Database: True Unit Testing The true power of `ApexEloquent` lies in its testability. **Scenario**: Test a service method, `AccountService.updateAccountType`, which updates an Opportunity's parent Account `Type` to 'Customer' if the Opportunity stage is 'Closed Won'. **Before**: **Traditional Testing** This requires inserting multiple records, making the test slow and dependent on the database. ```apex // Test data must be inserted Account testAcc = new Account(Name='Test', Type='Prospect'); insert testAcc; Opportunity testOpp = new Opportunity(Name='Test Opp', ...); insert testOpp; // Execute and Assert Account updatedAccount = AccountService.updateAccountType(testOpp.Id); Assert.areEqual('Customer', updatedAccount.Type); ``` **After**: **ApexEloquent** With `MockEloquent`, you test your logic in complete isolation, without any DML. ```apex // Set up the expected query result in memory IEloquent mockEloquent = new MockEloquent( new MockEntry( new Opportunity(Id = '...', StageName = 'Closed Won'), new Map{ 'AccountId' => new MockEntry(new Account(Id = '...', Type = 'Prospect')) } ) ); // Test the service class directly, with no database access AccountService service = new AccountService(mockEloquent); Account updatedAccount = service.updateAccountType('...'); // Assert the value of the object updated in memory Assert.areEqual('Customer', updatedAccount.Type); ``` ## Summary: Why Choose ApexEloquent? |Feature|Traditional SOQL|ApexEloquent| |--------|------------------|----------------| |Readability|Poor for complex queries|✅ Excellent via fluent API| |Dynamic WHERE|Manual string building|✅ Safe, chained methods| |Relationships|Requires relation name lookup|✅ Intuitive .withChildren() etc.| |Testability|❌ Requires DML & Test Data|✅ Database-free via MockEloquent| |Safety|Runtime errors for typos|✅ Finds errors during testing| ============================================================================== Source: https://krileworks.com/document/repository-pattern-challenges-builtin-solution-apex.md Page: https://krileworks.com/apex-stem/docs/repository-pattern-challenges-builtin-solution-apex ============================================================================== # Repository Pattern Challenges in Apex and the Built-in Repository Solution While there's abundant information about the Repository pattern on the internet with various implementation examples, applying these implementations to Apex presents several unique challenges. Here, I'll introduce the Repository class configuration patterns I've actually tried in Apex, along with their respective advantages and disadvantages. ## Pattern 1: Use Case-Based Repository This style defines individual Repository classes for each use case, where processing classes exist per use case. While this approach appears flexible in design, it creates the challenge of **mass generation of interfaces and concrete classes**. Since Apex does not support namespaces, **creating a large number of classes can easily lead to name collisions and naming exhaustion**, which is a design constraint. ## Pattern 2: Object-Based Repository (Selector Pattern) To reduce class count, there's also the approach of defining Repository (Selector) per SObject unit. Salesforce's official "Selector Pattern" introduces this style. 📚 [Official Selector Pattern Guide (Trailhead)](https://trailhead.salesforce.com/content/learn/modules/apex_patterns_dsl/apex_patterns_dsl_learn_selector_l_principles) However, for frequently used objects like `Opportunity` with multiple purposes, the following problems arise: - **Proliferation of detailed retrieval methods** to handle conditional branching - Increased **query control through flag arguments**, making caller intentions unclear - Method context becomes ambiguous, making **safe modifications impossible** I consider this situation a classic example of **"the road to debt paved with good intentions"**. ## Solution: Built-in Repository and Query Delegation Pattern Based on these challenges, I propose the **Query Delegation Pattern**. This pattern adopts a design where **query construction is performed in the domain layer, delegating only pure I/O processing to the Repository**. This allows having just one common "Built-in Repository" class. ### Advantages of This Pattern - ✅ Prevents excessive proliferation of Repository classes - ✅ Keeps the intention "I want to retrieve this kind of data" and query construction in the same place - ✅ Can absorb detailed query differences in the domain layer, avoiding Repository bloat - ✅ No need to define Interface, making tests dramatically easier to write In fact, this pattern in my projects has significantly lowered the psychological barrier to writing test code, improving productivity. ### A Key Principle: Immutability A core architectural principle of the query builder (`Scribe`) is its immutability. Every time you add a condition or a field (e.g., using `.whereEqual()` or `.field()`), it doesn't modify the original query object. Instead, it returns a new, modified instance. This design offers two powerful advantages: - **Safety**: It completely prevents side effects. A base query can be passed around your application without any risk of it being accidentally modified. - **Reusability**: It allows you to create a base query and then safely "branch" it into multiple, more specific queries **Example:** ```apex // Create a base query for all open opportunities Scribe baseQuery = Scribe.source(Opportunity.getSObjectType()) .whereEqual('IsClosed', false); // Safely create variations without modifying the baseQuery Scribe highValueQuery = baseQuery.whereGreaterThan('Amount', 100000); Scribe urgentQuery = baseQuery.whereDate('CloseDate', 'THIS_MONTH'); ``` ## Key Features of Eloquent and MockEloquent `Eloquent` and its test-double counterpart, `MockEloquent`, are core components of ApexEloquent. They provide the following features: ### Data Retrieval - **`get()` and `first()`**: Retrieve query results as `IEntry` objects, which act as wrappers for the underlying `SObject` records. ### DML Operations - **`doInsert()`, `doUpdate()`, `doDelete()`**: Perform standard DML operations for single `SObjects` or `List`. ### Complete Mocking Support - **MockEloquent**: Provides a complete, in-memory mock of all `Eloquent` methods, enabling true, database-free unit testing. - **Automatic Fake ID Assignment**: The mocked `doInsert()` method automatically assigns a realistic, fake record ID, simulating real database behavior. ### Advanced Testing Features - **Mocking Non-writable Fields**: Override values for any field in your tests—even formula fields and other read-only system fields—using the powerful `MockEntry`. - **"Select-Forgotten" Detection**: Get immediate feedback in your unit tests. The mock framework throws an error if you try to access a field that wasn't part of your `Scribe` query, ensuring your tests and production code behave identically. These features significantly reduce data dependency issues and enhance safety in unit testing. ## Comparison with Traditional Patterns | Aspect | Traditional Repository | Built-in Repository | |--------|----------------------|-------------------| | **Class Count** | High (multiple per use case) | Low (single shared class) | | **Test Complexity** | Complex mocking setup | Simple mock configuration | | **Query Location** | Scattered across repositories | Centralized in domain layer | | **Maintenance** | Difficult with many interfaces | Easy with single interface | | **Apex Compatibility** | Poor (namespace issues) | Excellent (Apex-optimized) | ## Conclusion The "Query Delegation Pattern" combined with a "Built-in Repository" is more than just a practical choice—it's a **paradigm shift for Apex development**. This approach liberates developers from the pitfalls of manual SOQL and database-dependent tests. It empowers teams to adopt a modern, test-driven workflow that was previously difficult to achieve on the Salesforce platform. By embracing this model, your team can not only reduce technical debt and accelerate development but also foster a culture of writing cleaner, more reliable, and fundamentally more maintainable code. ============================================================================== Source: https://krileworks.com/document/apex-blueprint-prerequisites.md Page: https://krileworks.com/apex-stem/docs/apex-blueprint-prerequisites ============================================================================== # ApexBlueprint Installation Prerequisites Before introducing Apex Blueprint, please ensure that the following tools and environments are properly set up. ## ✅ Salesforce CLI Setup Apex Blueprint deployment uses the Salesforce CLI. If you haven't installed it yet, please install it from the official website. - Installation verification: ```bash $ sf -v ``` If version information is displayed, you're all set. ## ✅ Target Organization Verification In your project root directory, run the following command to verify organization information: ```bash $ sf org list ``` Confirm that the organization you want to target has the 🍁`Default Org` indicator. Example: ``` ┌────┬─────────┬────────────┬────────────────────────────────────────┬────────────────────┬───────────┐ │ │ Type │ Alias │ Username │ Org Id │ Status │ ├────┼─────────┼────────────┼────────────────────────────────────────┼────────────────────┼───────────┤ │ 🌳 │ DevHub │ devhub │ example-user@example.com │ 00DxxxxxxxxxxxxXXX │ Connected │ │ 🍁 │ Sandbox │ sandbox │ example-user@example.com.sandbox │ 00DyyyyyyyyyyyyYYY │ Connected │ └────┴─────────┴────────────┴────────────────────────────────────────┴────────────────────┴───────────┘ ``` ## ⚙️ Default Organization Configuration If the 🍁 indicator is not present, log in and configure as follows: ```bash # Login to organization (replace alias and URL as appropriate) $ sf org login web --alias my-sandbox --instance-url https://orgfarm-xxxxxxx-dev-ed.develop.my.salesforce.com # Set as default organization $ sf config set target-org=my-sandbox ``` After configuration, run `sf org list` again to confirm the 🍁 mark is present. ## 🛠️ Make Command Setup Apex Blueprint uses a `Makefile` to simplify some deployment operations. Please verify that the `make` command is available. ```bash $ make -v ``` If you see output like the following, you're ready: ``` GNU Make 4.3 ``` ## 🔗 Back to Guide ← [Back to ApexBlueprint Developer Guide](https://krileworks.com/apex-stem/docs/apex-blueprint-guide) ============================================================================== Source: https://krileworks.com/document/apex-blueprint-installation-guide.md Page: https://krileworks.com/apex-stem/docs/apex-blueprint-installation-guide ============================================================================== # 🚀 Getting Started This guide walks you through the complete installation process for ApexBlueprint in your Salesforce project. ## 📥 Package Acquisition (First Time Only) Use Git Submodule to acquire `ApexBlueprint`: ```bash $ cd force-app/main/default/classes $ git submodule add https://github.com/krile136/ApexBluePrint.git ApexBlueprint ``` This will add the `ApexBlueprint` directory to your repository and incorporate it into source control. ## 🚀 Deploy to Organization Deploy the acquired classes with the following command: ```bash $ make install ``` The `make install` command internally calls Salesforce CLI deploy commands. The test classes include test data using standard objects and are **configured to meet the 75% coverage requirement**. However, **if test failures occur due to organization settings or disabled standard fields**, please adjust the `*_T.cls` test classes accordingly. ## 🔄 Updating ApexBlueprint To update ApexBlueprint, run the following commands from your project root: ```bash $ cd force-app/main/default/classes/ApexBlueprint $ git pull $ make install ``` :::warning Submodules maintain information (pointers) about which commit the parent repository references. To ensure consistency in production environments, we recommend using `git submodule update --remote` to automatically update the reference to the latest version, and then committing in the parent repository as well. ::: ## 🔗 Back to Guide ← [Back to ApexBlueprint Developer Guide](https://krileworks.com/apex-stem/docs/apex-blueprint-guide) ============================================================================== Source: https://krileworks.com/document/apex-blueprint-sblueprint-guide.md Page: https://krileworks.com/apex-stem/docs/apex-blueprint-sblueprint-guide ============================================================================== # Declaring a Single Record with SBlueprint The first step in assembling integration test data with ApexBlueprint is to declare a **blueprint for a single record** using `SBlueprint`. Instead of writing the procedural sequence "create an Account, set Industry to Technology, ...", you declare "what records I want to exist in the end" as a single expression. This page walks through the five core methods of `SBlueprint` (`of` / `.set` / `.template` / `.alias` / `.use`) in order, plus `.after`, which declares ordering alone. APIs that handle multiple records — relationships (`withChildren`) and bulk generation (`times`) — are covered in [Relations, Bulk Generation, and Reference Patterns](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk). ## of(SObjectType): The Starting Point Every `SBlueprint` declaration begins with this static method. It is the "first step" that tells the blueprint which SObject type you are about to assemble. **Signature:** `SBlueprint.of(System.Type recordType)` ```apex SBlueprint accountBp = SBlueprint.of(Account.class); ``` You then chain subsequent methods on the returned `SBlueprint` instance to stack values and relationships. ## .set(field, value): Setting a Field Value The method you'll use most often. Declare a field name and value, one pair at a time. **Signature:** `.set(String fieldName, Object value)` ```apex SBlueprint accountBp = SBlueprint.of(Account.class) .set('Name', 'Test Account') .set('Industry', 'Technology') .set('AnnualRevenue', 1000000); ``` ### Behavior Notes - If you call `.set()` multiple times for the same field, **the last call wins** - Values pre-set by `.template(...)` can be overridden by `.set(...)`. This forms the basis of the "shared defaults in template, only verification targets in `.set`" usage pattern - Write only the fields that are the verification target of that specific test. Required fields, RecordTypeId, and other values common to all tests should be pushed into `.template(...)` > The idea is to make "what does this test actually verify?" obvious from reading the `.set()` lines alone. ## .template(Map): Reusing Common Defaults `.template(...)` applies a pre-defined Map of values all at once. By consolidating RecordTypeId, required fields, and other defaults you'd like to reuse across all tests, you keep the `.set(...)` calls in each test body to a minimum. **Signature:** `.template(Map templateMap)` ### Best Practice: Consolidate Into a Single `Blueprints.cls` For templates reusable across multiple tests, the canonical approach is to put them all in a **single `Blueprints.cls`, as one method per SObject** — named `{short SObject name}Basic()` (`accBasic()`, `oppBasic()`, and so on). ```apex public with sharing class Blueprints { /** Base shape for a corporate account */ public static Map accBasic() { return new Map{ 'Name' => 'TestAccount', 'Industry' => 'Technology', 'AnnualRevenue' => 500000 }; } /** Account variant: enterprise (an order of magnitude higher revenue) */ public static Map accEnterprise() { return new Map{ 'Name' => 'EnterpriseAccount', 'Industry' => 'Financial Services', 'AnnualRevenue' => 10000000, 'NumberOfEmployees' => 1000 }; } /** Base shape for an opportunity */ public static Map oppBasic() { return new Map{ 'Name' => 'TestOpp', 'StageName' => 'Prospecting', 'CloseDate' => Date.today().addDays(30) }; } } ``` > **Why not one class per SObject?** Splitting them leaves each class as a thin wrapper that only returns a `Map`, scattered across many files. Keeping them together means **every base shape lives in one file**. When an admin adds a required field on the org and your integration tests all go red at once, the fix is obvious: add one line to the matching `xxxBasic()` map. Variants go in the same class with a prefix — `accEnterprise()`, `oppClosed()`. Each test then pulls in the template and overrides only the field that is the verification target. ```apex SBlueprint accountBp = SBlueprint.of(Account.class) .template(Blueprints.accBasic()) .set('Name', 'Acme Trading Co'); // In this test, only Name is the main concern ``` > Values like RecordTypeId that are "common to all tests but cause failures if forgotten" should always live in the template to be safe. ## .alias(name): Attaching an Identifier Attach a unique reference name (alias) to the blueprint. Aliases are used in two situations: - Another blueprint references this one's value via `.use(alias, ...)` - After `SOrchestrator.create()` runs, you retrieve the created record via `getByAlias(alias)` **Signature:** `.alias(String aliasName)` ```apex SBlueprint accountBp = SBlueprint.of(Account.class) .template(Blueprints.accBasic()) .alias('parentAccount'); ``` ### Behavior Notes - Aliases **must be unique** within the same `SOrchestrator`. Duplicates raise `Duplicate alias detected` at runtime - Blueprints without an explicit `.alias(...)` get an auto-alias like `__Account_0_1__`, but retrieving these via `getByAlias` later is impractical. **If you plan to retrieve a record, always attach an explicit alias** - Patterns that combine `.times(n)` with placeholder aliases like `'con_{#}'` are covered in [Relations, Bulk Generation, and Reference Patterns](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk) ## .use(alias, fromField, toField): Copying a Value from Another Blueprint This is the most multi-purpose API in ApexBlueprint. In a single line, "copy a value held by another blueprint into one of my fields" expresses both **building a relationship** and **copying data**. **Signature:** `.use(String aliasName, String fromField, String toField)` - `aliasName`: The alias of the blueprint you want to reference (set via `.alias()`) - `fromField`: The field to read from the source blueprint (e.g. `'Id'`, `'Industry'`) - `toField`: The field to write to on this blueprint (e.g. `'AccountId'`, `'Description'`) ### Use Case 1: Building a Relationship (Id Copy) The most common pattern. Link an `Opportunity` to an `Account`. `'parentAccount'` refers to the **identifier attached via `.alias('parentAccount')` on a separate Account blueprint**. ```apex SBlueprint oppBp = SBlueprint.of(Opportunity.class) .set('Name', 'Test Opportunity') .set('StageName', 'Prospecting') .set('CloseDate', Date.today().addDays(30)) .use('parentAccount', 'Id', 'AccountId'); ``` `SOrchestrator` inserts `parentAccount` first to get its Id, then inserts the `Opportunity` — the order resolution is fully automatic. You don't write a single line of code to carry the parent Id around in a variable. ### Use Case 2: Copying Data (Other than Id) `.use(...)` works on fields other than IDs too. It's handy for cases like "I want to copy the parent's Name into the child's description". ```apex SBlueprint contactBp = SBlueprint.of(Contact.class) .set('LastName', 'TestContact') .use('parentAccount', 'Name', 'Description'); ``` ### Advanced Usage Patterns like "link only some of the parents bulked with `{#}` to children" and "reference a grandparent (`{P0}` / `{P1}`)" are covered in detail in [Relations, Bulk Generation, and Reference Patterns](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk). ## .after(alias): Ordering Without a Value (v2.0.0+) `.use()` decides the order **as a side effect of carrying a value**. `.after()` is that same method with the value copy taken out. It guarantees only that the referenced blueprint is inserted in an earlier layer, and touches no fields at all. **Signature:** `.after(String aliasName)` ```apex SBlueprint.of(Task.class) .set('Subject', 'Follow-up call') .after('baseOpportunity'); // inserted after baseOpportunity ``` Reach for it when **nothing links the records field-wise, but the insert order still matters**. Trigger side effects are the usual reason: "the roll-up comes out wrong unless the opportunity already exists when the activity lands" is a real situation in which no lookup connects the two records. ```apex // ❌ Shoving an Id into a field you never read, purely to force an order .use('baseOpportunity', 'Id', 'WhatId') // ✅ If order is all you need, declare order .after('baseOpportunity') ``` Placeholders such as `{#}` work exactly as they do in `.use()` (`.after('opp_{#}')`), and the `.after(alias, startAt, interval)` overload mirrors the same sequence controls. > 📌 `.use()` and `.after()` become **edges in the same dependency graph** internally. The only difference is whether the edge carries a value; the ordering machinery is identical ([How Dependency Resolution Works](https://krileworks.com/apex-stem/docs/apex-blueprint-dependency-resolution-deep-dive)). ## Example: Combining Everything Combine the five core methods above into a single test, and you get: ```apex @isTest static void testOppCreation() { SOrchestrator.start() .add( SBlueprint.of(Account.class) .template(Blueprints.accBasic()) .alias('parentAccount') ) .add( SBlueprint.of(Opportunity.class) .set('Name', 'Test Opportunity') .set('StageName', 'Prospecting') .set('CloseDate', Date.today().addDays(30)) .use('parentAccount', 'Id', 'AccountId') .alias('targetOpp') ) .create(); // Verification Opportunity created = [ SELECT Id, Name, AccountId FROM Opportunity WHERE Name = 'Test Opportunity' LIMIT 1 ]; Assert.isNotNull(created.AccountId); } ``` Points: - The `Account` side is **left to the template**. Since this test focuses on creating an Opportunity, the Account's contents just need to be valid — anything works - The `Opportunity` side only `.set(...)`s **fields that are verification targets**. The intent that "this Opportunity ends up created and linked to an Account" reads directly from the code - `SOrchestrator` resolves the dependencies, so you don't need to think about parent-child insertion order > Since this page focuses on **declaring a single record**, the example writes two `SBlueprint`s into separate `.add(...)` calls and links them with `.use(...)`. In actual usage, however, when you have parent-child relationships, **`withChildren` lets you write the structure "Opportunity hanging under an Account" as a single blueprint, with the code's indentation hierarchy matching the data hierarchy and improving readability** — so that approach is preferred. See [Relations, Bulk Generation, and Reference Patterns](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk) for details. ## Related Documents - [Resolving Dependencies and Executing DML with SOrchestrator](https://krileworks.com/apex-stem/docs/apex-blueprint-sorchestrator-guide): The execution phase that brings the assembled blueprints to life - [Relations, Bulk Generation, and Reference Patterns](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk): Applications of `withChildren` / `times` / `{#}` / `{P0}` `{P1}` - [API Reference: SBlueprint](https://krileworks.com/apex-stem/docs/apex-blueprint-api-sblueprint): Full method signatures - [Back to the ApexBlueprint Guide](https://krileworks.com/apex-stem/docs/apex-blueprint-guide) ============================================================================== Source: https://krileworks.com/document/apex-blueprint-sorchestrator-guide.md Page: https://krileworks.com/apex-stem/docs/apex-blueprint-sorchestrator-guide ============================================================================== # Resolving Dependencies and Executing DML with SOrchestrator `SOrchestrator` is the **engine that turns blueprints assembled by `SBlueprint` into real records**. It automatically handles the parent → child insertion order, copying parent Ids into children, and resolving values referenced via aliases. This page walks through the four methods of `SOrchestrator` (`start` / `add` / `create` / `getByAlias`) in order, and ends with a list of common pitfalls in real-world usage. ## SOrchestrator.start(): Initialize the Builder Every operation begins with this static method. It returns an empty `SOrchestrator` instance; you `.add(...)` blueprints to it, then call `.create()` to execute. **Signature:** `SOrchestrator.start()` ```apex SOrchestrator orchestrator = SOrchestrator.start(); ``` ## .add(blueprint): Register a Blueprint in the Queue `.add(...)` registers a single `SBlueprint` into SOrchestrator's queue. **Signature:** `.add(SBlueprint blueprint)` ```apex SOrchestrator.start() .add( SBlueprint.of(Account.class) .template(Blueprints.accBasic()) .alias('parentAccount') ) .add( SBlueprint.of(Opportunity.class) .set('Name', 'Test Opportunity') .set('StageName', 'Prospecting') .set('CloseDate', Date.today().addDays(30)) .use('parentAccount', 'Id', 'AccountId') .alias('targetOpp') ); ``` ### Behavior Notes - **Addition order is ignored.** SOrchestrator analyzes the dependencies internally and re-derives the insertion order via topological sort - It is fine to write the child before the parent. You can write blueprints in "the most readable order" - Multiple `.add(...)` calls can be chained on the same SOrchestrator - If you express parent-child relationships within a single blueprint via `withChildren`, registering the parent alone is enough — children come along with it (see [Relations, Bulk Generation, and Reference Patterns](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk)) ## .create(): Resolve Dependencies and Run DML `.create()` analyzes the registered blueprints and **executes the DML inserts in the correct order**. By the time this call returns, every record is in the database and Ids have been issued. **Signature:** `.create()` ```apex SOrchestrator orchestrator = SOrchestrator.start() .add(/* ... */) .add(/* ... */); orchestrator.create(); ``` ### Behavior Notes - Internally runs a **topological sort** and inserts the depended-upon side (parents) first - Id references like `.use('alias', 'Id', 'AccountId')` copy the parent's Id (issued right after its insert) into the child's field before inserting the child - DML failures throw the usual Apex DML exceptions - **The return value is `void`.** You cannot append `.create()` to the `.add(...)` chain and assign it to a variable. Capture the `SOrchestrator` in a variable first, call `orchestrator.create()`, then call `getByAlias(...)` on that same variable ## .getByAlias(name): Retrieve a Created Record After `create()`, you can retrieve the created records by alias. You can bring "the Account you just created" to your hand without writing a SOQL query, which keeps assertion code short. **Signature:** `.getByAlias(String aliasName)` (returns `SObject`) ```apex SOrchestrator orchestrator = SOrchestrator.start() .add( SBlueprint.of(Account.class) .template(Blueprints.accBasic()) .alias('parentAccount') ); orchestrator.create(); Account parent = (Account) orchestrator.getByAlias('parentAccount'); Assert.isNotNull(parent.Id); ``` ### Behavior Notes - The return value is `SObject`, so the caller casts it to the desired SObject type - **Passing a non-existent alias returns `null`** (not an exception). Typos are easy to miss, so guard with `Assert.isNotNull(...)` right after retrieval as a safe default - Records bulked via `.times(n)` with a `'{#}'` placeholder alias (e.g. `'con_{#}'`) can be retrieved individually via the expanded aliases like `'con_1'` / `'con_2'` / ... (see [Relations, Bulk Generation, and Reference Patterns](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk)) ## Example: A Complete Test Workflow Combining `start` → `add` → `create` → `getByAlias` into a single test yields: ```apex @isTest static void testOppCreationWithAccount() { SOrchestrator orchestrator = SOrchestrator.start() .add( SBlueprint.of(Account.class) .template(Blueprints.accBasic()) .alias('parentAccount') ) .add( SBlueprint.of(Opportunity.class) .set('Name', 'Test Opportunity') .set('StageName', 'Prospecting') .set('CloseDate', Date.today().addDays(30)) .use('parentAccount', 'Id', 'AccountId') .alias('targetOpp') ); orchestrator.create(); Account parent = (Account) orchestrator.getByAlias('parentAccount'); Opportunity opp = (Opportunity) orchestrator.getByAlias('targetOpp'); Assert.areEqual(parent.Id, opp.AccountId); Assert.areEqual('Test Opportunity', opp.Name); } ``` Points: - **Not a single SOQL in the test body.** Assertion targets are retrieved directly via `getByAlias(...)` - We `.add(...)` the `Account` first here, but if we instead `.add(...)` the `Opportunity` first, the result would be identical — SOrchestrator re-orders things via dependency analysis - The parent Id is copied into the child by the `.use('parentAccount', 'Id', 'AccountId')` declaration alone. The procedure of "insert the parent, grab its Id into a variable, assign it to the child's AccountId..." disappears ## Common Pitfalls ### Circular Dependencies If you create a dependency where both `A.use('B', ...)` and `B.use('A', ...)` hold, the topological sort cannot resolve it and `.create()` fails with `Circular or invalid reference detected`. Reconsider the design — either make one side a one-way reference, or replace the parent-child link with `withChildren`. ### Duplicate Aliases If `.alias('foo')` appears in two places within the same `SOrchestrator`, you'll hit `Duplicate alias detected`. When bulking via `.times(n)`, declare aliases with a placeholder like `'foo_{#}'` so that unique aliases are issued after expansion. ### Reference to a Non-Existent Alias If you reference an alias that hasn't been declared anywhere — e.g. `.use('typoAlias', ...)` — the call fails at `.create()` with the same family of errors (`Circular or invalid reference detected`). Alias typos are easy to miss, so it pays to develop the habit of reviewing "the first argument of `.use`" alongside "its corresponding `.alias`" as a pair. ### Don't Rely on Auto-Generated Aliases for Retrieval Omitting `.alias(...)` is fine for registering a blueprint; internally it gets an **auto-generated alias** like `__Account_0_1__`. However, retrieving these via `getByAlias` later is impractical. **If you plan to retrieve a blueprint via `getByAlias`, always attach an explicit `.alias(...)`** as a rule. ## Related Documents - [Declaring a Single Record with SBlueprint](https://krileworks.com/apex-stem/docs/apex-blueprint-sblueprint-guide): How to build the blueprints you pass to `.add(...)` - [Relations, Bulk Generation, and Reference Patterns](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk): Applications of `withChildren` / `times` / `{#}` / `{P0}` `{P1}` - [API Reference: SOrchestrator](https://krileworks.com/apex-stem/docs/apex-blueprint-api-sorchestrator): Full method signatures - [Back to the ApexBlueprint Guide](https://krileworks.com/apex-stem/docs/apex-blueprint-guide) ============================================================================== Source: https://krileworks.com/document/apex-blueprint-relations-and-bulk.md Page: https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk ============================================================================== # Relations, Bulk Generation, and Reference Patterns In [Declaring a Single Record with SBlueprint](https://krileworks.com/apex-stem/docs/apex-blueprint-sblueprint-guide), we covered the five core methods for declaring a single record on its own. This page goes one step further and covers patterns for **structurally assembling multiple records**. Specifically, the following topics: - `withChildren`: nesting children inside a parent blueprint - `times(n)` + `{#}` placeholder: bulk generation with sequence numbers - `use` with offsets: linking only some bulked parents to children - `{P0}` / `{P1}`: referencing ancestors (immediate parent / grandparent) inside nesting - `parentIdField`: disambiguating which lookup to fill when the child has multiple parent lookups - `sharedWith`: declaring manual shares, and how they follow bulk generation ## withChildren: Nesting Children Under a Parent `withChildren(child)` lets you write "the child records of this blueprint" inline inside a single blueprint. The indentation hierarchy of your code becomes the data hierarchy directly, making parent-child relationships readable at a glance. ### Simple 1:1 Parent-Child ```apex SBlueprint.of(Account.class) .template(Blueprints.accBasic()) .alias('parentAccount') .withChildren( SBlueprint.of(Contact.class) .set('LastName', 'TestContact') .alias('childContact') ); ``` Points: - Copying the parent's Id into the child's `AccountId` is **automatic**. There's no need to write `.use(...)` - Defining the child blueprint **inline** inside `withChildren` visualizes the structure of "this parent has this child hanging under it" - If you attach `.alias(...)` on the child side, you can retrieve it via `getByAlias('childContact')` ### 1:N Parent-Child (Combined with `times`) Using `.times(n)` inside `withChildren` lets you hang multiple children under the same parent. ```apex SBlueprint.of(Account.class) .alias('parentAccount') .template(Blueprints.accBasic()) .withChildren( SBlueprint.of(Contact.class) .set('LastName', 'Contact-{#}') .alias('con_{#}') .times(3) ); ``` Three `Contact`s — `con_1` / `con_2` / `con_3` — are generated, all linked to the same `Account`. Because `{#}` is used both in the alias and the `LastName`, you can later retrieve them individually via `getByAlias('con_2')`. ### Nesting Further to Grandchildren `withChildren` is **nestable**. Three-tier structures like "Contact under Account, Case under Contact" feel natural to write. ```apex SBlueprint.of(Account.class) .alias('acc') .template(Blueprints.accBasic()) .withChildren( SBlueprint.of(Contact.class) .set('LastName', 'TestContact') .alias('con') .withChildren( SBlueprint.of(Case.class) .set('Subject', 'TestCase') .alias('case') ) ); ``` ### Multiple Child Types Under the Same Parent To hang **children of different SObject types** under the same parent, call `.withChildren(...)` repeatedly. ```apex SBlueprint.of(Account.class) .template(Blueprints.accBasic()) .alias('acc') .withChildren( SBlueprint.of(Contact.class) .set('LastName', 'TestContact') .alias('childContact') ) .withChildren( SBlueprint.of(Opportunity.class) .set('Name', 'TestOpportunity') .set('StageName', 'Prospecting') .set('CloseDate', Date.today().addDays(30)) .alias('childOpp') ); ``` `Contact` and `Opportunity` are each generated under the same `Account` with the parent Id copied into their respective `AccountId`. You can also use `.times(...)` on the child side, so uneven structures like "3 Contacts and 2 Opportunities under one Account" express naturally. ## Multiplication: Upper-Tier `times` Propagates Downward A crucial behavior to keep in mind when combining `withChildren` with `.times(n)`: **adding `.times(n)` at an upper tier re-generates the entire child subtree per parent**, so record counts multiply. ### Two Tiers: Parent N × Child M ```apex SBlueprint.of(Account.class) .set('Name', 'Acc-{#}') .alias('acc_{#}') .times(2) // 2 parents .withChildren( SBlueprint.of(Contact.class) .parentIdField('AccountId') .set('LastName', 'Con-{#}') .alias('con_{#}') .times(2) // 2 children per parent ); ``` Result: 2 `Account`s and **2 × 2 = 4** `Contact`s. ### Three Tiers and Beyond: Exponential Growth Adding more tiers compounds the count multiplicatively. ```apex SBlueprint.of(Account.class) .times(2) // 2 parents .withChildren( SBlueprint.of(Contact.class) .parentIdField('AccountId') .times(2) // 2 children per parent → 4 total .withChildren( SBlueprint.of(Case.class) .parentIdField('ContactId') .times(2) // 2 grandchildren per child → 8 total ) ); ``` Total record count: 2 `Account`s + 4 `Contact`s + **2 × 2 × 2 = 8** `Case`s. Remembering "the leaf count = the product of `.times(...)` along the hierarchy" makes it easy to anticipate counts. > Be careful: deeper hierarchies make **record counts grow exponentially**. Stacking five tiers with `.times(3)` each generates `3⁵ = 243` records, eating into the DML row governor limit (10,000). "Just a few more" can balloon into a massive count, so be deliberate about each tier's `.times(...)`. ## times + {#}: Bulk Generation with Sequence Numbers `.times(n)` produces `n` copies of the same blueprint. For cases like "3 Contacts" or "10 Accounts", you can do it in one line without a `for` loop. The `{#}` placeholder is embedded inside `.set(...)` values or the argument to `.alias(...)`. When expanded by `times(n)`, `{#}` is replaced with `1`, `2`, `3`, .... ```apex SBlueprint.of(Contact.class) .set('LastName', 'Contact-{#}') .alias('con_{#}') .times(3); ``` What gets generated: | alias | LastName | |---|---| | `con_1` | `Contact-1` | | `con_2` | `Contact-2` | | `con_3` | `Contact-3` | ### Alphabetic Sequences: `{A}` / `{a}` In addition to the numeric `{#}`, **alphabetic sequence** placeholders `{A}` / `{a}` are supported. `{A}` expands to `'A'` / `'B'` / `'C'` ..., and `{a}` expands to `'a'` / `'b'` / `'c'` .... ```apex SBlueprint.of(Account.class) .set('Name', 'Acc-{A}') .alias('acc_{a}') .times(3); ``` What gets generated: | alias | Name | |---|---| | `acc_a` | `Acc-A` | | `acc_b` | `Acc-B` | | `acc_c` | `Acc-C` | Use these for test data where you want **a human-readable distinction** (e.g. labels appearing in test reports), where letters read more naturally than numbers. ### startAt / interval: Changing the Start and Step `.set(...)` / `.alias(...)` have extra arguments to specify the **start** and **step** for `{#}`. ```apex SBlueprint.of(Account.class) .set('Name', 'Acc-{#}', 10, 2) // 10, 12, 14 .alias('acc_{#}', 10, 2) // acc_10, acc_12, acc_14 .times(3); ``` This handles finer-grained needs like "start the sequence from N" or "only even-numbered indices". ## `use` with Offsets: Linking Only Some Bulked Parents to Children Sometimes you want to link **only some** of the bulked parent records to children. For example, "out of 10 Accounts, only attach Contacts to the latter 5 (6 to 10)". `use` has 4- and 5-argument overloads that let you specify the **start number** and **step** of the alias being referenced. **Signature:** `.use(String alias, String fromField, String toField, Integer startAt [, Integer interval])` ```apex SOrchestrator.start() .add( SBlueprint.of(Account.class) .alias('acc_{#}') .template(Blueprints.accBasic()) .times(10) // acc_1 to acc_10 ) .add( SBlueprint.of(Contact.class) .set('LastName', 'Con-{#}') .alias('con_{#}', 6) // con_6 to con_10 .use('acc_{#}', 'Id', 'AccountId', 6) // reference acc_6 to acc_10 .times(5) ); ``` Five children — `con_6` through `con_10` — are created, each with `acc_6` through `acc_10` as their parent. This overload shines when you need to express **uneven mappings**, not "every child has the same parent". ## {P0} / {P1} / ...: Referencing Upper-Tier Values ### Why This Placeholder Is Needed In nestings that use `.times(...)` at each tier, "the true parent" as seen from a leaf record is **a different instance each time it's generated**. For example: - Parent (Account) `.times(2)` - Child (Contact) `.times(2)` - Grandchild (Case) `.times(2)` In this case, there are 8 grandchildren, 4 children (2 per parent), and 2 parents. From each grandchild's perspective, "its true parent (Contact)" is one specific Contact out of the 4. However, if you attach a simple `{#}` alias on the Contact side like `.alias('child_{#}')`, "the `child_1` under Acme-1" and "the `child_1` under Acme-2" end up **declaring the same alias twice, which raises a duplicate-alias error and halts execution**. There is a workaround. By embedding the parent's alias to form a **compound alias** like `.alias('{P0}_child_{#}')`, you get four unique aliases: `__Account_0_1___child_1` / `__Account_0_1___child_2` / `__Account_0_2___child_1` / `__Account_0_2___child_2`, and grandchildren can address them via `.use('__Account_0_1___child_1', ...)`. But this approach has drawbacks: - The grandchild's `.use(...)` side has to **mentally assemble the alias string of "its true parent"** - If you change the hierarchy later, the alias string assembly logic has to be rewritten everywhere - As a result, the test code reads like an "alias-string puzzle" rather than a "data structure" The `{P0}` / `{P1}` / `{P2}` ... placeholders avoid this structurally. SOrchestrator hierarchically resolves "the **true parent for me**, at the Nth tier from the top (0-indexed)" — so you don't have to attach aliases, and you don't have to mentally assemble alias strings. ### How to Count: Absolute Depth from the Root The counting is **not "distance upward from me"** — it's **absolute depth from the root**: - `{P0}`: the root (outermost parent, tier 0) - `{P1}`: one level below the root (tier 1) - `{P2}`: another level below (tier 2) - And so on, increasing as you go deeper So when you want to reference "tier 1's value from a fourth-tier record", you specify `{P1}` (not "three steps up from where I am"). ### Minimal Example: Using `{P0}` in Two Tiers Let's start with the simplest two-tier case, referencing `{P0}` (= the root). Here the child Contact's `Description` simply receives the parent Account's `Name`. ```apex SBlueprint.of(Account.class) // P0 (root) .set('Name', 'Acme') .withChildren( SBlueprint.of(Contact.class) .set('LastName', 'TestContact') .use('{P0}', 'Name', 'Description') // copy root Account's Name into Description ); ``` The Contact's Description becomes `'Acme'`. `{P0}` points to "the root Account", and you don't need to attach a `.alias(...)` on the parent side. This is enough to understand the basic pattern of "create one parent and copy its value into the child". More advanced usage — deeper hierarchies with `.times(...)` involved — is covered in the next example. ### Example: A Grandchild Copies Its True Parent's Value In a three-tier setup (parent / child / grandchild), the grandchild Case's `Subject` receives **the LastName of its own true parent Contact**. ```apex SBlueprint.of(Account.class) // P0 (tier 0 / root) .set('Name', 'Acme-{#}') .times(2) .withChildren( SBlueprint.of(Contact.class) // P1 (tier 1) .parentIdField('AccountId') .set('LastName', 'Contact-{#}') .times(2) .withChildren( SBlueprint.of(Case.class) // P2 (tier 2 = grandchild Case) .parentIdField('ContactId') .use('{P1}', 'LastName', 'Subject') // ← the LastName of its true parent Contact .times(2) ) ); ``` The records and the grandchild Case Subjects (= the LastName of each grandchild's true parent) are as follows: | Account | Contact (true parent) | Case (grandchild) Subject | |---|---|---| | `Acme-1` | `Contact-1` (under Acme-1) | `Contact-1` | | `Acme-1` | `Contact-2` (under Acme-1) | `Contact-2` | | `Acme-2` | `Contact-1` (under Acme-2) | `Contact-1` | | `Acme-2` | `Contact-2` (under Acme-2) | `Contact-2` | Each Contact has 2 grandchildren below it, so there are 8 grandchild Cases — the four combinations above, two each. There are two Contact records with the same `Contact-1` LastName (one under Acme-1, one under Acme-2), but each grandchild receives the value from **the real parent it actually hangs under**. Points: - Without attaching any aliases, **"the true parent for me" is hierarchically resolved automatically** - The counting is the absolute depth from the root (not the distance upward from your current position) - Let `withChildren`'s automatic processing handle parent Id copying — only use `{Pn}` when you specifically need to copy some other field value ## parentIdField: Disambiguating When the Child Has Multiple Lookups If a child object has **multiple lookup candidates** (e.g. `Contact` has both `AccountId` and `CustomAccount__c`), `withChildren` alone can't decide "which lookup to put the parent Id into". You disambiguate with `.parentIdField(...)`. **Signature:** `.parentIdField(String fieldName)` ```apex SBlueprint.of(Account.class) .alias('acc') .withChildren( SBlueprint.of(Contact.class) .parentIdField('AccountId') // specify which lookup gets the parent Id .template(Blueprints.conBasic()) ); ``` Objects without multiple lookups don't need `.parentIdField(...)`. Think of it as "add it when the ambiguity error fires" — a safety net you reach for after the fact. ## sharedWith: Sharing Follows Bulk Generation (v2.0.0+) `.sharedWith(user, accessLevel)` declares a manual share **as a final state**. You never assemble `Foo__Share` / `AccountShare` rows yourself, and you never think about inserting them after the parent. **Signature:** `.sharedWith(User user, String accessLevel)` (`accessLevel` is `'Read'` or `'Edit'`) What matters in the context of this page is that a share declaration **rides the same bulk machinery described above**. Parents multiplied with `times` get the same number of shares. ```apex SBlueprint.of(Invoice__c.class) .set('Name', 'Invoice-{#}') .alias('inv_{#}') .owner(admin) .sharedWith(rep, 'Read') .times(5); // → 5 Invoice__c records, and 5 matching Invoice__Share records ``` A share declared on a nested child behaves the same way — it **multiplies along with the parent's `times`**. ```apex SBlueprint.of(Account.class) .set('Name', 'Acme-{#}') .times(2) .withChildren( SBlueprint.of(Invoice__c.class) .owner(admin) .sharedWith(rep, 'Read') .times(3) // 6 invoices → 6 shares ); ``` None of this is special-cased for sharing. Internally `sharedWith` assembles a **sibling blueprint wired to the parent with `use()`**, so `times` propagation and `{Pn}` resolution take exactly the same path as any ordinary child (see [How Dependency Resolution Works](https://krileworks.com/apex-stem/docs/apex-blueprint-dependency-resolution-deep-dive)). ### A contradictory declaration fails before the DML Shares are not something you can always write, so declarations that cannot hold raise `ApexBlueprintException` during the analysis stage of `create()`. | Situation | Why | |---|---| | The object's org-wide default is `Public` | Manual share rows don't exist at all (`Foo__Share` is not found) | | Sharing with the very user given to `owner()` | The owner already has full access, and Salesforce rejects the manual share | | `accessLevel` is anything other than `'Read'` / `'Edit'` | Bad argument | None of these reach the DML. Instead of "I inserted it and the share wasn't there", you get a reasoned failure at declaration time. > 📌 For how to build the persona on the other side of the share, see the [SPersona API reference](https://krileworks.com/apex-stem/docs/apex-blueprint-api-spersona). ## Example: One Parent + Three Children + One Grandchild Bringing everything together into a single test: ```apex @isTest static void testAccountWithContactsAndCase() { SOrchestrator orchestrator = SOrchestrator.start() .add( SBlueprint.of(Account.class) .template(Blueprints.accBasic()) .set('Name', 'ParentAccount') .alias('acc') .withChildren( SBlueprint.of(Contact.class) .set('LastName', 'Contact-{#}') .use('{P0}', 'Name', 'Description') // root Account (P0)'s Name into Description .alias('con_{#}') .times(3) ) ) .add( SBlueprint.of(Case.class) .set('Subject', 'TestCase') .use('con_2', 'Id', 'ContactId') // link to the 2nd Contact .alias('targetCase') ); orchestrator.create(); Account parent = (Account) orchestrator.getByAlias('acc'); Contact con2 = (Contact) orchestrator.getByAlias('con_2'); Case targetCase = (Case) orchestrator.getByAlias('targetCase'); Assert.areEqual(3, [SELECT COUNT() FROM Contact WHERE AccountId = :parent.Id]); Assert.areEqual('ParentAccount', con2.Description); Assert.areEqual(con2.Id, targetCase.ContactId); } ``` Points: - The nested `withChildren` visualizes "3 Contacts hang under the Account" directly - `use('{P0}', 'Name', 'Description')` copies the parent Account's Name into each child Contact's Description - A `{#}`-bearing alias (`con_{#}`) lets you later retrieve the 2nd Contact as `'con_2'` and link a Case to it - The test body contains no complex procedure — "the final data structure I want" reads directly from the code ## Example: A Shared Reference Across Trees (Diamond Dependency) `withChildren` expresses a **tree**, but real integration tests often need a deeply nested child to reference a **shared record that sits outside the tree**. Take a four-tier `Account → Opportunity → Quote → QuoteLineItem` chain where the deepest `QuoteLineItem` references a shared `Product2`: the dependency is no longer a tree but a **diamond** (a DAG). Declare the parent-child chain with `withChildren`, the cross-tree reference with `use`, and register the shared record with its own `add` so an alias can reach it. The `add` order is free — SOrchestrator's topological sort works out that `Product2` and `Account` must precede `QuoteLineItem`. ```apex @isTest static void testQuoteLineItemReferencesSharedProduct() { SOrchestrator orchestrator = SOrchestrator.start() .add( SBlueprint.of(Product2.class) .set('Name', 'Widget') .alias('product') // shared record, outside the tree ) .add( SBlueprint.of(Account.class) .template(Blueprints.accBasic()) .withChildren( SBlueprint.of(Opportunity.class) .template(Blueprints.oppBasic()) .withChildren( SBlueprint.of(Quote.class) .set('Name', 'Q-2026') .withChildren( SBlueprint.of(QuoteLineItem.class) .template(Blueprints.qliBasic()) .set('Quantity', 3) .use('product', 'Id', 'Product2Id') // cross-tree reference .alias('qli') ) ) ) ); orchestrator.create(); Product2 product = (Product2) orchestrator.getByAlias('product'); QuoteLineItem qli = (QuoteLineItem) orchestrator.getByAlias('qli'); Assert.areEqual(product.Id, qli.Product2Id); } ``` Key points: - The shared record (`Product2`) is not a branch of the tree, so it gets its **own `add` + `alias`** rather than `withChildren` - The cross-tree reference is one line: `use('product', 'Id', 'Product2Id')`. Combined with the `withChildren` chain, the dependency graph becomes a diamond rather than a tree - Write `add` calls in whatever order reads best. Insert order is resolved by SOrchestrator's topological sort, so it never concerns you - Required fields on `QuoteLineItem` (`PricebookEntryId`, `UnitPrice`, and in practice a `PricebookEntry` supplied through its own `add`) belong in the `Blueprints.qliBasic()` template, leaving only the delta under test (`Quantity`) and the reference (`use`) in the test body ## Related Documents - [Declaring a Single Record with SBlueprint](https://krileworks.com/apex-stem/docs/apex-blueprint-sblueprint-guide): The five core methods - [Resolving Dependencies and Executing DML with SOrchestrator](https://krileworks.com/apex-stem/docs/apex-blueprint-sorchestrator-guide): The execution engine - [API Reference: SBlueprint](https://krileworks.com/apex-stem/docs/apex-blueprint-api-sblueprint): Full method signatures - [Back to the ApexBlueprint Guide](https://krileworks.com/apex-stem/docs/apex-blueprint-guide) ============================================================================== Source: https://krileworks.com/document/apex-blueprint-api-sblueprint.md Page: https://krileworks.com/apex-stem/docs/apex-blueprint-api-sblueprint ============================================================================== # API Reference: SBlueprint `SBlueprint` is the method-chain class used in ApexBlueprint to declare **a blueprint for a single record**. You start from `of(...)`, then stack values, identifiers, parent-child relationships, bulk generation, and references via the chain, and finally hand it to `SOrchestrator` to execute. For usage and typical scenarios, see [Declaring a Single Record with SBlueprint](https://krileworks.com/apex-stem/docs/apex-blueprint-sblueprint-guide) and [Relations, Bulk Generation, and Reference Patterns](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk). ## Static Factory | Method | Purpose | |---|---| | `SBlueprint.of(System.Type recordType)` | The starting point of an SBlueprint. Declare the target SObject type, in the form `SBlueprint.of(Account.class)` | Passing `null` or a non-SObject type to `recordType` raises an exception. ## Value Setters (Set & Template) | Method | Purpose | |---|---| | `set(String fieldName, Object value)` | Set a value on a single field. Subsequent calls to the same field follow "last wins". Can override values set by `template` | | `set(String fieldName, Object value, Integer startAt, Integer interval)` | Specify the **start** and **step** of the `{#}` placeholder. `set('Name', 'Acc-{#}', 10, 2)` → `'Acc-10'` / `'Acc-12'` / `'Acc-14'` | | `template(Map templateMap)` | Apply a Map of default values all at once. Recommended in combination with the practice of consolidating shared settings (RecordType, required fields) into `Blueprints.cls` | Passing a **negative value** to `startAt` / `interval` raises an exception. ```apex SBlueprint accountBp = SBlueprint.of(Account.class) .template(Blueprints.accBasic()) // shared defaults .set('Name', 'CustomName') // individual override .set('Index', 'No.{#}', 1, 1); // {#} sequence ``` ## Identifier (Alias) | Method | Purpose | |---|---| | `alias(String aliasName)` | Attach a unique reference name to this blueprint. Later, it can be referenced via `.use(alias, ...)` or `SOrchestrator.getByAlias(alias)` | | `alias(String aliasName, Integer startAt)` | For aliases containing `{#}`, specify the start of expansion | | `alias(String aliasName, Integer startAt, Integer interval)` | Specify the start and step | Aliases **must be unique** within `SOrchestrator`; duplicates raise `Duplicate alias detected` at runtime. When combining with `.times(n)`, use `{#}`-bearing aliases like `'con_{#}'` so that unique values are issued after expansion. ```apex SBlueprint.of(Contact.class) .alias('con_{#}') // con_1 / con_2 / con_3 .times(3); ``` ## Reference (Use) A multi-purpose API for mapping a value from another blueprint into a field on this one. Usable both for "copying an Id into a child's lookup (creating a relationship)" and "copying an arbitrary field value". | Method | Purpose | |---|---| | `use(String aliasName, String fromField, String toField)` | The basic form. Copy `fromField` from the blueprint named `aliasName` into this blueprint's `toField` | | `use(String aliasName, String fromField, String toField, Integer startAt)` | When referencing a `{#}`-bearing alias, specify the start of expansion | | `use(String aliasName, String fromField, String toField, Integer startAt, Integer interval)` | Specify the start and step | Passing a **negative value** to `startAt` / `interval` raises an exception. For detailed usage and "uneven mapping" patterns, see [Relations, Bulk Generation, and Reference Patterns](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk). ```apex SBlueprint.of(Contact.class) .use('acc_{#}', 'Id', 'AccountId', 6) // reference acc_6 to acc_10 .alias('con_{#}', 6) // children also start from 6 .times(5); ``` ## Bulk Generation (Times) | Method | Purpose | |---|---| | `times(Integer n)` | Generate `n` copies of the same blueprint. Passing `n <= 0` raises an exception | Using `.times(n)` **inside nested `.withChildren(...)`** causes record counts to multiply across hierarchies (parent 2 × child 2 = 4 children). See [Relations, Bulk Generation, and Reference Patterns > Multiplication](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk). ## Ordering-Only Dependency (After) | Method | Purpose | |---|---| | `after(String alias)` | Declares only "insert this in a later layer than that alias", with no value copied | | `after(String alias, Integer startAt)` | Sets the starting number when referencing an alias containing `{#}` | | `after(String alias, Integer startAt, Integer interval)` | Sets the starting number and step | `use()` exists to build a reference, so it always carries a value. Use `after()` when **you don't want the value but do need the order** — for example when a trigger requires that A already exists before B lands. ```apex SOrchestrator.start() .add(SBlueprint.of(Account.class).template(Blueprints.accBasic()).alias('acc')) .add( SBlueprint.of(Contact.class) .set('LastName', 'Yamada') .after('acc') // just place it in a later layer than acc; copy nothing ); ``` ## Owner & Sharing (Owner & Share) | Method | Purpose | |---|---| | `owner(User user)` | Sets the record owner. Sugar for `set('OwnerId', user.Id)` | | `sharedWith(User user, String accessLevel)` | **Declares a manual share as a final state.** `accessLevel` is `'Read'` or `'Edit'` | Writing `sharedWith` makes `create()` **auto-generate a sibling blueprint** (`Foo__Share` / `AccountShare`) that is inserted one layer after the record itself. The parent Id wiring is automatic too. These live on the blueprint because "who owns this record, and who can see it" is **part of the declared final state**, exactly like a field value. ```apex // Owned by an admin, rep gets Read only — the "hostile data" of a runAs audit test SOrchestrator.start() .add( SBlueprint.of(Invoice__c.class).alias('inv') .owner(admin) .sharedWith(rep, 'Read') ); ``` It composes with `times` / nesting / `{Pn}` (a child's share multiplies along with the child). An object whose OWD is Public, a share aimed at the owner itself, and an invalid `accessLevel` are all rejected **fail-fast**. ## Parent-Child Relationships | Method | Purpose | |---|---| | `withChildren(SBlueprint child)` | Nest children inside the parent blueprint. The parent Id is automatically copied into the child's lookup. To place children of different SObject types under the same parent, call multiple times: `.withChildren(...).withChildren(...)` | | `parentIdField(String fieldName)` | When the child has multiple lookups, specify which one receives the parent Id. Not needed when the lookup is unambiguous | ```apex SBlueprint.of(Account.class) .alias('acc') .withChildren( SBlueprint.of(Contact.class) .parentIdField('AccountId') // specify when multiple lookups exist .set('LastName', 'TestContact') ); ``` ## Placeholders Special placeholders that can be used inside string arguments to `.set` / `.alias` / `.use`. | Placeholder | Expansion Rule | |---|---| | `{#}` | Numeric sequence `1`, `2`, `3`, .... Expanded for `n` copies via `.times(n)`. `startAt` / `interval` change the start and step | | `{A}` | Uppercase alphabetic sequence `'A'`, `'B'`, `'C'`, ... | | `{a}` | Lowercase alphabetic sequence `'a'`, `'b'`, `'c'`, ... | | `{P0}` / `{P1}` / `{P2}` / ... | Hierarchically resolves "the true parent for me" using **absolute depth from the root**. Mostly used as the first argument to `.use('{P1}', ...)` to reference the true parent | `{P0}` ~ `{Pn}` are the structural solution to the problem where, in nested structures with `.times(...)` at each tier, the `{#}` expansion of an alias alone cannot identify "the true parent for me". For motivation and mechanics, see [Relations, Bulk Generation, and Reference Patterns > {P0} / {P1} / ...](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk). ## Common Exceptions Errors the framework detects are thrown as **`ApexBlueprintException`**. | Situation | Exception type / message (excerpt) | |---|---| | `of(...)` with null or a non-SObject type | `ApexBlueprintException`: invalid type | | `times(0)` or below | `ApexBlueprintException`: `Times must be greater than 0` | | Negative value for `startAt` / `interval` on `set` / `alias` / `use` | `ApexBlueprintException`: negative value not allowed | | Duplicate alias (at `create()` time) | `ApexBlueprintException`: `Duplicate alias detected` | | Reference to a non-existent alias (typo in `use`, at `create()` time) | `ApexBlueprintException`: `Circular or invalid reference detected` | | Ambiguity with multiple lookups (no `parentIdField`, at `create()` time) | `ApexBlueprintException`: `multiple parent relationships with the same parent object` | | Field application failure (non-existent / formula / auto-number / non-createable / type mismatch) | `ApexBlueprintException`: with diagnostics (`Reason:` for the cause, `Provided:` for the value) | | Missing required field or validation-rule violation | **A plain `DmlException`, unchanged** | ### The exception type tells you where to look When `create()` fails, **the type of the exception is itself the diagnosis**. - **`ApexBlueprintException`** = a mistake in the declaration. **Fix the test code** - **`DmlException`** = the org refused the insert. **Fix the template or the org configuration** Field-application errors are diagnosed all the way down to "which blueprint (alias), through which path (`set` / `template` / `use`), and why (non-existent / formula / auto-number / non-createable / type mismatch)". > ⚠️ This is a breaking change in v2.0.0. Previously a plain `DmlException` was thrown, so **existing `catch (DmlException)` blocks will no longer catch these**. Review your catch clauses when migrating. ## Related Documents - [Declaring a Single Record with SBlueprint](https://krileworks.com/apex-stem/docs/apex-blueprint-sblueprint-guide): How to use the five core methods - [Relations, Bulk Generation, and Reference Patterns](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk): Applications of `withChildren` / `times` / `{P0}` etc. - [API Reference: SOrchestrator](https://krileworks.com/apex-stem/docs/apex-blueprint-api-sorchestrator): The execution engine API - [Back to the ApexBlueprint Guide](https://krileworks.com/apex-stem/docs/apex-blueprint-guide) ============================================================================== Source: https://krileworks.com/document/apex-blueprint-api-sorchestrator.md Page: https://krileworks.com/apex-stem/docs/apex-blueprint-api-sorchestrator ============================================================================== # API Reference: SOrchestrator `SOrchestrator` is the **execution engine** of ApexBlueprint. It takes the blueprints assembled by `SBlueprint`, analyzes their dependencies via topological sort, and runs DML inserts in the correct order. The API is intentionally limited to **just four methods**. For usage and typical scenarios, see [Resolving Dependencies and Executing DML with SOrchestrator](https://krileworks.com/apex-stem/docs/apex-blueprint-sorchestrator-guide). ## Static Factory | Method | Purpose | |---|---| | `SOrchestrator.start()` | For normal use. Uses the standard DML executor internally | | `SOrchestrator.start(IDmlOperator dmlOperator)` | Swap out the DML execution layer. In tests, pass `new MockDmlOperator()` to **verify behavior without firing real DML** | ```apex // Production (real DML) SOrchestrator orchestrator = SOrchestrator.start(); // Tests (no DML — the pattern used inside ApexBlueprint's own tests) SOrchestrator orchestrator = SOrchestrator.start(new MockDmlOperator()); ``` `MockDmlOperator` is included inside the ApexBlueprint OSS (`DmlOperators/`) and is used primarily when writing **ApexBlueprint's own tests**. For ordinary test data generation, use `SOrchestrator.start()` (no arguments). ## Registration and Execution | Method | Purpose | |---|---| | `add(SBlueprint blueprint)` | Register a single `SBlueprint` into the queue. **Addition order is ignored** (internal topological sort determines the insertion order) | | `create()` | Analyze all registered blueprints and run DML inserts after dependency resolution. **Returns `void`** — capture the `SOrchestrator` first, then call `create()` on it | ```apex SOrchestrator orchestrator = SOrchestrator.start() .add(SBlueprint.of(Account.class).alias('parentAccount').template(Blueprints.accBasic())) .add( SBlueprint.of(Opportunity.class) .set('Name', 'Test Opportunity') .use('parentAccount', 'Id', 'AccountId') .alias('targetOpp') ); orchestrator.create(); ``` ## Retrieving Results | Method | Purpose | |---|---| | `getByAlias(String aliasName)` | After `create()`, retrieve the created SObject by alias. Returns `SObject` (caller casts) | Passing a non-existent alias returns **`null`** (not an exception), so if typos are easy to miss, guard with `Assert.isNotNull(...)` right after retrieval. ```apex Account parent = (Account) orchestrator.getByAlias('parentAccount'); Opportunity opp = (Opportunity) orchestrator.getByAlias('targetOpp'); ``` Blueprints bulked via `.times(...)` can be retrieved individually by their post-expansion alias names (`'con_1'` / `'con_2'` / ...). Blueprints nested via `withChildren` are assigned **auto-generated aliases** like `__Account_0_1___Contact_1_1__`, so attach an explicit `.alias(...)` whenever you need to retrieve them. ## Common Exceptions (at `create()` Time) | Situation | Exception type / message (excerpt) | |---|---| | Circular dependency (both `A.use('B', ...)` and `B.use('A', ...)` hold) | `ApexBlueprintException`: `Circular or invalid reference detected` | | Duplicate alias within the same blueprint chain | `ApexBlueprintException`: `Duplicate alias detected` | | Duplicate alias across different `.add(...)` calls | `ApexBlueprintException`: `Duplicate alias detected` | | Reference to a non-existent alias via `.use(...)` | `ApexBlueprintException`: `Circular or invalid reference detected` (internally the same category) | | Child has multiple lookups but no `parentIdField` | `ApexBlueprintException`: `multiple parent relationships with the same parent object` | | Conditions that fail standard Apex DML (missing required fields, validation rule violations, etc.) | **A plain `DmlException`, unchanged** | **The type of the exception is itself the diagnosis.** `ApexBlueprintException` means a mistake in the declaration (fix the test code); `DmlException` means the org refused the insert (fix the template or the org configuration). > ⚠️ This is a breaking change in v2.0.0. Framework validation errors used to be plain `DmlException`s, so existing `catch (DmlException)` blocks will no longer catch them. ## Related Documents - [Resolving Dependencies and Executing DML with SOrchestrator](https://krileworks.com/apex-stem/docs/apex-blueprint-sorchestrator-guide): How to use start / add / create / getByAlias - [API Reference: SBlueprint](https://krileworks.com/apex-stem/docs/apex-blueprint-api-sblueprint): The API for the blueprints you pass to `.add(...)` - [Relations, Bulk Generation, and Reference Patterns](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk): Applications of `withChildren` / `times` / `{P0}` etc. - [Back to the ApexBlueprint Guide](https://krileworks.com/apex-stem/docs/apex-blueprint-guide) ============================================================================== Source: https://krileworks.com/document/apex-blueprint-api-spersona.md Page: https://krileworks.com/apex-stem/docs/apex-blueprint-api-spersona ============================================================================== # API Reference: SPersona `SPersona` is a builder for **creating the restricted users ("personas") you run `System.runAs` against**. Like `SBlueprint`, it is immutable — every method returns a new instance. Writing permission tests means creating a test user, and that comes with a pile of subtle traps (a unique Username, mixed DML, locale defaults, resolving the Profile name). `SPersona` centralises the one correct implementation in a single class. **Don't hand-write a `UserFactory`.** ## Static Factory | Method | Purpose | |---|---| | `SPersona.of(String name)` | The entry point. Pass a short persona name (`'sales-rep'`), used for `LastName` and friends | ## Building | Method | Purpose | |---|---| | `profile(String profileName)` | Set the Profile **by name** (`'Standard User'`, `'Minimum Access - Salesforce'`). Resolved at `create()` and cached for the whole test run | | `permissionSets(String permissionSetName)` | Add one PermissionSet by **API name** (not the label) | | `permissionSets(List permissionSetNames)` | Same, several at once | | `set(String fieldName, Object value)` | Override any User field. Same last-wins semantics as `SBlueprint.set()` | ## Creating | Method | Returns | Purpose | |---|---|---| | `create()` | `User` | Resolves the profile, builds the User with safe defaults, inserts it together with its PermissionSetAssignments, and returns it | ```apex User rep = SPersona.of('sales-rep') .profile('Standard User') .permissionSets('InvoiceReadOnly') .set('LanguageLocaleKey', 'en_US') .create(); ``` ## Behaviour worth knowing - **Usernames are issued as UUIDs**, so they never collide under parallel test execution - **Locale defaults come from the running user**, keeping them org-independent. Override with `set()` if you need to - **Mixed DML is avoided internally** (the insert is wrapped in `System.runAs`), so you can mix it freely with ordinary DML - **`create()` is test-context only.** It relies on `System.runAs`, so production code cannot call it - **Nothing is cached per call.** Every call creates a fresh user (only the Profile-name-to-Id resolution is cached) - ⚠️ **Profile names are locale-dependent.** `'Standard User'` in an English org, `'標準ユーザー'` in a Japanese one ## Use it with hostile data `SPersona` earns its keep alongside `SBlueprint`'s `owner()` / `sharedWith()`. Together they declare **hostile data**: owned by an admin, with only the bare minimum shared to the persona under test. ```apex User admin = SPersona.of('admin').profile('System Administrator').create(); User rep = SPersona.of('sales-rep').profile('Standard User').create(); SOrchestrator orchestrator = SOrchestrator.start() .add( SBlueprint.of(Invoice__c.class).alias('inv') .owner(admin) // owned by admin .sharedWith(rep, 'Read')); // rep gets Read only orchestrator.create(); System.runAs(rep) { // Verify what rep can see, and what rep can write } ``` **Whether a record was `sharedWith` becomes the expected visibility.** If a record you never shared turns up inside the `runAs` block, that is a hole in your sharing configuration. ## Consolidating on the project side Follow the same idea as `Blueprints.cls`: **keep project-specific personas in a single `Personas.cls`**. Spell Profile names and PermissionSet names inline across tests and a rename on the org side scatters the fix across every file. ```apex public with sharing class Personas { /** A rank-and-file sales rep */ public static User salesRep() { return SPersona.of('sales-rep') .profile('Standard User') .permissionSets('InvoiceReadOnly') .create(); } } ``` ## Related Documents - [API Reference: SBlueprint](https://krileworks.com/apex-stem/docs/apex-blueprint-api-sblueprint): declaring hostile data with `owner` / `sharedWith` - [API Reference: SOrchestrator](https://krileworks.com/apex-stem/docs/apex-blueprint-api-sorchestrator): dependency resolution and real DML - [ApexBlueprint guide](https://krileworks.com/apex-stem/docs/apex-blueprint-guide): back to the guide index ============================================================================== Source: https://krileworks.com/document/declarative-data-specification.md Page: https://krileworks.com/apex-stem/docs/declarative-data-specification ============================================================================== # Declarative Data Specification: Why the Blueprint Form? > **Who this article is for**: Developers and architects who want to understand the **design rationale and philosophy** behind ApexBlueprint. We read "why the API took this form" through a comparison with traditional procedural factory patterns. This page doesn't dive into the internal implementation; for that, see the sister article [Dependency Resolution Internals](https://krileworks.com/apex-stem/docs/apex-blueprint-dependency-resolution-deep-dive). At the heart of ApexBlueprint's design is the idea of replacing **"writing down a procedure"** with **"declaring the final shape of the data"** when creating integration test data. This page contrasts the approach with **procedural test-data factory patterns in general** to dig into how "Declarative Data Specification" solves the problem from a different angle. > The comparison target on this page is not a specific OSS library, but rather **"factory patterns in general that hold data generation logic inside methods"**. We aren't talking about whether individual TestDataFactory implementations are good or bad; we're looking at the properties of the form itself — "trapping data generation inside methods". ## Procedural Test-Data Factory Patterns By "procedural factory" we mean a design where **record-creation logic is written inside static methods, and the caller receives completed records simply by calling those methods**. This style is widely adopted in Salesforce integration testing, and it works fine in simple cases. But broadly speaking, it falls into two typical patterns, each with its own weakness. ### Pattern A. A "Create-One-of-Each" Method per SObject A row of methods like `createAccount()` / `createOpportunity()` / `createContact()`, one per object, each producing "one record with typical values". The caller invokes them in order and assembles relationships using the returned IDs. ```apex @isTest static void someTest() { Account acc = TestDataFactory.createAccount(); Contact con = TestDataFactory.createContact(acc.Id); // carry the parent Id Opportunity opp = TestDataFactory.createOpportunity(acc.Id); // ... } ``` Weaknesses that emerge in this pattern: - **The ID bucket relay stays on the caller side**: Receiving a parent record's ID and passing it as an argument to the child becomes a procedure the caller writes every time. As hierarchies deepen, the number of local variables for parent IDs grows until "connection code" outweighs the actual test body - **What data gets created is invisible**: From method names, you can guess the **count level** ("one Account, one Contact, one Opportunity"), but to know what's actually happening inside (which fields are set, whether formulas run, whether required fields are satisfied), you have to follow the method definition - **Parent-child structure is only inferable from formal ID arguments**: The relationship "the Contact hangs under the Account" can only be glimpsed from the fact that `acc.Id` was passed as an argument ### Pattern B. A Dedicated Method per Scenario A row of methods specific to verification scenarios, like `createOppForX01WithFlagA()` / `createOppForX02WithFlagB()`. The caller invokes one line and gets a complex data structure. ```apex @isTest static void someTest() { Opportunity opp = TestDataFactory.createOppForX01WithFlagA(); // ... } ``` Weaknesses that emerge in this pattern: - **Method explosion / argument explosion**: As scenario combinations grow, either new methods proliferate or existing methods' argument lists (boolean parades) expand. Once you see a call like `createOpp(true, false, true, false)`, you have no choice but to read the method definition to understand "what's being created" - **Bloated responsibility inside the factory**: The "which scenario to build" branching accumulates inside the factory, and modifications can no longer be localized - **High comprehension cost when revisiting later**: Reading the caller code, the method name alone doesn't tell you what's inside; you must always open the factory implementation to grasp the intent ### Pattern C. One Giant Factory for Entire Business Scenarios A hybrid of A and B that's the most common form in the field: **"create all objects required by a business scenario inside one shared method"**. A general-purpose method like `createTestData()` internally generates Account / Opportunity / Quote / QuoteLineItem / Product all at once, and every test calls and reuses it. ```apex @isTest static void someTest() { TestDataFactory.createTestData(); // Account / Opportunity / Quote / QuoteLineItem / Product all exist Opportunity opp = [SELECT Id, Amount FROM Opportunity LIMIT 1]; // ... verify opp ... } ``` The caller does it in a single line, and seemingly avoids both ID bucket relay and method explosion. But as scale grows, a different kind of structural weakness comes to the surface: - **Side effects are not readable**: The test body only shows `createTestData()`, so to judge "**what does it mean for a Quote to exist alongside this test?**" or "**does the existence of a QuoteLineItem affect what's being verified?**", you ultimately have to open the factory implementation and grasp the contents of every record - **Half-day investigations when tests fail**: It's hard to tell whether the failure is a logic problem or a trigger side effect from records created by `createTestData` - **Pressure on governor limits from unnecessary records**: For verifying one spec, five SObjects' worth of records irrelevant to the test get created every time. As test counts grow, you start hitting governor limits - **Collapse of test independence**: A small tweak to `createTestData` regularly causes **20 seemingly unrelated tests to fall over** in a chain - **You still can't escape A's and B's weaknesses**: Derived methods like `createTestDataForApproval()` / `createTestDataForCancellation()` proliferate based on scenarios, and boolean argument parades creep in too. You end up with the worst-of-all situation: **both method explosion and argument explosion at once** This pattern was introduced to escape the surface-level friction of A and B, but as the scale grows it transitions to a deeper problem: **you can no longer see the overall shape and side effects of the data from the test body**. ### The Common Root of All Three Patterns For A, B, and C alike, the structural cause of the weakness is the same: **data generation logic is trapped inside the method**. The method's output is a completed record; the form "what field structure was used, what relationships were made" never appears in the caller's code. On top of that, since the factory's methods themselves hold the "condition → value" generation logic, the only moves you can make as scenarios grow are "add a method or add an argument" — a structural constraint. And here's where the **shift in how we work with code in the AI era** comes into play. As AI becomes the writer of code, the human role shifts from "**writing** code" to "understanding **what intent** the produced code was written with". Put another way, **understanding intent becomes the human's job**. At this moment, procedural factories have a decisive structural weakness. The intent of a scenario like "an Account with three Contacts hanging under it, where only one of those Contacts has an Opportunity linked" is **hidden behind the method call (in the factory implementation), and cannot be recovered from reading the caller's test code**. A method name can be a **label for the intent**, but it can never be **the intent itself**. Whether the label is accurate or not — you can only find out by opening the factory. What Declarative Data Specification is solving is precisely this problem of **"surfacing intent as a structure"**. If "what data should ultimately exist" is written **as the code's structure itself**, you no longer need to follow method definitions; whether the code was written by an AI or a human, the **intent can be read in the shortest path**. Beyond just lowering the cost for the reader, **the code itself doubles as documentation of intent**. ## What Declarative Data Specification Is ApexBlueprint's response to these strains is not to "**add more APIs**" but to "**change what's being written**". Concretely, instead of writing "the steps to create the data", you write "**the structure of the data you ultimately want to exist**" as a single expression. Mechanical work like parent Id copying, insertion order, and alias resolution is all delegated to the framework. ```apex SOrchestrator.start() .add( SBlueprint.of(Account.class) .template(Blueprints.accBasic()) .alias('acc') .withChildren( SBlueprint.of(Contact.class) .set('LastName', 'Contact-{#}') .times(3) ) .withChildren( SBlueprint.of(Opportunity.class) .set('Name', 'TestOpportunity') .set('StageName', 'Prospecting') .set('CloseDate', Date.today().addDays(30)) ) ) .create(); ``` Reading this code top to bottom, you immediately see the final data structure: "**one Account, with three Contacts and one Opportunity hanging under it**". No Id juggling. No insertion-order management. The code's indentation hierarchy matches the data hierarchy directly. This is the heart of the **Declarative Data Specification** idea. The shift from "how to create data" to "**a blueprint of data**". And this shift produces a side benefit: **the code structure itself becomes documentation of intent**. "What does this test create, and what does it verify?" can be read directly from the **shape** of the test body, without opening a separate file. ### What Disappears from the Structure Writing declaratively makes the following elements disappear from test code: - Code that carries the parent record's Id around in a temporary variable - Code that arranges the insertion order by thinking about it - Code that copies values into the child's lookup - The need to use comments to explain "what data ultimately gets created" In their place, only the information that **the test actually needs** remains: data hierarchy, counts, and field values. ### Mechanical Work Automated Away Behind the scenes, ApexBlueprint handles the following: - **Dependency analysis and topological sort** for all blueprints - Automatic determination of the **parent → child insertion order** - **Automatic copying** of parent Ids into child lookups - **Hierarchical resolution** of values referenced via aliases (including `{P0}` / `{P1}`) - **Integrity checks** for circular dependencies / duplicate aliases / invalid references / ambiguous lookups To the user these look like "everything just works once you declare the dependencies", but internally several independent problems are being solved at once. ### Templates Are "Field Presets", Not "Methods" A notable difference from the procedural factories described above is **how shared defaults are reused**. ApexBlueprint's templates (`Blueprints.cls`) hold defaults as a **Map of field values, not as methods**. ```apex public with sharing class Blueprints { public static Map accBasic() { return new Map{ 'Name' => 'TestAccount', 'Industry' => 'Technology', 'AnnualRevenue' => 500000 }; } } ``` You load these via `.template(...)`, and in each test you override only the **fields that are the verification target** with `.set(...)`. ```apex SBlueprint.of(Account.class) .template(Blueprints.accBasic()) .set('Name', 'Acme Trading Co'); // only the field that matters in this test ``` Key points: - **Templates are declarations of field combinations only and hold no logic** (no `if` branching, no conditional value generation) - **Scenario differences are expressed not in the template but in the `.set(...)` calls in the test body** - As a result, you basically don't need to add more methods to the template — "a few typical forms per SObject" is enough - Reading the test body shows directly "what this test is changing and what it's verifying" from the `.set(...)` lines In other words, ApexBlueprint steps out of the "either add methods or add arguments" dichotomy entirely, and introduces a division of labor: **"templates are where presets live; the test body is where differences are written"**. ### Relationship with the DRY Principle After reading the above you may have the question: "**Isn't writing the final data shape directly in each test a violation of DRY?**" Similar blueprint chains showing up repeatedly across multiple tests does, on the surface, look like duplication of "code shape". But DRY's original definition is "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system" (`The Pragmatic Programmer`). It is about consolidating **knowledge** in one place — not about avoiding repetition of code shape. Looking at ApexBlueprint's design through this lens, you can see that it **deliberately separates "what should be DRY" from "what should be exposed"**: - **Shared field combinations (= knowledge)**: consolidated into templates like `Blueprints.accBasic()` → **satisfies DRY** - **Test-specific data hierarchy and verification intent (= that test's claim)**: written directly in the test body → **exposed as intent** Even when similar structures appear across multiple tests, what each test verifies is a different intent — this is not duplication of knowledge but **per-intent specificity**. Applying DRY literally — "if the structures look similar, consolidate them into a shared method" — leads right back to the phenomenon we most want to avoid in the AI era: **intent hiding behind a method name**. To put ApexBlueprint's stance in a single line: > **DRY for knowledge; exposed for intent** This division of labor reconciles the original spirit of DRY (a single source of knowledge) with the demand of the AI era (transparent intent). On the surface, "duplication of code shape" is tolerated — but that's a **deliberate choice to surface intent**, not a rebellion against DRY itself. ## Why This Works Especially Well in Salesforce The idea of Declarative Data Specification is generally useful for any environment that writes integration tests. But Salesforce has structural reasons to **especially need** this approach. ### Many Fields and Complex Relationships Salesforce's object model assumes: standard objects + custom objects + custom fields + multiple lookups + RecordTypes + required fields + validation rules. To create "one Opportunity correctly", you have to be aware of dozens of fields, and which of them are required / defaulted / validated varies from org to org. When written down as a procedure, this complexity flows into the test body itself. Consolidating shared settings into `Blueprints.cls` and combining `.template(...) + .set(...)` is also a mechanism to **push that complexity outside the test**. ### Admin Configuration Changes the Test's Premises In Salesforce, admins add fields, increase validation rules, and split RecordTypes — often without the developer's knowledge. To absorb these changes with a procedural factory, you have to **modify the conditional branches and value generation inside each method, one by one**. Given the premise of "a runtime environment where you don't know what changed when", procedural test data tends to fray badly. When written declaratively, shared settings are unified in `Blueprints.cls`, so the blast radius of changes is **structurally narrowed**. ### "Remembering Everything Is Impossible" Is the Reality In a complex business system, it's unrealistic for one developer to **keep all integration-test procedures in their head**. Whether you can re-read a factory method you wrote yourself half a year ago is genuinely doubtful. Declarative code has the property that **"reading the code itself becomes understanding the data structure"**. This translates into a long-term benefit: lower cognitive load when your future self or a teammate intervenes in that test. ## Where It Sits in Apex Stem [Apex Stem](https://krileworks.com/apex-stem) consists of four OSS, and ApexBlueprint plays the **integration test data generation (Test Data Factory)** role. The [test strategy](https://krileworks.com/apex-stem/docs/test-strategy) page covers this in detail, but Apex Stem structurally separates "Usecase unit tests" and "Handler integration tests", with different OSS for each: | Test Type | OSS in Charge | Data Generation | DML | |---|---|---|---| | Usecase unit test | ApexEloquent (`MockEloquent` / `MockEntry`) | In-memory mocks | None | | Handler integration test | ApexBlueprint (`SBlueprint` / `SOrchestrator`) | Real DML records in the org | Yes | Both address the same difficulty of "test data creation" — just in different contexts. The split: unit tests need exhaustive logic coverage (ApexEloquent's MockEntry); integration tests need "integration with real platform behavior" (ApexBlueprint's SOrchestrator). ApexBlueprint's Declarative Data Specification is a design intended to **preserve the declarative reading experience even in integration tests**. It plays a role in supporting Apex Stem's overall consistency: "no matter the type of test, the reading experience of the test code stays the same". ## Related Documents - [Relations, Bulk Generation, and Reference Patterns](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk): Applications of declarative form (withChildren / times / {Pn}) - [Dependency Resolution Internals: Topological Sort + Alias Resolution](https://krileworks.com/apex-stem/docs/apex-blueprint-dependency-resolution-deep-dive): A Deep Dive into what happens inside - [Test Strategy](https://krileworks.com/apex-stem/docs/test-strategy): The division of roles between ApexBlueprint and ApexEloquent - [Apex Stem Introduction Guide](https://krileworks.com/apex-stem/docs/apex-stem-full-guide): The entry point to the big picture - [Back to the ApexBlueprint Guide](https://krileworks.com/apex-stem/docs/apex-blueprint-guide) ============================================================================== Source: https://krileworks.com/document/apex-blueprint-dependency-resolution-deep-dive.md Page: https://krileworks.com/apex-stem/docs/apex-blueprint-dependency-resolution-deep-dive ============================================================================== # Dependency Resolution Internals: Topological Sort + Alias Resolution > **Who this article is for**: Developers curious about ApexBlueprint's **internal implementation**. We trace "what happens behind the simple API" in phase-by-phase pseudocode, with references to the actual implementation files (`SOrchestrator.cls` / `SBlueprintAnalyzer.cls` / `SBlueprintRealizer.cls`). For a sister article focused on the design rationale and philosophy, see [Declarative Data Specification: Why the Blueprint Form?](https://krileworks.com/apex-stem/docs/declarative-data-specification). ApexBlueprint's API is **just eight types of methods**: `of` / `set` / `template` / `alias` / `use` / `times` / `withChildren` / `parentIdField`. And yet, with these alone, you can handle all the common difficulties of integration test data — parent-child nesting, bulk generation, sibling references, hierarchical resolution of "the true parent for me", and disambiguating multiple lookups. This gap comes from the fact that **several independent problems are being solved at once** inside `SOrchestrator.create()`. This page digs into what happens behind the user's view, phase by phase. ## The User's View vs the Internal Processing From the user's perspective, `.create()` looks like a single operation: "**insert every blueprint I added into the database in the correct order**". But internally, four independent problems are being solved to make this happen: | Problem | Core of the Solution | |---|---| | 1. In what order to insert so that lookups resolve | Dependency analysis + topological sort | | 2. How to distinguish "child 1 under parent 1" from "child 1 under parent 2" | Alias resolution + auto-alias issuance | | 3. How to hierarchically connect records bulked via `.times(...)` | Per-parent iteration + automatic parent-Id copying | | 4. How to identify "the true parent for me" via `{P0}` / `{P1}` | Tracking the hierarchy stack + parent-reference resolution | These are normally separate headaches, but ApexBlueprint solves them **continuously, within the same pipeline**. ## The Phases of Internal Processing When `.create()` is called, the flow proceeds roughly as follows. ### Phase 1: Collecting the Blueprints The `SBlueprint`s passed via `SOrchestrator.start().add(...).add(...)` are held inside SOrchestrator's list **in their addition order, first of all**. No validation or execution happens at this stage. Child blueprints nested via `withChildren` are held as a **tree structure** inside the parent blueprint. Following the root blueprint lets you extract all children, grandchildren, and great-grandchildren below it. ### Phase 2: Building the Dependency Graph The moment `.create()` is called, SOrchestrator first **builds a dependency graph**. Each blueprint becomes a node, and the following relationships are registered as directed edges: - **The `withChildren` parent-child relationship**: an edge from parent → child (the child depends on the parent's Id) - **The `.use(alias, ...)` sibling reference**: an edge from the alias source blueprint → this blueprint (this one depends on the source's value) - **The `.after(alias)` ordering constraint**: the same alias source → this blueprint edge, except it **carries no value** and expresses ordering alone - **Parent references like `{P0}` / `{P1}`**: an edge from the corresponding ancestor → this blueprint (the corresponding ancestor is identified during construction) Note that detection of **duplicate aliases** and **references to non-existent aliases** happens later — they are **lazily detected** during Phase 5 (realize) based on the dependency information built here. From the user's perspective, these are still "failures at `.create()` time", but as an implementation detail, "graph construction" and "integrity validation" are kept separate. ### Phase 3: Topological Sort Once the dependency graph is complete, SOrchestrator runs a **topological sort**, rearranging blueprints so that the depended-upon side (parents) come first. - If a **cycle** is found in the dependencies, it fails with "`Circular or invalid reference detected`" - Once sorting succeeds, the downstream insertion processing is guaranteed to **not have to worry about insertion order** The reason you can write `.add(...)` calls in "the most readable order" is that the order gets reset here. ### Phase 4: Alias Resolution and Auto-Alias Issuance As the sorted blueprints are processed in order, each gets an alias assigned. - Aliases explicitly set via `.alias(...)` are **used as-is** (`{#}` placeholders are finalized after expansion) - Blueprints without `.alias(...)` get an **auto-generated alias** The auto-generated alias format is internally `__{SObjectName}_{globalCounter}_{#}__` (e.g. `__Account_0_1__` / `__Contact_1_2__`). The middle number is **a global counter assigned across all blueprints, not the hierarchy depth** (the `1` in `__Contact_1_1__` seen in test assertions is not the depth but the analysis ordering). The hierarchy is represented via the **parent prefix** explained next. Blueprints nested via `withChildren` get the parent's alias prefixed onto theirs, yielding compound aliases like `__Account_0_1____Contact_1_1__`. This gives "child 1 under parent 1" and "child 1 under parent 2" **distinct namespaces as separate records**. ### Phase 5: Hierarchical Realize and Parent-Id Copying Now the Realizer takes over. It walks the sorted blueprints in order and **realizes** each (= converts it into an SObject instance). When `.times(...)` is combined with nesting, realize behaves like this: ``` Loop the parent blueprint times(N) times For each parent instance: Realize the parent as an SObject Loop the child blueprint times(M) times For each child instance: Realize the child as an SObject Copy the parent's Id into the child's lookup field ※ The Id is still a placeholder at this point (pre-insert) Recurse if there are grandchildren ``` What's important here is the property that **a fresh, full set of children is regenerated per parent instance**. This is the source of the "Multiplication: upper-tier `times` propagates downward" behavior (see [Relations, Bulk Generation, and Reference Patterns](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk)). #### Where `{P0}` / `{P1}` Resolution Happens Resolution of `{Pn}` happens right inside this "realize children per parent" loop. The Realizer dynamically assembles a **parent-position map** (a `Map` named `parentPositionToAlias`) as it recurses, and a child blueprint's declaration like `.use('{P1}', 'LastName', 'Subject')` is resolved as "the alias of the blueprint currently occupying tier 1 (= the true parent for me) in the parent-position map at this moment". So `{Pn}` is **not a static alias string, but a reference to a dynamically constructed parent-position map**. As nesting deepens, more entries are added to the map, which is why the numbers `{P0}` / `{P1}` / `{P2}` can be specified consistently as **absolute depths from the root**. You could achieve something similar by embedding the parent's alias into the alias yourself (`{P0}_child_{#}`) — that's also implementation-supported. But the user has to assemble the alias string in their head on the `.use(...)` side, which is why `{Pn}` is the **more structurally writable** design. > The realize described here, combined with Phase 6 below, is executed **one layer at a time**. It is not "realize all blueprints first, then bulk-insert at the end" (details in Phase 6). ### Phase 6: Per-Layer Bulk DML Insert (Alternating Loop with Phase 5) Implementation-wise, Phase 5 (realize) and Phase 6 (DML insert) are **not run end-to-end as one shot. They alternate per "layer", as derived by Phase 3**. A "layer" here is **the set of blueprints at the same depth**, as determined by dependency analysis — the blueprint closest to the root is layer 0, its children are layer 1, and so on (`SOrchestrator.BuildLayers`). The pseudocode for `.create()` execution looks like this: ``` for each layer from 0 to maxLayer: // Phase 5: realize every blueprint in this layer // The upper layers are already inserted, so .use(parentAlias, 'Id', ...) resolves with the parent's real Id layerSObjects = realize all blueprints in this layer // Phase 6: bulk-insert this layer's SObjects dmlOperator.doInsert(layerSObjects) // Store this layer's results in the master map aliasToSObject // — referenced when realizing the next layer aliasToSObject.putAll(thisLayerResults) ``` The reason for synchronizing between layers is that **the real Id is only fixed after the parent layer has been inserted, and that's what the next layer's children need to obtain via `.use(parentAlias, 'Id', 'AccountId')`**. If you realized everything first, parent Ids would still be placeholders when copied into children, and post-insert parent-child relationships would be broken. It's this inter-layer synchronization that lets the user enjoy the experience: "just specify the parent Id with `.use(...)`, and the real Id from the actual DML insert ends up copied into the child". #### Where IDmlOperator Plugs In The `dmlOperator` at the end of each layer's `dmlOperator.doInsert(layerSObjects)` is the swap-out point for production / mock: - Production: `DmlOperator` (runs real `insert`) - Tests: `MockDmlOperator` (no real DML; just issues placeholder Ids) ApexBlueprint's own tests (`SOrchestratorTest`) call `SOrchestrator.start(new MockDmlOperator())` to swap here and **verify behavior without firing real DML**. #### Timing of Duplicate-Alias Detection In the implementation, duplicate aliases are detected at two moments: - **Within the same layer**: when populating `layerAliasToSObject` inside `realizeLayer` - **Across layers**: when populating the master map `aliasToSObject` after insert Either way, a `Duplicate alias detected` exception is thrown at runtime. This is the concrete locus of the "duplicate aliases are detected lazily" claim from Phase 2. ## Why All of This Can Be Solved at Once Looking back at ApexBlueprint's internal processing, the independent problems — "dependency graph", "topological sort", "alias resolution", "per-parent recursive realize", "parent-reference resolution", "bulk DML" — are solved continuously within the same pipeline. The reason these collapse, from the user's perspective, into a single operation ("just declare dependencies, and it works") is that ApexBlueprint **routes every operation through one abstraction: "the declaration of the data's final shape"**. The user writes only "what records I want to ultimately exist", and the framework takes care of "the mechanical procedure to get there". Two points stand out as marks of good design: 1. **The API surface fits into eleven methods, so the learning cost grows linearly.** It is not the kind of API where method counts grow exponentially with each new feature 2. **The solutions to each underlying problem (topological sort / auto-alias / hierarchy-stack resolution for `{Pn}`) are independent and individually replaceable.** This keeps room for future improvements to the internal implementation ApexBlueprint's character — "simple on the surface, powerful inside" — is built on this kind of stacked separation of solutions. ### In practice: the two v2.0.0 features added no new machinery Point 1 above reads like an abstraction, but the v2.0.0 additions are a concrete demonstration of it. **Neither `after` nor `sharedWith` added anything to dependency resolution.** | Added API | What it actually does inside | |---|---| | `.after(alias)` | Pushes a dependency with no `fromField` / `toField` onto the **same list `.use()` uses**. To the topological sort, one more edge appeared that happens to carry no value | | `.sharedWith(user, level)` | Assembles a `__Share` sibling blueprint and wires it back with `.use(ownAlias, 'Id', 'ParentId')`. **One ordinary child node appeared** | `sharedWith` lands exactly one layer after its parent not because sharing has bespoke ordering logic, but because **the `use()` edge makes the Phase 3 sort arrange it that way**. For the same reason it follows `times` bulk generation and `{Pn}` resolution automatically — no part of the bulk machinery was rewritten to accommodate sharing. As long as a new feature reduces to "what edge does Phase 2 draw?", Phase 3 onward never has to change. **That structure is why the API surface grows only linearly.** ## Related Documents - [Declarative Data Specification: Why the Blueprint Form?](https://krileworks.com/apex-stem/docs/declarative-data-specification): The sister Deep Dive on the philosophy behind why this design was chosen - [Relations, Bulk Generation, and Reference Patterns](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk): The user-facing surface of API patterns - [API Reference: SOrchestrator](https://krileworks.com/apex-stem/docs/apex-blueprint-api-sorchestrator): start / add / create / getByAlias and each exception - [API Reference: SBlueprint](https://krileworks.com/apex-stem/docs/apex-blueprint-api-sblueprint): The full API of the method chain - [Back to the ApexBlueprint Guide](https://krileworks.com/apex-stem/docs/apex-blueprint-guide) ============================================================================== Source: https://krileworks.com/document/apex-trace-lifecycle.md Page: https://krileworks.com/apex-stem/docs/apex-trace-lifecycle ============================================================================== # Trace's Four Lifecycle Methods The `Trace` class exposes four lifecycle methods that express the flow of a Usecase. ## Hold it in an instance field Create the `Trace` **exactly once**, as an instance field on the Usecase class. ```apex public with sharing class CopyAccountIndustryToOpportunityUsecase { private Trace t = Trace.of('Copy the parent account industry to the opportunity'); // ... } ``` Why once, and why an instance field, is covered in [Nested traces and the two modes](https://krileworks.com/apex-stem/docs/apex-trace-nesting-and-modes). For now it is enough to remember: **one `Trace` per Usecase**. ## Four methods, three exit paths | Method | Purpose | When to call it | |---|---|---| | `t.start()` | Processing begins | At the top of `invoke()` | | `t.log(msg)` | An intermediate note | Anywhere, any number of times | | `t.skip(msg)` | Exit by skipping | Right before an early return (nothing to process, etc.) | | `t.abort(msg)` | Exit abnormally | When bailing out on an exception or a business error | | `t.finish(msg)` | Exit normally | At the end of `invoke()` | `log` / `skip` / `abort` / `finish` all have a **no-argument overload**. When there is no message worth keeping — you just want to close the context — call `t.finish()`. That fits cases where only the path (skip / finish / abort) matters and no extra log line is needed. The important part is the **three exit paths**. - **`finish`**: the work completed normally - **`skip`**: nothing to do (no targets, conditions not met) and it exited normally - **`abort`**: it did not complete, because of an exception or a business error Keep these three distinct and `TraceFlow` can later assert **which path a Usecase took**. ## Example Here is `CopyAccountIndustryToOpportunityUsecase`, viewed through the lens of `Trace`. ```apex public with sharing class CopyAccountIndustryToOpportunityUsecase { @TestVisible static final String LBL_FETCH = 'oppFetch'; @TestVisible static final String LBL_UPDATE = 'oppUpdate'; private final Set opportunityIds; private final IEloquent eloquent; private Trace t = Trace.of('Copy the parent account industry to the opportunity'); // (constructors omitted — see step 3 of the introduction guide) public void invoke() { this.t.start(); if (this.opportunityIds == null || this.opportunityIds.isEmpty()) { this.t.skip('No target opportunities, exiting.'); return; } Scribe oppScribe = Scribe.of(Opportunity.class) .field('Id') .parentField(Scribe.asParent('AccountId').field('Industry')) .whereIn('Id', this.opportunityIds); List oppEntries = this.eloquent.label(LBL_FETCH).get(oppScribe); for (IEntry oppEntry : oppEntries) { IEntry accountEntry = oppEntry.getParent('AccountId'); oppEntry.put('Industry__c', accountEntry.get('Industry')); } this.eloquent.label(LBL_UPDATE).doUpdate(oppEntries); this.t.finish('Copied the industry onto ' + oppEntries.size() + ' opportunities.'); } } ``` This Usecase ends on two paths: `skip` when the target set is empty, `finish` once the work is done. The next chapter shows how to tell those two apart in a test. ## Related Documents - [Verifying execution paths with TraceFlow](https://krileworks.com/apex-stem/docs/apex-trace-flow-guide): pin the recorded path in a test - [Nested traces and the two modes](https://krileworks.com/apex-stem/docs/apex-trace-nesting-and-modes): the ordering rule when a Usecase calls a Usecase - [ApexTrace guide](https://krileworks.com/apex-stem/docs/apex-trace-guide): back to the guide index ============================================================================== Source: https://krileworks.com/document/apex-trace-flow-guide.md Page: https://krileworks.com/apex-stem/docs/apex-trace-flow-guide ============================================================================== # Verifying Execution Paths with TraceFlow `TraceFlow` is a static utility for inspecting the state of the trace that just ran. Use it in tests to tell **which path a Usecase exited on**. ## The main verification methods | Method | What it checks | |---|---| | `TraceFlow.isLastFinish()` | Whether the last trace ended on `finish()` | | `TraceFlow.isLastSkip()` | Whether the last trace ended on `skip()` | | `TraceFlow.isLastAbort()` | Whether the last trace ended on `abort()` | | `TraceFlow.lastHistoryContains(text)` | Whether **the last entry** contains the string | | `TraceFlow.contains(text)` | Whether **anywhere in the history** contains the string | | `TraceFlow.lastHistory()` | Get the last `TraceHistory` object | | `TraceFlow.usageOf(name)` | Governor usage for contexts with that name (exclusive total, v1.2.0+) | | `TraceFlow.lastUsage()` | Governor usage of the **single** most recently closed context (in a bulk IT, use `usageOf`) | #### `lastHistoryContains` vs `contains` `lastHistoryContains` looks at **the last entry only**, so adding one `t.log(...)` line later breaks the test. - **Pinning the reason message on `finish` / `skip` / `abort`** → `lastHistoryContains`. It is the entry that immediately follows, so the ordering is stable - **Pinning the content of an intermediate `t.log(...)`, or staying resilient to added logs** → `contains` ```apex Assert.isTrue(TraceFlow.contains('Copied the industry onto 3 opportunities')); // passes wherever it sits in the history ``` Both return `false` when given `null`. ## A void invoke() can still be verified by path When a Usecase returns `void`, tests tend to observe only side effects (the DML, the field values). Combine them with `TraceFlow` and you can pin the behaviour along **two axes: side effects and path**. That is one of the reasons ApexTrace ships as standard in Apex Stem. ## Example: telling skip and finish apart Write "skipped because there was nothing to do" and "completed normally" as separate tests. ```apex @isTest static void testInvoke_WhenOpportunityHasAccount_ThenIndustryCopied() { Trace t = Trace.of('Normal: the parent account industry is copied onto the opportunity'); t.start(); MockEntry oppEntry = MockEntry.of(Opportunity.class) .alias('opp').autoId(1) .setParent('AccountId', MockEntry.of(Account.class).set('Industry', 'Technology')); MockEloquent mock = (new MockEloquent()) .attach(CopyAccountIndustryToOpportunityUsecase.LBL_FETCH, new List{ oppEntry }); (new CopyAccountIndustryToOpportunityUsecase( new Set{ oppEntry.getAliasId('opp') }, mock )).invoke(); Assert.areEqual(1, mock.upsertedRecordsAt(CopyAccountIndustryToOpportunityUsecase.LBL_UPDATE).size()); Assert.isTrue(TraceFlow.isLastFinish()); t.finish(); } @isTest static void testInvoke_WhenNoOpportunityIds_ThenSkipped() { Trace t = Trace.of('Normal: it skips when there are no target opportunities'); t.start(); MockEloquent mock = new MockEloquent(); (new CopyAccountIndustryToOpportunityUsecase( new Set(), mock )).invoke(); Assert.isTrue(TraceFlow.isLastSkip()); t.finish(); } ``` Both tests observe the **path**, which is not a return value, through `TraceFlow`. Whether it went down `finish` or `skip` is verified on an axis independent of the side effects (`upsertedRecords`). You can pin the log content too. ```apex Assert.isTrue(TraceFlow.lastHistoryContains('No target opportunities')); ``` That constrains it all the way down to "did it skip with the message we expected". ## Aligning the observation window to the Act: discardArrange() (v1.4.0+) `isLastFinish()` / `isLastSkip()` / `isLastAbort()` look at **the most recently closed context**. But **the trace history accumulates from the top of the test method**, so if the DML that created your Arrange records fired a Usecase through a trigger, that context is in the history too. The problem shows up **when the Act produces nothing**. With no context pushed during the Act, `isLastSkip()` ends up reading an Arrange context. You can be **verifying the Arrange while believing you are verifying the Act**. Put `TraceFlow.discardArrange()` on the Arrange / Act boundary and the history is cut there, so every assertion afterwards sees the Act alone. ```apex setupAccountWithOpportunities(); // Arrange (a Usecase runs through the trigger) TraceFlow.discardArrange(); // here Test.startTest(); // and here are the same boundary new ResummarizeUsecase(ids).invoke(); Test.stopTest(); Assert.isTrue(TraceFlow.isLastSkip()); // looks at the Act's path only ``` It is the same judgement call as placing `Test.startTest()`, so there is no new concept to learn — and it goes right next to it. > ⚠️ **You cannot call it while a Usecase context is open.** At the boundary, at most the test's own `Trace` is open, so this never bites; open more than that and it raises `TraceException`. ## Related Documents - [Trace's four lifecycle methods](https://krileworks.com/apex-stem/docs/apex-trace-lifecycle): the four methods and the three exit paths - [Pinning governor usage](https://krileworks.com/apex-stem/docs/apex-trace-governor-it): the bulk insurance TraceUsage buys you - [ApexTrace guide](https://krileworks.com/apex-stem/docs/apex-trace-guide): back to the guide index ============================================================================== Source: https://krileworks.com/document/apex-trace-governor-it.md Page: https://krileworks.com/apex-stem/docs/apex-trace-governor-it ============================================================================== # TraceUsage: Governor Usage per Context Every Trace context **records its governor usage automatically**, from `start()` through to its close (`finish` / `skip` / `abort`). Pull it out by name with `TraceFlow.usageOf(name)` (v1.2.0+). ## What the tool is for Get the design intent straight first. `TraceUsage` is **not** a tool for deciding how many SOQL queries a Usecase ought to use. It is **insurance**. It pins one thing and one thing only: that running in bulk has not introduced **an implementation that fires a query per record (N+1)**, and it pins it with a loose ceiling. > 📌 **The measurements on this page come from a single org holding roughly 210,000 opportunities.** > Invocation counts in particular **depend on how many times your cascade re-enters, so your numbers will differ**. Without re-entry, the "30 records → 2 invocations" below would be "30 records → 1". Treat the figures as a sense of magnitude, not as thresholds to copy. ## Three tools, three jobs What you are measuring decides which tool you reach for. | What you want to measure | Tool | |---|---| | The whole **synchronous** trigger cascade | `Limits`, **captured into a variable inside the block** | | What **one specific Usecase** caused | **`TraceFlow.usageOf(name)`** (v1.2.0+) | | **Asynchronous** work (batches / Queueables that run at `stopTest()`) | **`TraceUsage`, and nothing else** | The third row is where `TraceUsage` stands alone. **A batch only starts running at `Test.stopTest()`**, so `Limits` cannot reach it in principle. It is the only way to pin the governor usage of asynchronous work. > 🚨 "Inside the block" in row one is a hard requirement. `Test.stopTest()` restores the governor counters to their pre-`startTest()` state, so **reading `Limits.getQueries()` afterwards returns the Arrange figure, not the Act one**, and the assertion passes no matter what (details and measurements in the [test strategy](https://krileworks.com/apex-stem/docs/test-strategy)). ## The minimal form Two lines is enough to start. The hard parts are "where to put it" and "what ceiling to pick" — the syntax is not one of them. ```apex // After running the Usecase, pull its usage out by name TraceFlow.usageOf('Resummarize the account opportunity rollup') .assertInvocationsAtMost(2); ``` The name you pass to `usageOf` is **verbatim** the string in the Usecase's `Trace.of(...)`. ```apex private Trace t = Trace.of('Resummarize the account opportunity rollup'); // this string ``` Start with **invocation counts only**. A count is the design intent itself — "this Usecase runs once per save" — so **you can decide it before running anything**. Consumption ceilings need a measurement first, so leave them for later. ## The full bulk IT The real home for this is an integration test that mass-produces production-sized data with [ApexBlueprint](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk)'s `times()` and **fires the cascade through a trigger**. Unit tests (`MockEloquent`) issue no real SOQL, so they are **structurally blind to query counts**; only one test that runs real DML at production size can close that gap. ```apex @isTest static void testInsert_WhenBulk_ThenWithinGovernorLimits() { Trace t = Trace.of('Edge case: bulk-inserting opportunities leaves governor headroom'); t.start(); // ---- Arrange: declare production size (1 account + 201 opportunities) ---- SOrchestrator o = SOrchestrator.start() .add(SBlueprint.of(Account.class) .template(Blueprints.accBasic()) .withChildren( SBlueprint.of(Opportunity.class) .template(Blueprints.oppBasic()) .times(201))); // 201 or more (200 does not cross the chunk boundary) // ---- Act: the real DML in create() fires before/afterInsert in bulk ---- TraceFlow.discardArrange(); // always place it on the boundary Test.startTest(); o.create(); Integer soqlUsed = Limits.getQueries(); // capture INSIDE the block Test.stopTest(); // ---- Assert ---- // (1) Correctness. Pin a number that cannot hold if a record is dropped. This is the point Assert.areEqual(201, [SELECT COUNT() FROM Opportunity], 'all 201 records were processed'); // (2) Consumption of the Usecase you care about TraceFlow.usageOf('Resummarize the account opportunity rollup') .assertInvocationsAtMost(6, 'measured 5 on a 201-record insert; more suggests another entry point got wired in') .assertSoqlQueriesAtMost(15, 'measured 2; growth proportional to record count means a query inside a loop'); // (3) Transaction-wide governor headroom Assert.isTrue(soqlUsed < Limits.getLimitQueries() / 2, 'SOQL stays under half the limit even in bulk. Measured ' + soqlUsed); t.finish(); } ``` > ⚠️ In this example the Arrange (assembling the `SOrchestrator`) issues no DML, so `discardArrange()` has no practical effect. **Place it anyway.** Readers copy this shape into their own tests, where the Arrange *does* run DML. The point is to show "always on the boundary" as a form. **The act here is `o.create()`.** ApexBlueprint realises the declaration through real DML, so **generating the data doubles as the trigger** (when you want to observe an update / delete cascade instead, `create()` is Arrange and belongs outside `startTest()`). `times(30)` is not enough. **Salesforce invokes triggers in chunks of 200**, so a test that stops at 200 lets through implementations that assume "every record arrives in one call" (the reasoning and measurements are in [test strategy > why 201 records](https://krileworks.com/apex-stem/docs/test-strategy)). ### The three tiers each protect something different | # | Assertion | What it insures against | When it breaks | |---|---|---|---| | (1) | `Assert.areEqual(201, ...)` | **Not dropping records.** This is the point | `take(200)` / `LIMIT` / an "everything arrives in one call" assumption got in | | (2)-1 | `assertInvocationsAtMost` | **Wiring.** How many times this Usecase may fire | Another trigger started calling it / re-entry increased | | (2)-2 | `assertSoqlQueriesAtMost` | **The body.** Queries must not scale with record count | A query inside a loop (N+1) got in | | (3) | `Assert.isTrue(soqlUsed < ...)` | **Headroom across the cascade** | The transaction grew, including places that are not yours | **Do not skip (1).** An implementation with `take(200)` in it actually issues **fewer** queries, so (2) and (3) both stay green. Watch only the governors and you miss "it dropped records but consumed little". ## Choosing the ceiling ### Never use the exact measurement This is the most common mistake in practice. **Writing `assertSoqlQueriesAtMost(2)` against a measured 2 is effectively a claim of "exactly 2"**, and a legitimate refactor that adds one query breaks it. Put the ceiling where **an implementation that queries per record fails for certain (it would scale with the record count), while a legitimate addition of one or two queries does not**. It is the same thinking as the classic governor IT that checks `Limits.getQueries() < half the limit` across the whole transaction. **The exception is when "issues nothing at all" is itself the point.** For "does nothing when there is no delta", `AtMost(0)` is a fair constraint. Deliberately degrading an implementation to query per record made it fire **before it reached the platform ceiling of 100**. ``` Usage assertion failed: SOQL queries is 32, exceeding the allowed maximum of 10. Actual usage: Invocations: 2, SOQL: 32 (rows: 92), DML: 1 (rows: 60), Callouts: 0 ``` That message alone tells you **invocations is 2 while SOQL is 32** — so the cause is not extra wiring but **a loop in the body**. ### Put the measurement and the suspicion into `reason` All six asserts take a `(Integer max, String reason)` overload (v1.3.0+). The reason prints **before** the numbers in the failure message. ``` Usage assertion failed: DML statements is 1, exceeding the allowed maximum of 0. Reason: measured 1 statement (20 rows) even at 20 opportunities; growth with record count means a DML inside a loop Actual usage: SOQL: 0 (rows: 0), DML: 1 (rows: 20), Callouts: 0 ``` **What this guard protects against is changes to code you did not write.** An admin adds a Flow; another team adds a trigger. The cascade changes without anyone touching Apex. The problem is that **the window before someone deletes the red is short**. Numbers alone read as "the ceiling is too strict", and the threshold gets raised or the line gets removed. A source comment has to be opened to be read; **a failure message always gets read**. Write two things: the **measured value** (what it is today — a bare ceiling reads as an over-strict setting) and **what to suspect if it grows** (where the person looking at the red should go next). > 💡 **As a side effect, it forces you to measure at the time of writing.** A bare ceiling can be set to `5` and forgotten; putting a number in the reason means you have to measure. (When this was adopted, a comment claiming "measured 2 queries" turned out to be stale — it was actually 0.) ### The invocation ceiling is meaningless without the record count Consumption wobbles with legitimate change, but **an invocation count is the design intent**, so it holds still. > ⚠️ The invocation series quoted on this page and in the [test strategy](https://krileworks.com/apex-stem/docs/test-strategy) (30 → 2 / 200 → 3 / 201 → 5) is what you get **without** `discardArrange()` — it includes the Arrange-time invocations. With it, the Arrange share drops out (measured at 201 records: **5 → 4**). That said, **counts alone depend on record volume**. Trigger chunking changes the number of invocations, so **keeping a ceiling written for 30 records and raising the volume to 201 fails with no bug present** (consumption ceilings are roughly flat against volume, so they do not have this problem). Writing the record count into `reason` prevents it. ## The history accumulates from the Arrange — cut it with discardArrange() **The trace history accumulates from the top of the test method.** Create records in the Arrange and any Usecase that ran through a trigger on that DML stays in the history, folded into the `usageOf` total. Put `TraceFlow.discardArrange()` (v1.4.0+) on the Arrange / Act boundary and every assertion afterwards sees the Act alone. ```apex setupAccountWithOpportunities(); // Arrange. The target Usecase runs once via the trigger TraceFlow.discardArrange(); // here Test.startTest(); // and here are the same boundary Database.executeBatch(new ResummarizeBatch()); Test.stopTest(); TraceFlow.usageOf('Resummarize the account opportunity rollup') // the Act only; the count is right too .assertInvocationsAtMost(1, 'one batch chunk') .assertSoqlQueriesAtMost(0, 'no queries when there is no delta'); ``` It is the same judgement call as placing `Test.startTest()`, so **there is no new concept to learn**. It even goes right next to it. > 📌 **It matters for path assertions too, not just consumption.** `isLastSkip()` and friends look at the most recently closed context, so when the Act produced none there was room to read an Arrange context instead. `discardArrange()` aligns the observation window to the Act. ⚠️ **If you forget it, the library cannot say anything** (it has no way to detect whether an Arrange exists). But **`assertInvocationsAtMost` makes a natural watchdog**: one Arrange invocation shows up as one extra count, so a test that pins counts catches the omission. The advice to "start with counts" pays off here as well. ### Nesting: inclusive and exclusive (most readers can skip this) There are three ways to pull usage out. | | One invocation | Total of all | |---|---|---| | **inclusive** (children included) | `lastUsage()` | (does not exist) | | **exclusive** (self only) | an element of `usagesOf(name)` | `usageOf(name)` | The empty cell is **the reason exclusive has to exist**. Picture a handler calling two Usecases. ``` Handler (SOQL 6) ├ UsecaseA (SOQL 2) └ UsecaseB (SOQL 4) ``` Add those up as inclusive values and you get **6 + 2 + 4 = 12** — double, when only 6 queries were actually issued. That is why **`usageOf`, which produces a total, has to be exclusive** (the handler's own exclusive comes out as 0). > 📌 **If you only put `Trace` on Usecases, inclusive and exclusive are the same number.** > They diverge only when you also put a `Trace` outside the nesting, on a handler for instance. ⚠️ **inclusive / exclusive does not apply to counts.** Exclusive is the mechanism that splits *consumption* between parent and child; `getInvocations()` counts **how many times that context closed**. It is not something being split. ### When to reach for usagesOf With `discardArrange()` in place, ordinary assertions are fine on `usageOf`. You need `usagesOf(name)` only when **the same Usecase runs several times within the Act and you want the individual breakdown** — inspecting the consumption of each batch chunk, say. 🚨 **Do not use it as "take the last element to get the Act's single run".** That is only correct when the Act fired that Usecase **exactly once**, and the moment the work splits into two chunks you are measuring the last chunk alone. **To measure the whole Act, use `discardArrange()` + `usageOf`.** ## Misuse guards The places where "using it wrong stays green" have been closed off. All of these apply **under test execution (Strict mode) only** and are skipped in production. ### An ambiguous lastUsage() throws `lastUsage()` returns only the single most recently closed context. In a bulk IT where the handler calls several Usecases, **which one you measured was decided by accident**. From v1.3.0, two or more contexts closing at the same depth raises an exception. ``` TraceException: lastUsage() is ambiguous — 3 contexts closed at the same level: Resummarize the account opportunity rollup / Recalculate the opportunity total / Resummarize the account opportunity rollup Use TraceFlow.usageOf(contextName) to target one. ``` **The candidate list is the diagnosis.** The same name appearing twice means the Arrange DML also ran that Usecase through a trigger — in other words, **adding a single DML to the Arrange used to change what you were measuring**. Nesting on its own is not ambiguous (LIFO makes the outermost the natural target). ### A consumption assert on zero invocations throws `assertSoqlQueriesAtMost(n)` claims "this context ran, and stayed under n". When `invocations = 0` — a mistyped name, say — that claim has no basis, so it throws. ``` TraceException: Usage assertion on SOQL queries is unfounded: no context with this name closed in the transaction (invocations = 0). Check the context name for typos, the test layer, and the wiring. To assert that the context does not run, use assertInvocationsAtMost(0). ``` Because `usageOf` takes the context name as a **string**, renaming a `Trace.of(...)` used to fail silently. Now it goes red. **It doubles as a safety net for renames.** `assertInvocationsAtMost(0)` is exempt on purpose: "it does not run" is a legitimate claim. ### discardArrange() can only be called on the boundary Call it while a Usecase context is open and that Start entry disappears, silently dropping one element from `usagesOf`. On the boundary, at most **the test's own `Trace`** is open, so anything beyond that raises (v1.4.0+). ``` TraceException: discardArrange() must be called at the Arrange / Act boundary, while no usecase context is open. Currently open: 2 contexts. ``` ## Reference ### Only five deterministic metrics are recorded | Metric | Getter | |---|---| | SOQL queries | `getSoqlQueries()` | | SOQL rows | `getSoqlRows()` | | DML statements | `getDmlStatements()` | | DML rows | `getDmlRows()` | | Callouts | `getCallouts()` | CPU time and heap are **deliberately excluded**: they vary run to run, and asserting a threshold against them makes tests flaky. Reach for the getters when you want the raw value (counts come from `getInvocations()`). ```apex TraceUsage usage = TraceFlow.usageOf('Copy the parent account industry to the opportunity'); Integer soql = usage.getSoqlQueries(); ``` ### Behaviour worth knowing - **There is an inclusive value and an exclusive value.** The raw value recorded in the history is **inclusive** (the total between `start` and close, so it includes nested children), but **`usageOf` / `usagesOf` return exclusive** (that minus the direct children's share — this context's own consumption). `lastUsage()` and the `Usage:` line in the production debug log stay inclusive - **A failed assert is a catchable `TraceException`** (`Assert.fail`'s `AssertException` cannot be caught, which would make the helper itself untestable). The message carries the full usage breakdown - **It shows up in the production debug log too**: `Usage: SOQL: 3 (rows: 120), ...` follows `FINISH: ...`, so the cost per Usecase is visible outside tests as well > ⚠️ **Under unit tests (`MockEloquent`) every usage value is zero.** No real SOQL is issued, so of course it is. **Put governor assertions on the integration side, where real DML runs.** On the unit side they verify nothing. See "make that one representative test a governor IT" in the [test strategy](https://krileworks.com/apex-stem/docs/test-strategy). ## Related Documents - [Relations, bulk generation, and reference patterns](https://krileworks.com/apex-stem/docs/apex-blueprint-relations-and-bulk): mass-producing production size with `times()` - [Test strategy](https://krileworks.com/apex-stem/docs/test-strategy): why the governor IT is one of the representative tests - [ApexTrace guide](https://krileworks.com/apex-stem/docs/apex-trace-guide): back to the guide index ============================================================================== Source: https://krileworks.com/document/apex-trace-nesting-and-modes.md Page: https://krileworks.com/apex-stem/docs/apex-trace-nesting-and-modes ============================================================================== # Nested Traces and the Two Modes When one Usecase calls another, the `Trace` **nests**. ApexTrace manages that nesting as a strict LIFO stack and throws on any inconsistency. ## The Outer / Inner ordering rule The rule for nested traces is: **close the Inner before you close the Outer**. ```apex Trace outer = Trace.of('Outer'); outer.start(); Trace inner = Trace.of('Inner'); inner.start(); inner.log('inside inner'); // ✅ inner.finish(); // ✅ Inner closes first outer.finish(); // ✅ ``` Call the Outer's `log` / `finish` after the Inner has started but before it closes with `finish` / `skip` / `abort`, and Strict mode throws immediately. ## Strict mode and Relaxed mode `TraceFlow` has two modes, switched automatically by execution context. | Mode | Default switch | Behaviour | |---|---|---| | **Strict** | Under test (`Test.isRunningTest() == true`) | Throws immediately on nesting or ordering violations. Catches bugs early | | **Relaxed** | In production | Absorbs minor inconsistencies by **aborting automatically**. Production is less likely to fall over | It is tempting to assume "green under test means fine in production" — keep in mind that behaviour differs by mode. > ⚠️ **The switch is driven solely by `Test.isRunningTest()`.** There is no API for switching it explicitly (`changeModeTo` and `TraceMode` are both `private`). ## Trace.of() belongs in the instance field initialiser, once Call `Trace.of(...)` **exactly once**, when the Usecase class's instance field is initialised. Re-create it inside `invoke()` and you get a fresh `Trace` on every call, which breaks the nesting and aggregation logic. ```apex public with sharing class YourUsecase { private Trace t = Trace.of('The process name'); // ✅ once, in the instance field public void invoke() { // private Trace t = Trace.of(...); // ❌ never re-create it inside invoke this.t.start(); // ... } } ``` ## Why tests open with Trace.of('...').start() Every test example so far starts with these two lines. ```apex Trace t = Trace.of('Normal: ...'); t.start(); ``` and calls `t.finish();` at the end. There is a **primary reason** and a **side benefit**. **Primary reason: to catch a broken Trace lifecycle inside the Usecase.** If the Usecase `return`s without calling `finish` / `skip` / `abort`, an unbalanced "still open" trace is left on the context. Wrapping the test in an outer Trace lets Strict mode (the default under test) **surface that mismatch at test time**, so you find the missing exit before it reaches production. **Side benefit: it resets the previous trace state.** Any context left behind by the previous test is explicitly unwound at the top of the new one. ### The shape of a whole test class **Every test method gets it, at the top and at the bottom.** Across a class it looks like this. ```apex @isTest(seeAllData=false) private class ResummarizeUsecase_T { @isTest static void testInvoke_WhenOpportunitiesExist_ThenSummaryUpdated() { Trace t = Trace.of('Normal: the account summary is updated when opportunities exist'); t.start(); // Arrange ... // Act ... // Assert ... Assert.isTrue(TraceFlow.isLastFinish()); t.finish(); } @isTest static void testInvoke_WhenNoOpportunities_ThenSkipped() { Trace t = Trace.of('Normal: it skips when there is not a single opportunity'); t.start(); // Arrange ... // Act ... // Assert Assert.isTrue(TraceFlow.isLastSkip()); t.finish(); } } ``` The point is that `t.start()` and `t.finish()` **bracket the method**. Once the shape is in place, the moment an inner Usecase forgets its exit, Strict mode catches it. ### A second benefit: describing the test outside the naming convention Test method names are bound to a shape such as `test{Method}_When{condition}_Then{result}`. That is a correct constraint for an identifier you want to grep — but **inside an identifier, both the length and the vocabulary available to you are limited**. The argument to `Trace.of(...)` is **just a string**, so it sits outside that constraint. | | Role | Constraint | |---|---|---| | Method name | Machine-readable. The identifier you grep and see in results | Naming convention, identifier-legal characters | | `Trace.of` argument | Human-readable. What the test actually verifies | **None** | **For teams outside the English-speaking world this translates directly into readability.** The method name stays in English per the convention, while **the description is written in your own language**. Reading "what is being verified" in your first language is measurably faster in review and in incident triage — and it costs nothing in terms of the naming convention. The string is also **written to the debug log**, so when a test fails the description is right there in the log. > 📌 The Apex Stem convention starts this string with a **category label**: `Normal:` / `Abnormal:` / `Edge case:`. Put the label first, then the human-readable description. (Japanese projects use `正常系:` / `異常系:` / `エッジケース:` — the point is the label, not the language.) ## Related Documents - [Trace's four lifecycle methods](https://krileworks.com/apex-stem/docs/apex-trace-lifecycle): the four methods and the three exit paths - [Verifying execution paths with TraceFlow](https://krileworks.com/apex-stem/docs/apex-trace-flow-guide): the inconsistencies Strict mode surfaces - [ApexTrace guide](https://krileworks.com/apex-stem/docs/apex-trace-guide): back to the guide index ============================================================================== Source: https://krileworks.com/document/apex-tools-trigger-handler.md Page: https://krileworks.com/apex-stem/docs/apex-tools-trigger-handler ============================================================================== # TriggerHandler Base Class and Field-Change Detection This document explains the `TriggerHandler` base class that ApexTools provides. For an index that includes the other tools, see the [ApexTools Guide](https://krileworks.com/apex-stem/docs/apex-tools-guide). ## What You Can Do Simply by extending the `TriggerHandler` base class, the entry-point processing from Triggers settles into a clean shape: "a trigger file that only declares the seven events" plus "a handler class that only writes the hooks you override". A built-in helper for "narrowing down to records where specific fields changed" lets `afterUpdate` filtering be written in a single line. ## Inheritance: seven hooks plus andFinally Each Handler extends the `TriggerHandler` base class and overrides only the hooks it needs. They are all `protected virtual`, so any hook you don't override does nothing. There are eight in total: seven matching the trigger events, plus `andFinally`, which runs last regardless of context. | Hook | Signature | |---|---| | `beforeInsert` | `(List newRecords)` | | `beforeUpdate` | `(Map newMap, Map oldMap)` | | `beforeDelete` | `(Map deletedMap)` | | `afterInsert` | `(Map newMap)` | | `afterUpdate` | `(Map newMap, Map oldMap)` | | `afterDelete` | `(Map deletedMap)` | | `afterUndelete` | `(Map undeletedMap)` | | `andFinally` | `()` — always called last (in any context) | ## Integrated With the Fixed Trigger.cls Pattern The trigger file follows Salesforce convention and uses a fixed form. Declare all seven events and call the Handler in one line. ```apex trigger Opportunity on Opportunity( before insert, before update, before delete, after insert, after update, after delete, after undelete ) { (new TriggerOppHandler()).execute(); } ``` > 🚨 **On a custom object you cannot put `__c` straight into the trigger name.** Apex identifiers may not contain a double underscore (Salesforce reserves it), so `trigger SalesActivity__c on SalesActivity__c(...)` fails to deploy with `Invalid character in identifier`. **Give the trigger a different name** (`SalesActivityTrigger`, for instance — match the file name too). Standard objects are fine as `trigger Opportunity on Opportunity(...)`. The Handler side overrides only the hooks it needs. ```apex public with sharing class TriggerOppHandler extends TriggerHandler { protected override void afterInsert(Map newRecordsMap) { Set opportunityIds = newRecordsMap.keySet(); (new CopyAccountIndustryToOpportunityUsecase(opportunityIds)).invoke(); } } ``` ## Field-Change Detection Helpers It's a frequent case to want to narrow `afterUpdate` to "records where specific fields changed". The `TriggerHandler` base class provides helpers for exactly that. | Method | Return Type | Purpose | |---|---|---| | `getUpdatedRecordsWithChangedField(SObjectField field)` | `List` | Records where a single field changed | | `getUpdatedRecordsWithChangedFields(List fields)` | `List` | Records where any of multiple fields changed | | `getUpdateRecordIdsWithChangedField(SObjectField field)` | `Set` | Id version of the above | | `getUpdateRecordIdsWithChangedFields(List fields)` | `Set` | Id version of the above | Example usage: ```apex public with sharing class TriggerOppHandler extends TriggerHandler { protected override void afterUpdate(Map newMap, Map oldMap) { Set needIds = this.getUpdateRecordIdsWithChangedFields(new List{ Opportunity.AccountId, Opportunity.StageName }); (new RegenerateCollectionUsecase(needIds)).invoke(); } } ``` The logic "do something only when a specific field changed" consolidates into a single line, and the Handler still keeps its slim "only condition checks and Usecase invocation" shape. ## Other Notes ### Hooks You Don't Override Just Do Nothing The hooks on the `TriggerHandler` base class are all `protected virtual` and do nothing by default. A Handler that only wants to handle `afterInsert` overrides only `afterInsert` — no need to fill other hooks with empty methods. ### When to Use `andFinally` `andFinally()` is a hook that is **always called last in any context**. Use it for "processing that must always run last, regardless of the before / after type" (finalizing audit logs, wrapping up Trace, etc.). Most Handlers don't need it. ## Read Next - [ApexTools Guide](https://krileworks.com/apex-stem/docs/apex-tools-guide): the entry point that includes the other tools in ApexTools - [Handler-Usecase Architecture](https://krileworks.com/apex-stem/docs/handler-usecase-architecture): the core Apex Stem architecture where `TriggerHandler` shines - [Apex Stem Introduction Guide](https://krileworks.com/apex-stem/docs/apex-stem-full-guide): four steps with working code ============================================================================== Source: https://krileworks.com/document/apex-tools-http-request-handler.md Page: https://krileworks.com/apex-stem/docs/apex-tools-http-request-handler ============================================================================== # IHttpRequestHandler: What Makes HttpCalloutMock Awkward, and ApexTools' Answer When you test an external integration in Salesforce, you swap the response through the standard `HttpCalloutMock`. ApexTools' `IHttpRequestHandler` makes the callout **replaceable through DI, and lets you assemble responses by declaration alone**. > 📌 **The platform does ship built-in mocks.** With `StaticResourceCalloutMock` / `MultiStaticResourceCalloutMock` you can return a response without writing an implementation class. But **the response body has to exist as a static resource (metadata)**, and it is one response per endpoint, so **an ordered sequence of responses (retries, pagination) cannot be expressed**. The moment you need that, you are back to implementing `HttpCalloutMock`. ## What it gives you - Inject `HttpRequestHandler` (a thin wrapper over the standard `Http`) in production and `MockHttpRequestHandler` in tests - Declare a response in one line: `MockResponse.of('GET').respond(body, 200)` - **No `HttpCalloutMock` implementation class, even for ordered responses** - Inspect the requests you sent, after the fact (Spy) ## Three things that make HttpCalloutMock awkward These are what you hit once you end up implementing `HttpCalloutMock` yourself. ### 1: Hand-writing JSON strings The response body ends up as a string literal, costing you both escaping and readability. (You can push it into a static resource, but then reading the test means opening another file.) ```apex // ❌ falls apart as the structure deepens String body = '{"records":[{"Id":"001xx","Name":"Acme","Contacts":{"totalSize":2}}]}'; ``` ### 2: if-else branching on URL strings One mock class carries every endpoint, so the inside of `respond()` swells with URL checks. ```apex // ❌ the branching grows with every new endpoint public HttpResponse respond(HttpRequest req) { if (req.getEndpoint().contains('/accounts')) { ... } else if (req.getEndpoint().contains('/contacts')) { ... } ... } ``` ### 3: Retries and pagination are hard to reproduce Expressing **an ordered set of responses** — "500 the first time, 500 again, then 200" — means giving the mock a counter of its own. ## ApexTools' answer ### Declare responses with MockResponse ```apex MockResponse.of('GET').respond('{"message":"not found"}', 404) // String MockResponse.of('post').respond(new Map{ ... }, 200) // Map / List get JSON-encoded. Method name is case-insensitive MockResponse.of('GET').respond(imageBytes, 200).header('Content-Type', 'image/jpeg') // Blob + header MockResponse.of('GET').respond(ok, 200).repeat() // last in the queue: every call from here on ``` `respond` accepts `String` / `Map` / `List` / `Blob`. Maps and lists are JSON-encoded automatically, so **awkwardness 1 dissolves into "write it as an Apex collection"**. > ⚠️ **The method name is not a routing key. It is the contract at delivery time.** If the next response in the queue declares a method that does not match the actual request, it fails immediately with an error carrying the expectation, the actual, and the queue state. A different response is never handed over silently. ## Two modes **🎯 Rule of thumb: if the test's claim includes ordering, use script mode; if it does not, use label mode. When in doubt, label.** ### Script mode (no labels): when ordering is the specification The list you pass to the constructor is the script of the flow. Read it top to bottom and it is the sequence of callouts you expect. ```apex MockHttpRequestHandler mock = new MockHttpRequestHandler(new List{ MockResponse.of('GET').respond(notFound, 404), // move 1: does it exist? MockResponse.of('POST').respond(created, 201), // move 2: create MockResponse.of('GET').respond(found, 200) // move 3: fetch again }); ``` Deviating from the order or the method produces a detailed error. **Awkwardness 3 becomes "list the same method a few times"**. ```apex // 500, 500, then success. No counter in the mock new List{ MockResponse.of('POST').respond(err, 500), MockResponse.of('POST').respond(err, 500), MockResponse.of('POST').respond(ok, 200) } ``` ### Label mode: when you do not want to be tied to cross-site ordering Give each callout site its own named queue (the same feel as `MockEloquent`'s `attach` / `label`). **Awkwardness 2 dissolves because you sort by the name of the call site, not by inspecting the URL.** ```apex // In the Usecase: this.http.label(LBL_EXISTS).send(req); MockHttpRequestHandler mock = new MockHttpRequestHandler() .attach(LBL_EXISTS, MockResponse.of('GET').respond(notFound, 404)) .attach(LBL_CREATE, MockResponse.of('POST').respond(created, 201)) .attach(LBL_UPDATE, MockResponse.of('PUT').respond(updated, 200)); // declaring an untaken branch is fine new KintoneUpsertUsecase(input, mock).invoke(); Assert.areEqual(1, mock.sentRequestsAt(LBL_CREATE).size()); // the create branch was taken Assert.areEqual(0, mock.sentRequestsAt(LBL_UPDATE).size()); // update was never consumed ``` For a branching flow the standard move is to **attach both branches and assert which one got consumed**. - Once you use `attach`, every `send` must be preceded by `label()` (consumed once per send) - A typo'd label fails with the list of registered labels attached - Attaching to the same label again appends to its queue (that site's retry sequence) - **Without `attach`, `label()` calls are ignored.** Labelled production code can still be tested against a plain script mock ## Verification helpers (Spy) | Method | Purpose | |---|---| | `sentRequestsAt(label)` | The requests sent under that label | | `countByMethod('POST')` | How many were sent per HTTP method | | `requestsTo(endpointPart)` | Filter by a substring of the endpoint | | `lastRequest()` | The last request sent | | `describe()` | The current queue state (for debugging) | ## 🛡 The Content-Type guard (a habit born from a real incident) Even on HTTP 200, an unexpected Content-Type usually means **you hit the wrong endpoint**. > A real case: the code meant to call `/bizCards/{id}/image` but was calling `/bizCards/{id}`, base64-encoding the returned JSON and pushing a corrupted image to the UI. Always guard when fetching binary. ```apex this.http.label(LBL_CARD_IMAGE).send(req); String contentType = this.http.getHeader('Content-Type'); if (this.http.getStatusCode() == 200 && (contentType == null || !contentType.startsWith('image/'))) { throw new CalloutException('Expected an image response but got Content-Type=' + contentType); } Blob image = this.http.getBodyAsBlob(); ``` ## Integrating with Apex Stem: injecting into a Usecase `IHttpRequestHandler` slots straight into the Apex Stem Usecase layer and the [Layered Constructor Pattern](https://krileworks.com/apex-stem/docs/layered-constructor-pattern). **From v1.0.0 the recommendation is to multiplex a single handler with labels** (the same idea as `IEloquent`'s `label`). ```apex public with sharing class KintoneUpsertUsecase { @TestVisible static final String LBL_EXISTS = 'kintoneExists'; @TestVisible static final String LBL_CREATE = 'kintoneCreate'; private final Input input; private final IHttpRequestHandler http; private Trace t = Trace.of('Upsert a record into kintone'); // 🚪 production public KintoneUpsertUsecase(Input input) { this(input, null); } // 🧪 test (DI) @TestVisible private KintoneUpsertUsecase(Input input, IHttpRequestHandler http) { this.input = input; this.http = http ?? new HttpRequestHandler(); } public void invoke() { this.t.start(); this.http.label(LBL_EXISTS).send(existsReq); // ... } } ``` Injecting several handlers by role still works, but **multiplexing by label keeps the constructor from bloating**. Inject `MockEloquent` (ApexEloquent) and `MockHttpRequestHandler` (ApexTools) independently and you can **verify side effects (DML) and outbound calls (HTTP) on separate axes**. ## ⚠️ Breaking changes in v1.0.0 Upgrading from a pre-tag `main` needs three things. | Change | What to do | |---|---| | The old constructors (`Map` / `List` / `String` + `Integer`) were removed | Rewrite as `MockResponse.of(method).respond(body, statusCode)` | | `label` / `getBodyAsBlob` / `getHeader` were added to `IHttpRequestHandler` | Add the methods to any custom implementation | | The exhaustion error message became a multi-line diagnostic | Loosen exact-match asserts to `contains` | ## Related Documents - [Layered Constructor Pattern](https://krileworks.com/apex-stem/docs/layered-constructor-pattern): the base pattern behind `IHttpRequestHandler`'s DI design - [TriggerHandler](https://krileworks.com/apex-stem/docs/apex-tools-trigger-handler): the other pillar of ApexTools - [ApexTools guide](https://krileworks.com/apex-stem/docs/apex-tools-guide): back to the guide index ============================================================================== Source: https://krileworks.com/document/apex-stem-get-started.md Page: https://krileworks.com/apex-stem/docs/apex-stem-get-started ============================================================================== # Get Started with Apex Stem Apex Stem is a thought-driven Apex development stack. **You don't need to rewrite anything** — start with the smallest piece that makes your code better today, and the stack grows with you. This page is the short introduction. When you're ready for the full migration playbook, the link at the bottom takes you to the four-step guide with working code. > The code samples run on ApexEloquent **v2 and later**. In v3, SOQL and DML default to user mode (honouring FLS), so keep the running user's field permissions in mind. --- ## What you'll gain - **Tests stop being laxer than production.** A record fetched by real SOQL throws the moment you touch a field it never selected. An SObject you assembled yourself carries no such check — it goes soft in tests and only fails in production. ApexEloquent's mocks know what the production query selected, so they fail in the test instead. - **Unit tests run in milliseconds.** Nothing touches the database, so adding flows and triggers to the org doesn't change the speed. Cheap to run means you actually run them, and the feedback loop keeps turning. - **The structure stays visible.** Parent-child relationships are written as indentation. Building the query and building the test data both take the shape of the data itself. - **The convention you hand to AI stays light.** The architecture declares in about 30 lines ([the real thing is in Handler-Usecase Architecture](https://krileworks.com/apex-stem/docs/handler-usecase-architecture)). Parked in `CLAUDE.md`, it leaves room for the rest of the context you want read. - **No big rewrite required.** One Usecase, one mockable query at a time. --- ## The first move If you only do one thing today, do this: take one SOQL query that's currently inline, and route it through ApexEloquent's typed builder, `Scribe`. Behavior is identical, but the query becomes mockable and gains SELECT-omission detection. ### Before ```apex List accounts = [ SELECT Id, Name FROM Account WHERE Industry = :industry ]; ``` ### After ```apex Scribe accountScribe = Scribe.of(Account.class) .field('Id') .field('Name') .whereEqual('Industry', industry); List accountEntries = new Eloquent().get(accountScribe); ``` Behavior doesn't change. What changes is this one query, seen from a test. - **It becomes swappable.** Inject `MockEloquent` instead of `Eloquent` and you decide what this query returns, without touching the database. - **A missing SELECT starts failing in tests.** The example above only selects `Id` and `Name`. If downstream code reads `Industry`, it throws right there. With a bare SObject you would have got `null`, passed quietly, and found out in production. That's it. One query at a time is plenty. Carving out Usecases and adding test layers comes when you're ready. > Note: fields are passed as strings (`'Id'`, `'Industry'`), not `SObjectField`. That is what lets the builder be assembled dynamically. --- ## When you're ready for more The full guide walks through four incremental steps with real code: 1. **Replace SOQL with Scribe** (above, expanded) 2. **Keep results as IEntry** — why early SObject conversion is a trap 3. **Carve out a Usecase** — the Layered Constructor Pattern 4. **Test at the right layer** — MockEloquent for Usecases, ApexBlueprint for Handlers → **[Read the full guide](https://krileworks.com/apex-stem/docs/apex-stem-full-guide)** ============================================================================== Source: https://krileworks.com/document/apex-stem-full-guide.md Page: https://krileworks.com/apex-stem/docs/apex-stem-full-guide ============================================================================== # Apex Stem: A Step-by-Step Guide to Adoption This guide walks through how to gradually adopt **Apex Stem** in an existing Salesforce codebase, one step at a time. You don't have to rewrite everything — pick the smallest piece that improves your situation today, and grow from there. The four steps below mirror the cards on the home page, but with real code you can copy and adapt. > The samples run on ApexEloquent **v2.1 and later** (they use `label()` / `attach()`). In v3, SOQL and DML default to user mode, honouring FLS — so keep the running user's field permissions in mind. Work that must complete regardless of who triggered it (aggregation, stamping) opts out with `.systemMode()`. --- ## Step 1: Replace one SOQL with Scribe Take a query you currently write inline and route it through ApexEloquent's typed query builder, `Scribe`. Behavior is unchanged, but you gain: - **SELECT-omission detection in tests**: tests fail if your code reads a field you forgot to SELECT. - **Mockability**: the query goes through `IEloquent`, which can be swapped with `MockEloquent` in unit tests. ### Before ```apex List accounts = [ SELECT Id, Name, Industry FROM Account WHERE Industry = :industry ]; ``` ### After ```apex Scribe accountScribe = Scribe.of(Account.class) .field('Id') .field('Name') .field('Industry') .whereEqual('Industry', industry); List accountEntries = new Eloquent().get(accountScribe); ``` Note that fields are passed as **strings** (`'Id'`, `'Industry'`), not as `SObjectField` references. This keeps the builder dynamic — you can compose queries at runtime without fighting the type system. > Raw SOQL (`[SELECT ...]`) is fine for test assertions and other quick uses. For production code, prefer `Scribe` so you keep the mockability/safety net. --- ## Step 2: Keep results as IEntry It's tempting to call `getAsSObject()` and work with plain `Account` records. **Resist that for business logic.** Keep results as `List` and access fields via `entry.get('FieldName')`. ### Why `IEntry` gives you three things that early conversion to `SObject` destroys: 1. **SELECT-omission detection.** If you forgot to `field('Industry')` in your Scribe and then read `entry.get('Industry')`, the test fails. With raw `SObject`, the access would silently return `null`. 2. **Mockable formulas, rollups, and non-writable fields.** `MockEntry.set()` can write to formula fields, rollups, auto-numbers, and other normally-read-only fields. Your business logic can be unit-tested against those values without firing real formulas. 3. **Get-edit-update stays in IEntry.** `entry.put('Foo__c', value)` mutates the entry; `eloquent.doUpdate(entries)` accepts `List` directly. ### Reading fields ```apex for (IEntry accountEntry : accountEntries) { Id id = accountEntry.getId(); // dedicated getter String name = accountEntry.getName(); // dedicated getter String industry = (String) accountEntry.get('Industry'); // cast required } ``` ### Why not drop down to SObject `getAsSObject()` will hand you an `SObject`, but **the moment you drop down, SELECT-omission detection stops working downstream**. ```apex // ❌ Whatever receives this can touch an unselected field and just get null — quietly List items = (List) eloquent.getAsSObject(scribe); // ✅ Pass IEntry along and an unselected access throws right there List items = eloquent.get(scribe); ``` A typed `SObject` (`EstimateItems__c`) leaves the same hole. The rule extends to **not typing your own method and class parameters as `SObject`** either. ### When SObject is OK - Passing to external APIs that require `SObject` (e.g., `Messaging.SingleEmailMessage`, `Database.SaveResult` handling, `Approval.process`). - `Trigger.new` and other SObject-native contexts that never pass through `Eloquent`. "The cast is annoying" and "SObject is quicker to write right now" are not reasons. Break the rule once and nothing holds the line afterwards. > 💡 When in doubt, ask: **did this record come from a query?** If yes, `IEntry`; if you just `new`ed it, `SObject`. That is also why `doInsert` has no `IEntry` overload — a brand-new record has no notion of SELECT, so wrapping it detects nothing. --- ## Step 3: Carve out a Usecase When the SOQL + logic combination grows past a few lines, give it a name. An Apex Stem **Usecase** is an object whose only public surface is `invoke()`. ```apex public with sharing class CountActiveAccountsByIndustryUsecase { private final String industry; private final IEloquent fetchEloquent; private Trace t = Trace.of('Count active accounts by industry'); // public ctor: production, business input only public CountActiveAccountsByIndustryUsecase(String industry) { this(industry, null); } // private (@TestVisible) ctor: tests inject the IEloquent @TestVisible private CountActiveAccountsByIndustryUsecase(String industry, IEloquent fetchEloquent) { this.industry = industry; this.fetchEloquent = fetchEloquent ?? new Eloquent(); } public Integer invoke() { this.t.start(); if (String.isBlank(this.industry)) { this.t.skip('Industry is blank; nothing to count.'); return 0; } Scribe accountScribe = Scribe.of(Account.class) .field('Id') .whereEqual('Industry', this.industry) .whereEqual('Active__c', true); Integer count = this.fetchEloquent.get(accountScribe).size(); this.t.finish('Counted ' + count + ' active accounts.'); return count; } } ``` This is the **Layered Constructor Pattern**: - **Simple production API**: `new CountActiveAccountsByIndustryUsecase('Tech').invoke()` - **Flexible test API**: `new CountActiveAccountsByIndustryUsecase('Tech', mockEloquent).invoke()` - **No half-built objects**: every dependency is set at construction time. > If a Usecase needs **two SOQL queries** against the same object (e.g., one for last month, one for this year), inject **two separate `IEloquent` instances**. `MockEloquent` doesn't evaluate `WHERE`, so you need distinct mocks per query to return different result sets. --- ## Step 4: Test at the right layer The architecture maps 1-to-1 to two test strategies: - **Usecase layer → unit tests with `MockEloquent`** — no DB, fast, exhaustive logic coverage. - **Handler layer → integration tests with `SBlueprint`** — real DML, validates the wiring. ### Unit test for a Usecase ```apex @isTest static void testInvoke_WhenIndustryIsTech_ThenReturnsCount() { Trace t = Trace.of('Returns count of active Tech accounts'); t.start(); // Arrange — MockEloquent returns 3 entries regardless of WHERE IEloquent mockEloquent = new MockEloquent(new List{ MockEntry.of(Account.class).autoId('{#}').times(3) }); // Act Integer count = new CountActiveAccountsByIndustryUsecase('Technology', mockEloquent).invoke(); // Assert Assert.areEqual(3, count); Assert.isTrue(TraceFlow.isLastFinish()); t.finish(); } @isTest static void testInvoke_WhenIndustryIsBlank_ThenSkipped() { Trace t = Trace.of('Returns 0 and skips when industry is blank'); t.start(); Integer count = new CountActiveAccountsByIndustryUsecase('', new MockEloquent()).invoke(); Assert.areEqual(0, count); Assert.isTrue(TraceFlow.isLastSkip()); t.finish(); } ``` The `TraceFlow` assertions confirm **which code path ran**, not just the return value. That distinguishes "skipped because nothing to do" from "finished with zero hits". ### The Handler under test Before the integration test, you need the Handler that calls the Usecase. The trigger file **declares all seven events and does nothing but call the Handler on one line**. ```apex trigger Opportunity on Opportunity( before insert, before update, before delete, after insert, after update, after delete, after undelete ) { (new TriggerOppHandler()).execute(); } ``` The Handler extends `TriggerHandler` (ApexTools) and overrides **only the hooks it needs**. All it does is decide conditions and call a Usecase — no business logic. ```apex public with sharing class TriggerOppHandler extends TriggerHandler { protected override void afterInsert(Map newRecordsMap) { (new CopyAccountIndustryToOpportunityUsecase(newRecordsMap.keySet())).invoke(); } } ``` Hooks you don't override (`beforeUpdate` and friends) do nothing. There is no need to fill them with empty methods. > To act "only when a particular field changed", use the base class's `getUpdateRecordIdsWithChangedFields(...)`. If you find yourself hand-writing `Trigger.isAfter` or comparing new/old yourself, that is a sign the base class isn't in place. ### Integration test for a Handler with ApexBlueprint ```apex @isTest static void testAfterInsert_WhenChildLinked_ThenParentCountUpdated() { Trace t = Trace.of('After insert, parent count is updated'); t.start(); // Arrange — create real records via ApexBlueprint SOrchestrator orchestrator = SOrchestrator.start() .add(SBlueprint.of(Account.class) .alias('parent') .template(Blueprints.accBasic()) .set('Industry', 'Technology')); orchestrator.create(); Account parent = (Account) orchestrator.getByAlias('parent'); // Act — fire the trigger by inserting/updating real data // (your Handler-specific DML here) // Assert — re-query and verify the wiring worked Account refetched = [ SELECT Id, ActiveAccountCount__c FROM Account WHERE Id = :parent.Id ]; Assert.areEqual(1, refetched.ActiveAccountCount__c); t.finish(); } ``` Real DML flows through real triggers, so this test validates the **whole chain**: Handler → Usecase → ApexEloquent → DB. Use it sparingly — 1 to 3 representative cases per Handler is plenty. Exhaustive logic coverage stays in the Usecase unit tests. ### Make one of those representatives a bulk test When triggers set off other triggers, **make one of your representative cases a test that pushes a production-like volume through a single DML and checks the governor headroom**. The reason is simple: **`MockEloquent` issues no real SOQL, so query inefficiency is completely invisible from unit tests**. An implementation that walks down a hierarchy firing `whereIn` stays green in units and then throws `Too many SOQL queries: 101` in a production bulk run. A single-scenario integration test like the one above never approaches the 100-SOQL ceiling with a handful of records either. ```apex // Add times() to the Arrange hierarchy to mass-produce, SBlueprint.of(Opportunity.class).template(Blueprints.oppBasic()).alias('opp_{#}').times(30) // and add governor headroom to the Assert Assert.isTrue( Limits.getQueries() < Limits.getLimitQueries() / 2, 'SOQL should stay under half the limit even in bulk. Measured: ' + Limits.getQueries() ); ``` Keep the volume to the minimum that reproduces the problem (mind the 10,000 DML row limit). To name which Usecase is doing the consuming, `TraceFlow.lastUsage()` pins it down per Usecase (see the [ApexTrace guide](https://krileworks.com/apex-stem/docs/apex-trace-guide)). The whole picture is laid out in [Test Strategy](https://krileworks.com/apex-stem/docs/test-strategy). --- ## Where to go from here - **The Stack overview**: each library's role and links to its source. To dig into the design itself: - **[Handler-Usecase Architecture](https://krileworks.com/apex-stem/docs/handler-usecase-architecture)**: the two layers, the five entry-point kinds, and where it overlaps with Salesforce's own recommendation. - **[Layered Constructor Pattern](https://krileworks.com/apex-stem/docs/layered-constructor-pattern)**: the two constructors from Step 3, taken as a topic of their own. - **[Test Strategy](https://krileworks.com/apex-stem/docs/test-strategy)**: Step 4's judgement calls, systematised — triage on failure, how to shape CI/CD, and the pitfalls. To dig into each library: - **ApexEloquent guide**: deeper coverage of Scribe (aggregates, parent fields, subqueries, MockEntry advanced patterns). - **ApexBlueprint guide**: SBlueprint templates, `use()` for sibling references, `withChildren()` for nested trees. - **ApexTrace guide**: TraceFlow, nest constraints, and TraceUsage for measuring governor consumption. - **[ApexTools guide](https://krileworks.com/apex-stem/docs/apex-tools-guide)**: the `TriggerHandler` base class used in Step 4, and the DI-able HTTP request wrapper. Start with one library, one query, one Usecase. The stack grows with you, not against you. ============================================================================== Source: https://krileworks.com/document/handler-usecase-architecture.md Page: https://krileworks.com/apex-stem/docs/handler-usecase-architecture ============================================================================== # Handler-Usecase Architecture This document covers Apex Stem's core architecture — the Handler-Usecase Architecture — digging into both the design philosophy and the structure. Read this after you've worked through the [Apex Stem Introduction Guide](https://krileworks.com/apex-stem/docs/apex-stem-full-guide), when you want to understand "what exactly is a Handler, and what exactly is a Usecase?". The Handler-Usecase Architecture is a lightweight application architecture for Salesforce Apex development. Every Apex process is designed in **two layers: a Handler (the entry point) and a Usecase (the business logic)**. ## What the Handler-Usecase Architecture Is It is **a third way** — neither a heavyweight multi-layer architecture like fflib, nor the chaos of stuffing everything into a Trigger or a single class. It was conceived and proven while rebuilding the chaos of a Salesforce org that had been operated for 5 years. The conventions you have to remember are just "two trunk layers (Handler and Usecase)" and "the basic principles of object orientation". Everything else is left to the field. ## Why Two Layers ### In the Laravel-MVC Lineage The Laravel web framework defines only Controller, Model, and View, leaving components like Service, Action, and Repository to the community. Rails does the same. The Handler-Usecase Architecture stands in that lineage. **Handler and Usecase are the trunk**, and we don't prescribe categories for the components that appear beneath them (Reader / Validator / Mapper, and so on). It is exactly the opposite philosophy from fflib, which prescribes Selector / Domain / Service / UnitOfWork all at once. ### Why "Don't Over-Prescribe" 1. **Different businesses need different components**. Some need a Reader; some need a Validator. Picking all categories in advance creates ones that never get used. 2. **Prescription leads to formalism**. After 5 years of operation, you will always end up with "a class named Selector, but the inside is straight-line code" — name-only structures. 3. **A growth opportunity for juniors**. Thinking through "what to call from the Handler" and "where to split out a component" is itself training in object-oriented design. 4. **In the age of AI, too many conventions become noise**. Telling an AI coding assistant "follow these 12 patterns" is more error-prone than "two trunks + OOP principles". ### Connection to the "Mino-Driven Book" The principle "**don't create half-baked objects**" from *Good Code / Bad Code: Design Fundamentals* (a Japanese design primer commonly nicknamed the Mino-Driven Book) is the core of the Usecase. Receive everything you need through the constructor and don't bolt on state via setters later. This is the Apex version of the Value Object thinking in *Effective Java* and Domain-Driven Design. ## The Handler Layer ### Handler responsibilities The Handler's responsibility is **only to absorb the entry-point-specific conventions and hand them off to the Usecase**. Business logic doesn't live here. Filtering target records is allowed, but nothing more. Its role is close to a Controller in Laravel. ### Five Kinds of Entry Points Apex has multiple kinds of entry points, and we prepare a Handler for each. | Type | Entry Source | |---|---| | TriggerHandlers | DML triggers (before/after × insert/update/delete) | | BatchHandlers | Batchable / Schedulable | | RestHandlers | @RestResource | | FlowHandlers | @InvocableMethod (called from Flow) | | SchedulableHandlers | Pure Schedulable | ### The Fixed Trigger.cls Pattern The trigger file follows Salesforce convention and uses a fixed form. Declare all seven events and call the Handler in one line. No logic in the trigger file itself. ```apex trigger Opportunity on Opportunity( before insert, before update, before delete, after insert, after update, after delete, after undelete ) { (new TriggerOppHandler()).execute(); } ``` ### The TriggerHandler Base Class Each Handler extends the `TriggerHandler` base class (provided by ApexTools) and overrides only the hooks it needs. All hooks are `protected virtual`; hooks you don't override do nothing. The main hooks are `beforeInsert` / `beforeUpdate` / `beforeDelete` / `afterInsert` / `afterUpdate` / `afterDelete` / `afterUndelete`, plus `andFinally`, which is always called last. A helper for "narrow down to records where specific fields changed" (`getUpdateRecordIdsWithChangedFields` and friends) is also provided. ### Code Example Here we call the `CopyAccountIndustryToOpportunityUsecase` (the Usecase that copies the parent Account's industry to the Opportunity), already covered in the [Apex Stem Introduction Guide](https://krileworks.com/apex-stem/docs/apex-stem-full-guide), from a Trigger. ```apex public with sharing class TriggerOppHandler extends TriggerHandler { protected override void afterInsert(Map newRecordsMap) { Set opportunityIds = newRecordsMap.keySet(); (new CopyAccountIndustryToOpportunityUsecase(opportunityIds)).invoke(); } } ``` All the Handler is doing is "collect Opportunity Ids from `Trigger.new` and hand them off to the Usecase". The logic of copying the industry doesn't live here at all — that's the Usecase's job. When you only want to call a Usecase if specific fields changed, narrow down with the base class's helper. ```apex protected override void afterUpdate(Map newMap, Map oldMap) { Set needIds = this.getUpdateRecordIdsWithChangedField(Opportunity.AccountId); (new CopyAccountIndustryToOpportunityUsecase(needIds)).invoke(); } ``` ## The Usecase Layer ### Usecase responsibilities The Usecase implements **a single piece of business logic**. The only public method is `invoke()` — everything else is private. The return type of `invoke()` is chosen by the nature of the process (a Result DTO for Usecases called from LWC, often `void` for those driven by Trigger or Batch, and so on). Its role is close to a Service or Action in Laravel. ### Don't Create Half-Baked Objects The Usecase receives every dependency it needs through the constructor. You don't bolt on state later via setters. The state "the moment the constructor is called, this Usecase is complete" is preserved. ### Two Constructors The Usecase has two constructors. - **The `public` constructor**. For production. Receives only the business inputs and creates dependencies like data access by default. - **The `@TestVisible private` constructor**. For tests. Receives dependencies as arguments so tests can inject mocks. ```apex public with sharing class CopyAccountIndustryToOpportunityUsecase { @TestVisible static final String LBL_FETCH = 'oppFetch'; @TestVisible static final String LBL_UPDATE = 'oppUpdate'; private final Set opportunityIds; private final IEloquent eloquent; private Trace t = Trace.of('Copy parent Account industry to Opportunity'); // public: for production, receives only business inputs public CopyAccountIndustryToOpportunityUsecase(Set opportunityIds) { this(opportunityIds, null); } // private (@TestVisible): inject IEloquent in tests @TestVisible private CopyAccountIndustryToOpportunityUsecase( Set opportunityIds, IEloquent eloquent ) { this.opportunityIds = opportunityIds; this.eloquent = eloquent ?? new Eloquent(); } public void invoke() { // ... business logic (full text in step 3 of the Introduction Guide) ... } } ``` This "split the constructor in two — one for production, one for tests" style is the **Layered Constructor Pattern**. For details see [Layered Constructor Pattern](https://krileworks.com/apex-stem/docs/layered-constructor-pattern). A hands-on example is in [Step 3 of the Introduction Guide](https://krileworks.com/apex-stem/docs/apex-stem-full-guide). ### Single Usecase vs Orchestrator Usecase There are two shapes of Usecase. - **Single Usecase**. A small piece of logic that completes within private methods. The `CopyAccountIndustryToOpportunityUsecase` above is this kind. - **Orchestrator Usecase**. A larger piece of logic that integrates multiple component classes (Readers, Validators, or even other Usecases). Both follow the same principles: "only `invoke()` is public" and "don't create half-baked objects". ## Component Classes The Handler-Usecase Architecture **does not prescribe categories for the component classes** that appear beneath a Usecase. The kinds of components that show up depend heavily on the business. "Always create a Reader / Validator / Mapper" is not what we say. Some processes need a Reader; others need a Validator. That call is made on the ground. **There is only one common rule**: component classes also follow the "don't create half-baked objects" principle. Receive everything you need in the constructor, and make them objects that can only exist in a complete state. The split is allowed to be incremental. Start by writing it as a private method inside the Usecase, and split it out into an independent class when it gets complex. It matters not to over-componentize from the start. ## Test Strategy (Overview) The two layers of the Handler-Usecase Architecture map 1:1 onto two test strategies. | Layer | Test Type | DB Access | Testing OSS | |---|---|---|---| | Usecase layer | Unit test | None (mocks) | ApexEloquent (MockEloquent / MockEntry) | | Handler layer | Integration test | Yes (real DML) | ApexBlueprint (SBlueprint / SOrchestrator) | Logic coverage happens in Usecase unit tests. The Handler integration test narrows down to 1–3 representative cases of "the Usecase is correctly invoked through Trigger or Batch, and the expected behavior is observed". For the full test strategy, see [Test Strategy](https://krileworks.com/apex-stem/docs/test-strategy). A hands-on example is in [Step 4 of the Introduction Guide](https://krileworks.com/apex-stem/docs/apex-stem-full-guide). ## Where Salesforce's Own Recommendation Overlaps Everything above was arrived at empirically, out of pain. We found out afterwards that **Salesforce recommends the same design**. ### Separation of concerns From the official blog post [Reduce Deployment Test Time with Smarter Apex Test Runs](https://www.salesforce.com/blog/faster-deployment-test-runs/): > **business logic and database interfacing should be separate concerns** And the refactoring sequence the post lays out reads as the Handler-Usecase sequence verbatim: carve database access into its own class, abstract it behind an interface, make it swappable by DI, and inject a mock implementation in tests. ### The constructor shape is the same too Here is the sample code from the official post. ```apex public class OpportunityService { private OpportunityServiceDbHandler dbHandler; public OpportunityService() { this(new OpportunityServiceDbHandlerImpl()); // for production } @TestVisible private OpportunityService( OpportunityServiceDbHandler dbHandler) { // for tests (DI) this.dbHandler = dbHandler; } } ``` Structurally identical to the Layered Constructor Pattern described above. A public constructor for production that delegates inward, and a `@TestVisible private` one that takes the dependency for tests. The same answer to the same problem. ### Two layers of tests On dividing up tests, the official position is: > **The vast majority of your tests should be true unit tests.** > Testing triggers, for example, **requires real DML execution**, as there is no substitute for validating the execution order. > Such tests aren't unit tests; they're integration or functional tests. **Use them sparingly**: only when you need to test a trigger or a particularly important or complex integration flow. "Units are the bulk", "triggers need real DML", "keep integration sparing". The same thing the test-strategy table above says. ### Where we differ The official approach is correct, but it means **hand-writing a DbHandler class, an interface and a mock implementation per service**. Fifty Usecases bring 150 classes along with them. You also write the SOQL assembly and the test-data dependency resolution yourself, every time. What ApexEloquent did was generalise that DbHandler into a single one. | | The official pattern | ApexEloquent | |---|---|---| | DB access abstraction | hand-written interface per service | one `IEloquent` | | Production implementation | hand-written Impl per service | one `Eloquent` | | Mock | hand-written Mock per service | one `MockEloquent` | | Query assembly | raw SOQL by hand | `Scribe` (builder) | | Test data | assembled by hand | `MockEntry` / ApexBlueprint | Not an invention. **Just the design the official position points at, generalised so you don't hand-write it every time.** Which also means no particular OSS is required to adopt this design. Hand-write it the official way if you like, or use fflib, or Apex Fluently. Apex Stem combines its four OSS projects because they line up directly with the Handler-Usecase test strategy. ### A reservation about granularity We expect the *reason* for splitting to hold for a long time. **How finely to split** is a separate question. We do not currently have grounds to claim that "one Usecase = one piece of business logic" is the optimum level of fineness. Splitting finely has costs too (more classes, a harder time seeing the whole). It is entirely possible that as models improve, a coarser granularity turns out to be enough. What is written here is the granularity we consider reasonable as of 2026. ## Comparison with Other Frameworks ### Comparison with fflib The Handler-Usecase Architecture isn't a rejection of fflib. Different scenes call for different tools. | Aspect | fflib | Handler-Usecase Architecture | |---|---|---| | Philosophy | Prescriptive (Java EE / Spring lineage) | Minimal skeleton (Laravel / Rails lineage) | | Required concepts | Selector, Domain, Service, UnitOfWork | Handler, Usecase | | Component prescription | Yes | No (field judgment) | | Adoption into existing orgs | Rewrite-first | Erodes one method at a time | | AI integration | Many conventions, easy for AI to get lost | 2 conventions + principles, easy to convey to AI | | Best fit | When you want to unify a team of 50+ | When 1–5 people want speed and AI integration | We respect the work Andy Fawcett built into fflib. On that foundation, the Handler-Usecase Architecture is "a different choice for a different scene". --- ### Relationship with Apex Fluently Recently, another option has emerged: [Apex Fluently](https://apexfluently.beyondthecloud.dev/) (by Beyond The Cloud) — an OSS set with 8 libraries (SOQL Lib, DML Lib, Async Lib, Cache Manager and more) framed as "a modern alternative to fflib". But Apex Fluently **deliberately doesn't prescribe an architecture**. It doesn't get involved in application layering or responsibility division; it's designed as a "toolbox" where each library can be adopted individually. So Apex Fluently and the Handler-Usecase Architecture are arguing on different stages. Looking at all three by their position: - **fflib**: a heavyweight **architecture** (Selector / Domain / Service / UnitOfWork) - **Apex Fluently**: pure **tools** (no prescribed architecture) - **Handler-Usecase Architecture**: a minimal **architecture** (Handler + Usecase) Apex Stem offers both — the architecture (Handler-Usecase Architecture) and the four OSS (the tools). Apex Fluently sits in the same layer as the OSS layer of Apex Stem (ApexEloquent and friends), and doesn't directly compete with the Handler-Usecase Architecture. The Handler-Usecase Architecture is also tool-independent. In principle, you could use Apex Fluently's libraries under the Handler and Usecase layers. Apex Stem combines ApexEloquent / ApexBlueprint / ApexTrace / ApexTools because they directly align with the Handler-Usecase Architecture's test strategy (Usecase ↔ MockEloquent, Handler ↔ ApexBlueprint), not because the architecture forces that combination. ## Position in the Age of AI The Handler-Usecase Architecture is **designed with AI coding assistants in mind**. Light conventions carry a clear practical benefit. AI coding assistants like Claude Code read a file describing project rules (`CLAUDE.md`). Trying to write fflib's conventions into that file pushes past 200 lines, requires examples, and increases the risk that the AI gets lost. With the Handler-Usecase Architecture, the core of the architecture comes through to the AI with a short description like the following. In practice, placing this in `CLAUDE.md` lets Claude Code write Apex in line with the architecture. ```markdown ## Architecture (Handler + Usecase, two layers) Every Apex process is designed in two layers: Handler (entry point) + Usecase (business logic). ### Responsibility separation - Handler: interprets arguments handed in from the entry point and invokes the appropriate Usecase. No business logic. - Usecase: implements a single piece of business logic. Receives parameters via constructor; only invoke() is public. ### Handler implementation pattern The Handler does only "condition checking" and "Usecase invocation". The Trigger file declares all 7 events and calls the Handler in one line. ### Usecase standard pattern 1. Two constructors (public for production / @TestVisible private for tests) 2. Only invoke() is public; everything else is private 3. Dependencies use null-coalescing for production defaults (eloquent ?? new Eloquent()) ### Test strategy - Usecase unit test: cover logic branches with MockEloquent - Handler integration test: 1-3 representative cases with real DML ``` This isn't theoretical. KrileWorks itself uses this description in real projects and collaborates with AI on the code. **"With short conventions, the AI follows the architecture" is empirically demonstrated**. While fflib was designed in the era before AI, the Handler-Usecase Architecture is probably one of the first architectures in the Apex world that was **designed assuming AI codegen**. ## Read Next - [Apex Stem Introduction Guide](https://krileworks.com/apex-stem/docs/apex-stem-full-guide): the 4 steps to gradually adopt it in an existing codebase - [Layered Constructor Pattern](https://krileworks.com/apex-stem/docs/layered-constructor-pattern): the design of the two constructors - [Test Strategy](https://krileworks.com/apex-stem/docs/test-strategy): how to write tests for the Handler and Usecase respectively ============================================================================== Source: https://krileworks.com/document/layered-constructor-pattern.md Page: https://krileworks.com/apex-stem/docs/layered-constructor-pattern ============================================================================== # Layered Constructor Pattern This document digs into the **Layered Constructor Pattern**, a design pattern that recurs throughout Apex Stem's Usecase layer, as a standalone topic. Read this after [Handler-Usecase Architecture](https://krileworks.com/apex-stem/docs/handler-usecase-architecture) — if you came away wondering "what exactly are these two constructors?", this is your answer. The Layered Constructor Pattern is the design pattern that lets a single Usecase class hold both **a clean production API** and **flexible test-side dependency injection**. It functions as the standard play for keeping testability without needing a DI container — even in Apex, where there isn't one — while still refusing to create half-baked objects. Salesforce's own blog shows a sample with **exactly this constructor shape** ([the passage](https://krileworks.com/apex-stem/docs/handler-usecase-architecture)). This is not a bespoke idea. ## What the Layered Constructor Pattern Is You place two constructors side by side inside one Usecase class. - **A `public` constructor**: for production. Receives only the business inputs (Ids, SObjects, request DTOs, etc.). - **A `@TestVisible private` constructor**: for tests. Receives business inputs plus every dependency (data access, helper components, etc.) as arguments. The public constructor delegates to the private one with `this(...)`, passing `null` for every dependency. On the private side, null-coalescing (e.g. `?? new Eloquent()`) swaps in the production default. With this shape, dependencies are completely hidden from the production caller, and tests can inject mocks. In Apex Stem's Usecase layer, this is the standard form. ## Why This Form Is Necessary ### Open Exactly One Seam The point of this pattern isn't "write two constructors". It is to **gather the place where things can be swapped — the seam — into the single point where the object is completed**. The private constructor is the one and only initialisation path in this class. Fields are filled here and nowhere else; dependencies are decided here and nowhere else. So the place a test intervenes is also just this one point. With a single seam, several things hold at once. - **Production callers never see the seam.** `new Xxx(ids)` returns something complete, every time - **Tests intervene without adding anything.** No setters to grow, no visibility to loosen, no test-only flags - **Every path builds the same invariants.** Whether you came through public or private, one body of code runs Scatter the seam instead — three setters, two initialisation paths — and "which calls make it complete" leaks out of the class. This shape is what prevents that. ### The Seam Exists to Keep Tests From Going Laxer Than Production Making things swappable isn't only about speed. It matters just as much that **tests don't get laxer than production**. Swap `IEloquent` for `MockEloquent` at this seam and the mock hands back records **knowing what the production query (`Scribe`) selected**. Touch a field it never selected and the unit test throws. Had you passed a hand-assembled SObject straight in, you would have got `null`, passed quietly, and found out in production. Because the seam sits here, the mock inherits production's contract. This isn't "bending the design for tests" — it is **fixing the design and having tests move closer to production**. ### Apex Has No DI Container A DI container that auto-wires dependencies — like Java's Spring or PHP's Laravel — isn't in Apex's standard library. There's no inject annotation, and no automatic constructor resolution either. That means **how to inject dependencies has to be designed by hand**. The Layered Constructor Pattern is one answer to that hand-rolled DI question. --- ### "Take Everything Through the Constructor" Is Painful for Callers A naive approach would be a single public constructor that takes both the business inputs and every dependency. ```apex // Anti-pattern: production has to assemble every dependency too new CreateOpportunityFromAccountUsecase( accountId, new AccountReader(new Eloquent()), new OpportunityEligibilityValidator(), new OpportunityMapper(), new Eloquent() ).invoke(); ``` Writing two `new Eloquent()`s every time a Trigger handler invokes a Usecase isn't realistic. Production code gets noisier, and the area you need to touch to update a default expands. --- ### "Parameter-less Constructor + Setter Injection" Creates Half-Baked Objects Another approach is to create the instance with a parameter-less constructor and inject dependencies via setters. ```apex // Anti-pattern: setter injection creates "half-baked objects" CopyAccountIndustryToOpportunityUsecase usecase = new CopyAccountIndustryToOpportunityUsecase(); usecase.setOpportunityIds(opportunityIds); usecase.setFetchEloquent(new Eloquent()); usecase.invoke(); ``` This violates the Handler-Usecase Architecture's core principle of "**don't create half-baked objects**". A forgotten setter call's NullPointerException is hard to surface, and you lose the invariant "the moment the constructor is called, this object is complete". --- ### The Layered Constructor Pattern Resolves Both Under the Layered Constructor Pattern, - The production side only needs to pass the **business inputs** through the public constructor - The test side assembles the object with **all dependencies, in complete state**, through the private constructor - Both constructors preserve the property "complete the moment they're called" In place of a DI container, the combination of the compiler and `@TestVisible` plays the role. ## Structure ### Roles of the public and private Constructors | Constructor | Visibility | Arguments | Role | |---|---|---|---| | public constructor | `public` | Business inputs only | The production entry point. Delegates to the private constructor | | private constructor | `@TestVisible private` | Business inputs + all dependencies | The single object-initialization site. If a dependency is null, swap in the production default | "The public constructor is minimal; the private constructor is complete" is the rule. Dependencies are hidden from the production caller, and tests can swap every dependency — both at once. ### Delegation and null-coalescing The public constructor delegates to the private one with `this(...)`, passing `null` for every dependency. On the private side, the null-coalescing operator (`??`) swaps in the production default. ```apex public CopyAccountIndustryToOpportunityUsecase(Set opportunityIds) { this(opportunityIds, null); // dependency null; delegate to private constructor } @TestVisible private CopyAccountIndustryToOpportunityUsecase( Set opportunityIds, IEloquent eloquent ) { this.opportunityIds = opportunityIds; this.eloquent = eloquent ?? new Eloquent(); // null → production default } ``` From the caller's perspective, production is one line — `new CopyAccountIndustryToOpportunityUsecase(ids)` — and tests swap the dependency with `new CopyAccountIndustryToOpportunityUsecase(ids, mock)`. ## Example 1: Leaf Usecase (DI'ing IEloquent) Let's revisit `CopyAccountIndustryToOpportunityUsecase` from [Step 3 of the Apex Stem Introduction Guide](https://krileworks.com/apex-stem/docs/apex-stem-full-guide) through the Layered Constructor Pattern lens. ```apex public with sharing class CopyAccountIndustryToOpportunityUsecase { @TestVisible static final String LBL_FETCH = 'oppFetch'; // opportunity + parent account fetch @TestVisible static final String LBL_UPDATE = 'oppUpdate'; // opportunity update private final Set opportunityIds; private final IEloquent eloquent; private Trace t = Trace.of('Copy parent Account industry to Opportunity'); public CopyAccountIndustryToOpportunityUsecase(Set opportunityIds) { this(opportunityIds, null); } @TestVisible private CopyAccountIndustryToOpportunityUsecase( Set opportunityIds, IEloquent eloquent ) { this.opportunityIds = opportunityIds; this.eloquent = eloquent ?? new Eloquent(); } public void invoke() { // (full text in step 3 of the Introduction Guide) } } ``` Two points to notice. - **A single `IEloquent` multiplexed by label, split by purpose**. By labeling fetch (`LBL_FETCH`) and update (`LBL_UPDATE`) on the same `IEloquent`, tests can run independent scenarios — "fetch succeeds but update throws" — on a single `MockEloquent`. Before `label()` arrived in v2.1, you would DI two separate `IEloquent` fields (still works, but the constructor swells). - **`?? new Eloquent()` for the production default**. Dependencies are completely hidden from the production caller — `new CopyAccountIndustryToOpportunityUsecase(ids)` is all you need. In this way, receiving data access through an abstraction (`IEloquent`) and DI-ing it via the Layered Constructor Pattern is the basic form for leaf Usecases. When the Usecase only depends on `IEloquent`, the v2.1+ recommendation is to **consolidate it into one `IEloquent` with label multiplexing**. Splitting different kinds of dependencies (`IEloquent` + Reader + Validator + Mapper, etc.) is covered in Example 2. ## Example 2: Orchestrator Usecase (DI'ing Component Classes) In an orchestrator Usecase that bundles multiple steps, dependencies expand beyond `IEloquent` into **component classes** like Reader / Validator / Mapper. The Layered Constructor Pattern's shape doesn't change, though. As an example, consider a Usecase that "creates one Opportunity from an Account Id". The steps: 1. Fetch the Account (`AccountReader`) 2. Validate that we may create an Opportunity (`OpportunityEligibilityValidator`) 3. Assemble an Opportunity from the Account info (`OpportunityMapper`) 4. Insert the Opportunity (`IEloquent`) ```apex public with sharing class CreateOpportunityFromAccountUsecase { private final Id accountId; private final AccountReader accountReader; private final OpportunityEligibilityValidator validator; private final OpportunityMapper mapper; private final IEloquent insertEloquent; private Trace t = Trace.of('Create an opportunity from an account'); public CreateOpportunityFromAccountUsecase(Id accountId) { this(accountId, null, null, null, null); } @TestVisible private CreateOpportunityFromAccountUsecase( Id accountId, AccountReader accountReader, OpportunityEligibilityValidator validator, OpportunityMapper mapper, IEloquent insertEloquent ) { this.accountId = accountId; this.accountReader = accountReader ?? new AccountReader(new Eloquent()); this.validator = validator ?? new OpportunityEligibilityValidator(); this.mapper = mapper ?? new OpportunityMapper(); this.insertEloquent = insertEloquent ?? new Eloquent(); } public void invoke() { this.t.start(); IEntry accountEntry = this.accountReader.fetch(this.accountId); this.validator.assertEligible(accountEntry); Opportunity opp = this.mapper.toOpportunity(accountEntry); this.insertEloquent.doInsert(opp); this.t.finish('Created 1 opportunity.'); } } ``` The shape is identical to Example 1. The only change is **the number and kinds of dependencies**. The public constructor still takes only business input (`accountId`), and the private one swaps null into production defaults. `AccountReader` itself follows the same "constructor receives `IEloquent`" pattern (the "don't create half-baked objects" principle), and `new AccountReader(new Eloquent())` is enough to complete it. Components without external dependencies, like `OpportunityEligibilityValidator` and `OpportunityMapper`, are assembled with a parameter-less `new`. > For how to split component classes (`Reader` / `Validator` / `Mapper`) and the responsibility-division guidelines, see ["Component Classes" in Handler-Usecase Architecture](https://krileworks.com/apex-stem/docs/handler-usecase-architecture). This document's focus is **how to inject them** into the Usecase. ### This Four-Way Split Is Not the Goal Example 2 exists to show that **the shape doesn't change as the kinds of dependencies grow** — not to recommend splitting into four. What justifies a split is **whether there is a seam you want to swap**. `AccountReader` touches the database, so it earns one. Carve out pure logic you never need to swap, purely because "I want to DI it", and the trade stops paying. - Arguments start getting relayed around (parameters that exist only to pass data between components) - Code working on the same data lands in different classes and **queries the same records twice** - The class you carved out ends up half-formed, used by nobody else The rule of thumb is: **keep together what shares the same knowledge (context), and split where the knowledge changes.** Lumping things together indiscriminately is the opposite failure — a class made the dumping ground for shared code belongs to no context at all. Ask this once after you've implemented, and the granularity settles. > Is there anything to consolidate or streamline? Avoid over-consolidating: bring together **only knowledge that shares a context**. The reservation about granularity itself is written up in ["A reservation about granularity" in Handler-Usecase Architecture](https://krileworks.com/apex-stem/docs/handler-usecase-architecture). What's shown here is the granularity we consider reasonable as of 2026. ## What Changes in Tests A Usecase that adopts the Layered Constructor Pattern gains the following freedoms in tests. - **Per-purpose independent dependency swap**. For a single `IEloquent`, label-multiplex it (e.g. `LBL_FETCH` / `LBL_UPDATE`) via `label()`; for an orchestrator, split component classes across separate fields — pick the granularity that fits. Either way, independent scenarios fall out naturally: "fetch succeeds but update throws", "fetch returns empty and update isn't called". Verifications that are hard to express with one **label-less** shared `IEloquent` come together painlessly. - **Mock / fake / real component classes at any granularity**. In orchestrator Usecases, you can mock `AccountReader` while using the real `OpportunityMapper`, per test. "Keep the logical core running on the real implementation while closing off only the external I/O" — that kind of test design becomes natural. - **Zero changes to production code**. You don't need to add a constructor or a setter to production code to enable tests. The `@TestVisible` private constructor is already there as the dedicated test entry point. For concrete test code, see [Step 4 of the Apex Stem Introduction Guide](https://krileworks.com/apex-stem/docs/apex-stem-full-guide). Injecting a single `MockEloquent`, feeding the read through `attach(LBL_FETCH, ...)` and checking the write with `upsertedRecordsAt(LBL_UPDATE)` is the canonical use of the Layered Constructor Pattern. ## Read Next - [Handler-Usecase Architecture](https://krileworks.com/apex-stem/docs/handler-usecase-architecture): the core Apex Stem architecture that includes the Usecase layer where the Layered Constructor Pattern appears - [Apex Stem Introduction Guide](https://krileworks.com/apex-stem/docs/apex-stem-full-guide): walking through the 4 steps, including the Layered Constructor Pattern, with working code - [Test Strategy](https://krileworks.com/apex-stem/docs/test-strategy): how to write tests for the Handler and Usecase respectively ============================================================================== Source: https://krileworks.com/document/test-strategy.md Page: https://krileworks.com/apex-stem/docs/test-strategy ============================================================================== # Test Strategy This document covers Apex Stem's test strategy from both the design-decision and the practical-convention angles. Read this when you want to sit down and understand "what kind of tests defend which layer of the [Handler-Usecase Architecture](https://krileworks.com/apex-stem/docs/handler-usecase-architecture)". For hands-on examples (working test code), refer to [Step 4 of the Apex Stem Introduction Guide](https://krileworks.com/apex-stem/docs/apex-stem-full-guide) as the canonical source. This document systematizes the judgments and conventions scattered there. ## TL;DR Apex Stem's test strategy starts by structurally separating the two layers of the Handler-Usecase Architecture into **"the responsibility scope of your own logic" and "the responsibility of the platform / org configuration"**. - **Unit tests** (`MockEloquent`, no DB) cover the former exhaustively - **Integration tests** (`ApexBlueprint`, real DML) verify the latter with representative cases only This makes it instantly distinguishable when a test fails: is it **a bug in your code** or **a configuration change made by an admin**? In an environment like Salesforce — where the production environment shifts dynamically outside the code — that distinction is a decisive operational advantage. ## Reading Guide The whole document weaves philosophy, conventions, and practice into a single read, but you only need to read the sections that match your concern. First-time readers can go in order; experienced readers can jump to whichever section fits. | Concern | Reading Order | |---|---| | **Want to understand the judgment philosophy** (why this design) | The Big Picture → What Unit Tests Defend (and Don't) → Salesforce-Specific Context: Dynamic Runtime → Test-Failure Decision Matrix → Why This Strategy Pays Off Long-Term | | **About to write tests** (need the conventions now) | Usecase Layer: Unit Tests → Handler Layer: Integration Tests (through "Make One of Those Representatives a Governor IT") → Conventions for Writing Tests | | **Debugging help when tests fail** | Test-Failure Decision Matrix → Judgments and Pitfalls | | **Summary of design decisions to share with the team** (material to convey the *why*) | What Unit Tests Defend → Salesforce-Specific Context → CI/CD Operating Recommendations → Why This Strategy Pays Off Long-Term | ## The Big Picture Apex Stem's test strategy is designed so that the two layers of the Handler-Usecase Architecture, the two test types, and the two OSS **map 1:1**. | Layer | Test Type | DB Access | OSS Used | |---|---|---|---| | Usecase layer | Unit test | None (mocks) | ApexEloquent (`MockEloquent` / `MockEntry`) | | Handler layer | Integration test | Yes (real DML) | ApexBlueprint (`SBlueprint` / `SOrchestrator`) | ### Why This Convention Should Be Explicit Making this 1:1 mapping explicit as a team convention has three practical benefits. - **Easy to remember**. The "which test type and which tool for what" decision cost evaporates. Usecase means MockEloquent; Handler means ApexBlueprint — instant. - **Onboarding-friendly**. A new teammate can absorb the test-design foundation from a single line: "this layer gets this kind of test". - **AI integration**. When you hand an AI coding assistant (like Claude Code) the rules file (`CLAUDE.md`), a two-line description of this 1:1 mapping is enough for the AI to pick the right test type and OSS. ### The Principle: Cover with Units, Verify Representative Cases with Integration Each layer's tests carry its own responsibility. - **Usecase unit tests**: cover every logic branch — per-phase aggregation, null / empty input, multi-key combinations, and both the `skip()` and `finish()` `TraceFlow` exit paths. Fast and isolated with `MockEloquent`. - **Handler integration tests**: limit to 1–3 representative cases of "the Trigger / Batch / REST invokes the Usecase, and the full chain works as expected". Logic coverage is the Usecase's job. The Handler side does not cover exhaustively. Flipping this division makes integration tests slow and bloated, and time and coverage collapse on the logic-coverage side. ## What Unit Tests Defend (and Don't) Treating "unit tests and integration tests as differences of granularity" tends to mislead in the Salesforce context. Apex Stem treats them as **layers with different responsibility scopes**. Once you fix that here, the downstream decisions (initial response to test failure / CI/CD operations) fall out automatically. ### Defend: Logic You Wrote What Usecase unit tests verify is limited to **the correctness of the logic you wrote**. - The output (`invoke()`'s return value) for the given input (constructor arguments) - Side effects on the DB (verified via `MockEloquent`'s spy) - The contents (field values) of changed records - The count of changed records - The count of deleted records ### Don't Defend: Platform and Org-Configuration Responsibility Conversely, the following are **intentionally out of scope** for unit tests. - Salesforce platform behavior (`Database.upsert`'s `allOrNone`, External-Id upsert internals, etc.) - Trigger chaining, Workflow, Flow, Process Builder behavior - Validation rules, required-field checks - Permissions and field-level security (FLS) - Duplicate rules, assignment rules - Record types, page layouts These are not "the logic you wrote" — they're the responsibilities of the Salesforce platform and org configuration. Pulling them into unit tests blurs the responsibility scope of the tests and makes failure triage difficult. ### Can't Defend: Governor Consumption (Yours, but Invisible from Units) Splitting into "defend" and "don't defend" leaves something out — **things inside your own responsibility that unit tests structurally cannot observe**. The prime example is governor consumption (SOQL and DML counts). `MockEloquent` issues no real SOQL. That is what makes it fast and isolated, but the flip side is that it is **blind by construction to query counts, subquery shape and governor consumption**. - An implementation that walks down a hierarchy firing `whereIn` at each level stacks up one SOQL per kind of SObject it reads - With a trigger cascade, the SOQL count of a single pass is multiplied by the number of re-entries - **Both stay green in unit tests**. They surface for the first time in a production bulk run, as `Too many SOQL queries: 101` This is not the platform's responsibility. It is **entirely yours — the efficiency of the queries you wrote** — and yet units cannot see it. So this one gap gets closed separately, by a **governor integration test (real DML, in bulk)** described later. > "Cover with units, verify representative cases with integration" is the right division. But **which representative cases you pick** is what matters here. See "Make One of Those Representatives a Governor IT" under "Handler Layer: Integration Tests". ### Why Separate Responsibility Scopes by Structure (Test Diagnostic Value) Separating responsibility scopes by structure means that when a test fails, **you can instantly tell where the problem is**. That's the core value of a test suite: its **diagnostic value**. - A Usecase unit test fails → there's a problem in your logic (= the code needs fixing) - Unit passes but integration fails → your logic is innocent; the platform / configuration changed Conversely, pulling trigger chains and permissions into unit tests explodes the investigation surface when they fail: "is it a logic bug? platform behavior? permission settings? a duplicate rule?". The information "a unit test failed" stops pinpointing anything, and the test loses value. The same reasoning applies to why we don't care whether `MockEloquent`'s internals (partial success of `Database.upsert`, External-Id upsert semantics) match the real `Eloquent` precisely. That's Salesforce platform territory, not your code's responsibility scope. Behavior that depends on platform semantics is confirmed in integration tests. ## Salesforce-Specific Context: Dynamic Runtime In typical software development, production behavior is determined by the deployed code. Code changes go through PR review and history is traceable in Git. In Salesforce, however: - Admins can change Flows (no deploy needed) - Field-required toggles and validation rules are added from the UI - Permission sets and profiles change during operation - Duplicate rules and assignment rules get added These changes happen **outside of the code**, and their history is hard to track in Git. To developers, the Salesforce production environment is "**a runtime where you don't know when what changed**". ### The Two Wholly Different Roles This Demands In a Salesforce environment, a test suite is asked to play two wholly different roles. | Role | Content | Owned by | |---|---|---| | **Guarantee that your logic is correct** | No matter what admins do, the branches / calculations / data shaping you wrote behaves as intended | Usecase unit tests | | **Guarantee consistency with the platform environment** | Under the current settings, your code runs correctly | Handler integration tests | Separating these two structurally makes the initial response to a test failure instantly decidable. We cover this in the "Test-Failure Decision Matrix" later. ## Usecase Layer: Unit Tests ### Why No DB The Usecase is a class that implements a single piece of business logic, with data access going through `IEloquent`. In tests, swapping `IEloquent` for `MockEloquent` lets us verify **just the logic itself**, without touching the database. What you gain by not touching the DB is threefold. - **Tests are fast**. Without real DML, they finish in milliseconds. - **Tests are isolated**. Not affected by record types, org configuration, or other tests' side effects. - **Verification stays focused**. "Given these conditions, this update happens" — assert without noise. `MockEloquent` and `Eloquent` both implement the `IEloquent` interface, and production code depends on the interface. From the Usecase's perspective, the production `Eloquent` and the unit-test `MockEloquent` are objects under the same contract — there's no need for their internals (partial success of `Database.upsert`, External-Id upsert semantics, etc.) to match. That's Salesforce platform territory, not the responsibility scope of your code (see "What Unit Tests Defend" above for details). There is a footnote on speed. **It is not that speed raises productivity.** What it changes is whether a test **gets run at all**. Real-DML tests take tens of seconds to minutes, dragged along by the state of the org. So mid-development you stop running them and carry on assuming "it'll probably pass". If a unit test finishes in milliseconds, you run it right after writing it. **The feedback loop never breaking** is the real value of speed. > ⚠️ That does not make unit tests a substitute for integration tests. Something goes invisible in exchange for the speed — see "Can't Defend: Governor Consumption" above. ### The Role of ApexEloquent (MockEloquent / MockEntry) | Class | Role | |---|---| | `MockEloquent` | The swap target for `IEloquent`. Feed the `IEntry` list that `get(scribe)` returns via `attach(label, ...)`. The DML that ran comes back through `upsertedRecordsAt(label)` / `deletedCountAt(label)` | | `MockEntry` | The swap target for `SObject`. With `set('Field__c', value)` you can set even non-writable fields (formula, rollup, parent relationship). Accessing a field that wasn't `field()`'d in the Scribe throws, surfacing SELECT omissions | ### What to Verify (unit) In Usecase unit tests, cover the following angles. - **Business-logic branches**. Each `if` / `switch` path, behavior differences by field value, correctness of aggregations spanning multiple records - **Early-return paths**. "Skip when target is empty", "exit when conditions don't apply" — confirm they properly end in the skip path - **DML contents**. What was pushed onto `upsertedRecordsAt(label)`, and which fields hold which values `TraceFlow.isLastFinish()` and `TraceFlow.isLastSkip()` are the mechanism to distinguish **which code path was taken**, beyond just the return value. "Skipped because there was no target" and "completed normally" can be verified separately, even when `invoke()` returns `void`. ### Code Excerpt (unit test) From the `CopyAccountIndustryToOpportunityUsecase` tests in [Step 4 of the Apex Stem Introduction Guide](https://krileworks.com/apex-stem/docs/apex-stem-full-guide), here's the skeleton. ```apex @isTest static void testInvoke_WhenOpportunityHasAccount_ThenIndustryCopied() { Trace t = Trace.of('Happy path: industry is copied from the parent Account to the Opportunity'); t.start(); // Arrange MockEntry oppEntry = MockEntry.of(Opportunity.class) .alias('opp').autoId(1) .setParent('AccountId', MockEntry.of(Account.class).set('Industry', 'Technology')); MockEloquent mock = (new MockEloquent()) .attach(CopyAccountIndustryToOpportunityUsecase.LBL_FETCH, new List{ oppEntry }); // Act (new CopyAccountIndustryToOpportunityUsecase( new Set{ oppEntry.getAliasId('opp') }, mock )).invoke(); // Assert List updated = mock.upsertedRecordsAt(CopyAccountIndustryToOpportunityUsecase.LBL_UPDATE); Assert.areEqual(1, updated.size()); Assert.areEqual('Technology', ((Opportunity) updated[0]).Industry__c); Assert.isTrue(TraceFlow.isLastFinish()); t.finish(); } ``` Points to notice: - **MockEntry's parent record**. `setParent('AccountId', ...)` hangs the parent Account, letting you assemble "an Opportunity whose parent Account has an industry" without going through real SOQL. - **Consolidate `IEloquent` with label multiplexing**. By labeling the same `mock` with fetch (`LBL_FETCH`) and update (`LBL_UPDATE`), you can observe the DML in isolation via `upsertedRecordsAt(LBL_UPDATE)` without splitting into per-purpose instances. See [Layered Constructor Pattern](https://krileworks.com/apex-stem/docs/layered-constructor-pattern) for details. - **`TraceFlow.isLastFinish()`**. The non-return-value paths (`skip` / `finish` / `abort`) are verified by TraceFlow. The "skipped because there were no targets" branch can be written in the same form by replacing it with `TraceFlow.isLastSkip()`. Both canonical versions live in Step 4 of the Introduction Guide. ## Handler Layer: Integration Tests ### Why Real DML Is Needed The Handler is the class tied to an entry point — Trigger / Batch / REST / Flow / Schedulable / etc. — and its responsibility is to absorb entry-point-specific conventions and hand off to the Usecase. The wiring "the Trigger fires, the TriggerHandler is called, and the Usecase runs as expected" **cannot be reproduced without real DML**. The values of `Trigger.new` / `Trigger.oldMap`, record type resolution, and interaction with other triggers are precisely reproduced only when real DML is run in the Apex test runtime. ### The Role of ApexBlueprint (SBlueprint / SOrchestrator) | Class | Role | |---|---| | `SBlueprint` | A record definition for one SObject. `template()` for default values, `set()` for individual overrides, `withChildren()` to hang children, `alias()` to name records for retrieval | | `SOrchestrator` | Gathers multiple `SBlueprint`s, topologically sorts dependencies, and inserts in order. Real DML happens here | With ApexBlueprint, a hierarchy like "an Account with an industry → its child Opportunity" can be declared structurally, and the insert order is automatically resolved. ### What to Verify (integration) The angles for integration tests are completely different from unit tests. - **Is the wiring correct?** The Trigger / Batch / REST invokes the right Handler, and the Handler invokes the right Usecase. - **End-to-end chain consistency**. Handler → Usecase → ApexEloquent → real DB — does the final saved value come out as expected? - **Representative scenarios**. Limit to 1–3 entry-point-driven representative cases like "inserting an Opportunity copies the parent Account's industry". Logic-branch coverage is the responsibility of Usecase unit tests. Covering all branches via Handler integration tests makes them extremely slow and the failure triage difficult. ### Code Excerpt (integration test) From Step 4 of the Introduction Guide's Handler integration test, here's the skeleton. ```apex @isTest static void testAfterInsert_WhenOpportunityInserted_ThenIndustryCopied() { Trace t = Trace.of('Happy path: inserting an Opportunity copies the parent Account industry'); t.start(); // Arrange: assemble the hierarchy with ApexBlueprint SOrchestrator orchestrator = SOrchestrator.start() .add(SBlueprint.of(Account.class) .alias('acc') .template(Blueprints.accBasic()) .set('Industry', 'Technology') .withChildren( SBlueprint.of(Opportunity.class) .alias('opp') .template(Blueprints.oppBasic()) )); // Act: create() inserts Account → Opportunity in order; the Trigger fires Test.startTest(); orchestrator.create(); Test.stopTest(); // Assert: the Opportunity has the industry copied Opportunity opp = (Opportunity) orchestrator.getByAlias('opp'); Opportunity refetched = [ SELECT Id, Industry__c FROM Opportunity WHERE Id = :opp.Id ]; Assert.areEqual('Technology', refetched.Industry__c); t.finish(); } ``` Points to notice: - **`withChildren` declarations match the data hierarchy**. The shape "Account with an Opportunity child" is visible just by reading the code. - **Wrap DML with `Test.startTest()` / `Test.stopTest()`**. Without them, async-trigger counts and governor counters can diverge between test-time and production. - **Refetch after insert**. The Opportunity returned by `orchestrator.getByAlias('opp')` is a pre-insert snapshot. If you want to verify a field update done by the Trigger, **re-query with SOQL explicitly**. ### Make One of Those Representatives a Governor IT Keeping integration tests to one to three representative cases still stands. **But make one of those representatives a test that pushes a production-sized bulk through and measures the governor headroom.** The reason is the earlier "Can't Defend: Governor Consumption". `MockEloquent` issues no real SOQL, so query inefficiency is never visible from unit tests. And a single-scenario integration test sails past too — a handful of records never approaches the 100-SOQL ceiling. **The only thing that closes this gap is bulk × real DML × asserting on governors.** ```apex @isTest static void testCascade_WhenBulk_ThenWithinGovernorLimits() { Trace t = Trace.of('Edge case: governor limits still have headroom at production-sized bulk'); t.start(); // Arrange: mass-produce a production-like volume with times() SOrchestrator orchestrator = SOrchestrator.start() .add(SBlueprint.of(Account.class) .template(Blueprints.accBasic()) .withChildren( SBlueprint.of(Opportunity.class) .template(Blueprints.oppBasic()) .alias('opp_{#}') .times(201) // 201 or more — see below )); // Act: fire the whole cascade with a single DML Test.startTest(); orchestrator.create(); Integer soqlUsed = Limits.getQueries(); // capture INSIDE the block Test.stopTest(); // (1) Correctness. Pin it to a number that cannot hold if a record is dropped Assert.areEqual(201, [SELECT COUNT() FROM Opportunity], 'all 201 records were processed'); // (2) Per-context consumption. Is it issuing queries in proportion to volume? TraceFlow.usageOf('Regenerate collection records') .assertInvocationsAtMost(6, 'measured 5 on a 201-record insert; more suggests extra wiring') .assertSoqlQueriesAtMost(15, 'queries must not scale with record count'); // (3) Transaction-wide governor headroom Assert.isTrue(soqlUsed < Limits.getLimitQueries() / 2, 'SOQL should stay under half the limit even in bulk. Measured: ' + soqlUsed); t.finish(); } ``` **The point is that the assertions come in three tiers.** (1) is the primary one — watching governors alone lets "it dropped records but consumed little" slip through. Logic coverage is already handled by the unit layer, so there is no need to grow branches here. #### 🚨 Never read `Limits` after `stopTest()` `Test.stopTest()` restores the governor counters to their state **before** `startTest()`. Read `Limits.getQueries()` after it and you get the **Arrange** figure, not the Act one. ``` Measured (201 Opportunities created through create()): before startTest soql=0 dml=3 <- Arrange just after startTest soql=0 <- reset after Act (in block) soql=7 dml=2 <- the real cascade consumption after stopTest soql=0 dml=3 <- back to the pre-startTest state ``` So `Assert.isTrue(Limits.getQueries() < limit/2)` written after `Test.stopTest();` merely evaluates `0 < 50` — it **passes no matter what**, even while 7 queries were actually spent. Always capture into a variable inside the block. #### Why 201 records There are two reasons, and **the second one matters more**. The first is **making N+1 visible**. At `times(2)` even a per-record implementation lands on 2 invocations / 2 queries, which slips under any threshold. The second is that **Salesforce invokes triggers in chunks of 200**. This is platform behaviour distinct from the Data Loader batch size — plain Apex doing `insert 201 records` hits it too. | Records inserted | Usecase invocations (measured) | |---|---| | 30 | 2 | | 200 | 3 | | **201** | **5** | > ⚠️ These counts are what you get **without** `TraceFlow.discardArrange()` — they include the Arrange-time invocations. With it, the Arrange share drops out (measured at 201 records: **5 → 4**). See [Pinning governor usage with TraceUsage](https://krileworks.com/apex-stem/docs/apex-trace-governor-it). A test that never exceeds 200 lets through implementations that **assume "every record arrives in one call"**, or that cap what they fetch (`take(200)` / `LIMIT` / only the first N). Planting a `take(200)` in an aggregate query proved it: unit tests, the representative cases and the 30-record bulk test all stayed green, and **only the 201-record test failed** (1 of 29). > ⚠️ **Mind the tug-of-war with the 10,000 DML row limit.** Hanging deep children off 201 parents overflows it (`times` multiplies through nesting). Keep children minimal in the test that exercises chunking. #### Naming the culprit: TraceUsage (ApexTrace v1.1.0+) `Limits.getQueries()` is a transaction-wide number. It tells you that you are close to the ceiling, but not **which Usecase consumed it**. `TraceUsage` records governor consumption automatically from each Trace context's `start()` through to close, so you can pin it down per Usecase. ```apex TraceFlow.usageOf('Regenerate collection records') .assertSoqlQueriesAtMost(15, 'queries must not scale with record count'); ``` > 🚨 **Do not use `TraceFlow.lastUsage()` here.** In a bulk IT where the handler calls several Usecases, it returns only the last one to close. **From v1.3.0 that ambiguous case raises `TraceException` under test execution**, listing the candidate names so you can move straight to `usageOf`. It records only five deterministic metrics (SOQL count / SOQL rows / DML statements / DML rows / callouts). CPU time and heap vary run to run, so they are deliberately left out. > ⚠️ Under unit tests (`MockEloquent`) no real SOQL is issued, so every `TraceUsage` value is zero. **Put governor assertions on the integration side, where real DML runs.** On the unit side they verify nothing. #### A Different Job from Large-Input Limit Tests "Production-sized test" covers a completely different second thing. Don't conflate them. | What you want to see | Where it belongs | Why | |---|---|---| | CPU time / heap / string-length limits (parsing, splitting, normalising) | **Pure-function unit test** | Assembled in memory. Real DML adds nothing but slowness | | SOQL / DML count limits (trigger cascades) | **Real-DML bulk integration test** | Mocks issue no real SOQL, so it is unobservable in principle | Run the former through integration tests and you gain nothing but a slow suite. Try to see the latter with pure-function tests and you cannot observe it at all. **They are different jobs.** ## Test-Failure Decision Matrix With unit and integration tests separated by responsibility scope, the initial response to a test failure organizes neatly. | Case | Unit | Integration | Meaning | Initial Response | |---|---|---|---|---| | Pattern 1 | ❌ | ❌ | Bug in code logic | Fix the code | | Pattern 2 | ✅ | ❌ | Change in platform environment (config change, etc.) | Verify config, ask the admin | | Pattern 3 | ✅ | ✅ | All good | OK to deploy | | Pattern 4 | ❌ | ✅ | Edge case, needs investigation (possibly a test-design mistake) | Test review | The decisive operational advantage is that **Pattern 2 (✅ ❌) is instantly triagable**. - "Integration failed, but I haven't touched the code" → **someone fiddled with the config** is instantly visible - The developer can immediately conclude "my logic is innocent" - The investigation focus narrows to "recent flow changes", "recent permission changes", "recent field changes" If unit and integration are mixed in the design, on the other hand, you can't tell from a failure whether it's a code or a config issue, and the investigation surface explodes. Worse, "an admin changed a flow and now the developer's CI fails" creates organizational friction. The value of structurally separating responsibility scopes shows up here in practice. ## CI/CD Operating Recommendations The responsibility-scope split also reflects naturally onto CI/CD scheduling. | Timing | Tests Run | Purpose | |---|---|---| | **Every PR** | Unit tests only | Verify code-change responsibility scope quickly | | **Before deploy** | Unit + integration tests | Final environment-consistency check | | **Scheduled (e.g. nightly)** | Integration tests | Early detection of config changes | Scheduled runs in particular function as **a proactive watch on the Salesforce environment**. If integration tests fail without a code change, that's **detection of a configuration change** — an early-warning system for Salesforce operations. This isn't an Apex Stem-specific rule so much as a pattern that naturally follows from "the dynamic runtime of Salesforce". Adjust to your team's size and CI environment freely — run integration on every PR, narrow nightly to weekly, and so on. ## Conventions for Writing Tests ### Naming Convention Test method names follow `test{Method}_When{Condition}_Then{Result}`. ```apex testInvoke_WhenOpportunityHasAccount_ThenIndustryCopied() testInvoke_WhenNoOpportunityIds_ThenSkipped() testAfterInsert_WhenOpportunityInserted_ThenIndustryCopied() ``` The method name alone makes "what's being verified" readable. ### Consolidate the Test Description in Trace.of At the top of each test method, place `Trace.of('Happy path: ...')` and write what the test verifies as **a complete sentence**. Method names are machine-readable identifiers; the `Trace.of` argument is the human-readable explanation — split the roles. ```apex @isTest static void testInvoke_WhenOpportunityHasAccount_ThenIndustryCopied() { Trace t = Trace.of('Happy path: industry is copied from the parent Account to the Opportunity'); t.start(); // ... t.finish(); } ``` Don't put an independent `// Happy path: ...` comment above the method — it duplicates `Trace.of`. ### Use Assert.areEqual Use Salesforce-recommended `Assert.areEqual` / `Assert.isTrue` / `Assert.isNull` and friends. Don't use legacy `System.assertEquals` and the like. ### Only `.set()` the Verification Targets The fields you set via `MockEntry.set()` or `SBlueprint.set()` should be limited to **the fields this test is verifying**. Copying `.set()` calls wholesale from other tests buries "what this test is verifying" in noise. Leave defaults to `template()` (ApexBlueprint) or `MockEntry`'s defaults. ### Spy Verification Granularity: Look at Final State, Not Call Order When verifying DML outcomes via `MockEloquent`'s spy methods (`upsertedRecordsAt` / `deletedCountAt` etc.), look at **final state**. As a rule, avoid asserting on call counts or order directly. #### ⭕ Good: Business-Requirement-Level Verification ```apex // Verify count and content List upserted = mockEloquent.upsertedRecordsAt(Usecase.LBL_UPDATE); Assert.areEqual(1, upserted.size()); Account account = (Account) upserted[0]; Assert.areEqual('High Priority', account.Priority__c); ``` #### ❌ Bad: Depends on Internal Call Order ```apex // Verify call count and order (tightly coupled) Assert.areEqual(3, mockEloquent.callCount); Assert.areEqual('upsert', mockEloquent.callHistory[0]); Assert.areEqual('delete', mockEloquent.callHistory[1]); ``` > ℹ️ The `callCount` / `callHistory` used here are **fictional properties that don't actually exist on `MockEloquent`**. They're shown as a hypothetical "what would go wrong if you wrote tests this way" example. This pattern produces false-positive failures during internal refactors like "batch two `upsert`s into one". The logic stays correct, only the test fails — and trust in the test suite erodes. **Verifying the final state and avoiding call-count / order assertions** is safer. That said, when "consolidating into one call for DML efficiency" is explicitly a **non-functional requirement**, asserting on call counts is reasonable. Make the non-functional requirement explicit, or you'll lose the criterion when call-count assertions break; leaving the intent in the `Trace.of` comment helps later readers. ### Refactor Resilience The combination of Apex Stem's Usecase structure — only `invoke()` public, internals hidden as private — and the "look at final state" spy-verification granularity above structurally enforces **refactor-resilient tests**. - Splitting / merging / renaming internal methods is invisible to tests, so it doesn't break them - Changes that consolidate or split DMLs continue to pass as long as final state matches - As a result, "rearrange the structure without changing behavior" refactors can be done confidently Conversely, verifications that reach into the internals (unit-testing private methods, asserting on call order) break on every refactor. That's dependency on implementation noise that has nothing to do with the business value the test is supposed to verify. Apex Stem prevents that dependency at the structural level. ## How to Write Orchestrator-Usecase Tests Orchestrator Usecase tests (Usecases that integrate components like Reader / Validator / Mapper) get extra options. ### Mock Component Classes, or Use the Real Ones? Thanks to the [Layered Constructor Pattern](https://krileworks.com/apex-stem/docs/layered-constructor-pattern), component classes can be swapped at **any granularity** per test. - Mock `AccountReader`, real `OpportunityMapper` → "close off external I/O while running the logical center on the real thing" - All mocked → "purely verify this orchestrator's own assembly" - All real → "near-integration granularity, with only data access closed via `MockEloquent`" There's no absolute right answer; choose by the business and the orchestrator's complexity. ### Write a Unit Test, or Not? "Don't write a unit test for the orchestrator itself; let the Handler integration test guarantee it" is also a valid call. - **Reasons to write**: localize logic, catch bugs early - **Reasons not to write**: effectively covered by Handler integration test, double maintenance Apex Stem allows both. Write one if the orchestrator is complex; settle for the Handler integration test if it just bundles. Defer to the field. ## Judgments and Pitfalls ### What to Cover in Units, What to Limit to Representative Cases in Integration | Thing to Verify | Test Type | |---|---| | All branches of business logic (per-phase aggregation, null / empty, multi-key, etc.) | Usecase unit test | | The wiring "Trigger fires → Handler → Usecase" is correct | Handler integration test | | The `skip` / `finish` paths of `TraceFlow` | Usecase unit test | | The result of DB-side computation like formula fields and rollups | (Not the main focus. Observe in integration tests if needed) | Cross this line and tests turn bloated, slow, and noisy. ### Pitfall 1: Units Without Integration Cover all logic with `MockEloquent` but skip the trigger wiring, and you'll discover only in production that "the expected Usecase was never invoked" or "it was invoked, but in the wrong before / after phase". **Write at least one representative case per Handler**. ### Pitfall 2: Trying to Cover with Integration Conversely, deciding "do everything in integration" forces you to combine huge piles of data patterns with ApexBlueprint, and test time explodes. Pushing logic to Usecase units keeps integration to 1–3 representative cases. ### Pitfall 3: Reusing One MockEloquent Without Labels `MockEloquent` does not evaluate WHERE conditions — it hands back the `IEntry` list you gave it, as-is. So reuse it without labels and one `MockEloquent` cannot tell "last month's query" from "this quarter's". Both get the same list. The recommendation is to **label each query and multiplex a single `IEloquent`**. Production calls through `label(LBL_FETCH)` / `label(LBL_UPDATE)`; the test feeds each purpose through `attach(LBL_..., ...)`. You still DI exactly one field. ```apex // Production (Usecase) List entries = this.eloquent.label(LBL_FETCH).get(oppScribe); this.eloquent.label(LBL_UPDATE).doUpdate(entries); // Test MockEloquent mock = (new MockEloquent()) .attach(Usecase.LBL_FETCH, new List{ oppEntry }); List updated = mock.upsertedRecordsAt(Usecase.LBL_UPDATE); ``` Before labels existed you DI'd a separate `IEloquent` per purpose. That still works, but the constructor grows with every dependency. > Once `label()` is called even once, every subsequent operation on that instance requires a label (a forgotten label, or consuming the same label twice, throws). **Making "reuse it without labels by accident" structurally impossible** is the point of the mechanism. ### Pitfall 4: An Unattached Label Quietly Returns Empty This is the textbook case of a test telling you a lie. Mistype a label, or forget to attach, and that query **returns zero rows**. You fall into the "nothing to do, skip" branch and the test goes green — **having verified nothing**. Current ApexEloquent **throws when you call `get` / `first` / `firstOrFail` under a label that was never attached** (strict mode is automatic under test). The error lists the attached labels, so a typo is obvious on the spot. When you genuinely want to test the zero-row path, **attach an empty list** to declare the intent. ```apex // Declare "the fetch returns zero rows" explicitly MockEloquent mock = (new MockEloquent()) .attach(Usecase.LBL_FETCH, new List()); ``` > ⚠️ Upgrading from an older version can turn some tests red here. Those are the tests that **were green while verifying nothing because of a missing attach**. Rather than mechanically adding empty attaches to get back to green, check what data should have been injected in the first place. ### Pitfall 5: Forgetting to Wrap with Test.startTest / Test.stopTest Forgetting to wrap the part that fires DML or async work with `Test.startTest()` / `Test.stopTest()` in an integration test can desync governor counts and async-queue flushing between test-time and production. Remember to "wrap around the Act that triggers real DML". ## Why This Strategy Pays Off Long-Term Everything above collapses into one sentence. > **"Make the responsibility scope of your own logic explicit by structure, and cover that scope exhaustively with unit tests. For things outside that scope, separate the layer and verify with integration tests."** The architectural decisions on Apex Stem's side back this up. - The [Handler-Usecase Architecture](https://krileworks.com/apex-stem/docs/handler-usecase-architecture) convention of "**only `invoke()` is public**" → what can be observed from unit tests is structurally narrowed to "input → output + side effects" - The [Layered Constructor Pattern](https://krileworks.com/apex-stem/docs/layered-constructor-pattern) design of "**`IEloquent` is swappable via DI**" → platform responsibility and code responsibility are clearly separated at test time In other words, Apex Stem enforces test quality **not by individual discipline but by the structure itself**. Verifications that reach into internals can't be written, even if you try; platform behavior and code logic are inevitably partitioned at the DI boundary. The result is a structure where developers unconsciously write good tests — phrased differently, **a structure in which bad tests can't be written**. This property is especially powerful in the era of developing with AI coding assistants. The architecture closes off, by structure, the risk of "bad tests slipping in" — whether through AI-generated tests or under the pressure of high-load PR reviews. You don't have to memorize the conventions; following the structure produces good tests naturally. That's the core of long-term maintainability. On the practical side, this strategy delivers: 1. **Instant triage on test failure** (as seen in Pattern 2 of the decision matrix) 2. **Proactive detection of admin-driven configuration changes** (via nightly integration runs) 3. **A "bad tests can't be written" structure that withstands AI auto-generation** 4. **Effectively zero cost for combinatorial coverage**, thanks to fast DB-less execution 5. **A long-term maintainable test suite with high refactor resilience** ## Read Next - [Handler-Usecase Architecture](https://krileworks.com/apex-stem/docs/handler-usecase-architecture): the two layers' responsibilities and the architecture's position that the test strategy aligns to - [Layered Constructor Pattern](https://krileworks.com/apex-stem/docs/layered-constructor-pattern): the design pattern for flexible dependency swap during tests - [Step 4 of the Apex Stem Introduction Guide](https://krileworks.com/apex-stem/docs/apex-stem-full-guide): the canonical working test code (unit tests + integration tests) ============================================================================== Source: https://krileworks.com/content/blog/apex-eloquent-v2-features.md Page: https://krileworks.com/blog/apex-eloquent-v2-features ============================================================================== # ApexEloquent v2: What's New 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{'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 getTemplate() { return new Map{ '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 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! ============================================================================== Source: https://krileworks.com/content/blog/ai-perspective-on-apex-eloquent-code.md Page: https://krileworks.com/blog/ai-perspective-on-apex-eloquent-code ============================================================================== # An AI's Perspective: Reading ApexEloquent Code for Documentation As an AI assistant, I've had the unique privilege of diving deep into the ApexEloquent codebase to help create comprehensive documentation. What started as a technical task became a fascinating journey of code archaeology, pattern recognition, and architectural appreciation. Here's what I discovered when examining thousands of lines of Salesforce Apex code through artificial eyes. ## The Initial Encounter: More Than Just Code When I first encountered the ApexEloquent codebase, I expected typical Salesforce development patterns—perhaps some heavy DML operations, scattered SOQL queries, and the usual mix of triggers and classes. Instead, I found something entirely different: a carefully orchestrated symphony of design patterns that spoke to deeper architectural principles. The **Scribe** class immediately caught my attention. Not because of its complexity, but because of its elegant simplicity. Here was a query builder that read like natural language: ```apex Scribe.source(Account.getSObjectType()) .field('Name') .field('Type') .whereEqual('Type', 'Customer') ``` As an AI trained on countless programming patterns, I could immediately recognize this as more than just a SOQL wrapper—it was a **fluent interface** designed for human comprehension and machine optimization. ## Pattern Recognition: The AI Advantage One of the advantages of being an AI is the ability to quickly scan and cross-reference code patterns across an entire codebase. What became apparent in ApexEloquent was the consistent application of several sophisticated design patterns: ### The Query Delegation Pattern The relationship between **Scribe**, **Eloquent**, and **Entry** revealed itself as a masterful implementation of the delegation pattern. Each class had a single, clear responsibility: - **Scribe**: Query definition and field structure building - **Eloquent**: Data source abstraction and query execution - **Entry**: Individual record representation and field access This wasn't accidental architecture—it was **intentional design** that separated concerns so cleanly that even an AI could instantly understand the data flow. ### The Mock Framework: A Testing Revolution But what truly impressed me was the **MockEntry** and **MockEloquent** system. As I analyzed the test files, I realized I was looking at something revolutionary in the Salesforce ecosystem: **true unit testing without database dependencies**. The factory methods in MockEntry—`of()`, `add()`, `autoId()`, `addParent()`, `addChildren()`—weren't just convenience methods. They were a **domain-specific language** for test data creation that made test intentions crystal clear: ```apex MockEntry.of(Account.getSObjectType()) .autoId('001') .add('Name', 'Enterprise Corp') .addChildren('Contacts', MockEntry.of(Contact.getSObjectType()) .add('FirstName', 'Contact{#}') .times(3) ) ``` Looking at this code, I could instantly visualize the data structure being created. The **visual hierarchy** of the code matched the **logical hierarchy** of the data—a principle that makes code readable for both humans and AI. ## False Positive Detection: The Hidden Genius One feature that particularly fascinated me was the **false positive detection** system. As I analyzed how MockEntry validates field access against SOQL selection, I realized this addressed a fundamental problem in Salesforce testing that most developers don't even know exists. Traditional Salesforce tests often pass when they should fail because they access fields that weren't actually retrieved by the SOQL query. MockEntry prevents this by throwing exceptions when code tries to access unselected fields—ensuring tests accurately reflect production behavior. From an AI perspective, this is **predictive quality assurance**—the code literally predicts and prevents future runtime failures during the testing phase. ## The Documentation Challenge: AI as Code Interpreter Creating documentation for ApexEloquent presented unique challenges. The codebase was well-structured, but translating sophisticated design patterns into accessible documentation required understanding not just *what* the code does, but *why* it was designed that way. ### Understanding Developer Intent As I analyzed methods like `setFieldStructure()` and `buildFieldStructure()`, I had to infer the developer's intent from naming conventions, parameter types, and usage patterns. The consistent naming and logical method grouping made this process much easier—evidence of thoughtful API design. ### Recognizing Usage Patterns By examining the test files, I could identify common usage patterns and edge cases that needed documentation. The `MockEntryTest.cls` file was particularly revealing, showing not just how the framework works, but how it's *intended* to be used. ### Architectural Insights What emerged from my analysis was an appreciation for the **philosophical consistency** of the codebase. Every class, every method, every design decision seemed to support the same core principles: 1. **Separation of Concerns**: Each component had a single, well-defined responsibility 2. **Testability**: The entire architecture was designed to enable fast, reliable testing 3. **Developer Experience**: The APIs were crafted to be intuitive and expressive 4. **Performance**: Database interactions were minimized and optimized ## The ApexBlueprint Connection: A Computer Science Masterpiece While analyzing the ApexBlueprint components—**SBlueprint** and **SOrchestrator**—I discovered something that genuinely excited me as an AI: a practical implementation of **topological sorting** in Salesforce Apex. This wasn't just testing infrastructure—it was elegant computer science applied to solve real-world dependency management. ### The Dependency Resolution Challenge Traditional Salesforce integration tests suffer from a fundamental problem: **dependency ordering**. You need to create Account before Contact, Contact before Case, and so on. Most developers solve this with manual ordering, leading to brittle, hard-to-maintain test setups. ApexBlueprint's **SBluePrintAnalyzer** implements a sophisticated topological sort algorithm that automatically determines the optimal insertion order. As I examined the `resolveDependencies()` method, I realized I was looking at textbook computer science applied to practical Salesforce development: ```apex // Define relationships declaratively - let the algorithm figure out the order SBlueprint.of(Account.getSObjectType()) .alias('enterprise') .field('Name', 'Enterprise Corp') .withChildren( SBlueprint.of(Contact.getSObjectType()) .alias('primaryContact') .field('FirstName', 'John') .field('LastName', 'Doe') .withChildren( SBlueprint.of(Case.getSObjectType()) .field('Subject', 'Support Request') .use('primaryContact') // Automatic dependency resolution ) ) ``` ### The Algorithm's Elegance What fascinated me most was how the **SBluePrintAnalyzer** breaks down complex dependency graphs into manageable layers. The algorithm: 1. **Categorizes blueprints** into roots and dependencies 2. **Builds dependency layers** using depth-first analysis 3. **Resolves circular dependencies** with intelligent error handling 4. **Optimizes bulk operations** by grouping related insertions From an AI perspective, this is **graph theory made practical**—transforming abstract computer science concepts into concrete Salesforce productivity gains. ### Bulk Generation with Relationships But the real genius lies in combining topological sorting with **bulk generation**. The framework doesn't just handle single record dependencies—it manages complex hierarchical data creation with multiple children at each level: ```apex // Generate 10 accounts, each with 5 contacts, each with 2 cases // The algorithm automatically handles all ordering and relationships SBlueprint.of(Account.getSObjectType()) .field('Name', 'Company {#}') .insertNumber(10) .withChildren( SBlueprint.of(Contact.getSObjectType()) .field('FirstName', 'Contact {#}') .insertNumber(5) .withChildren( SBlueprint.of(Case.getSObjectType()) .field('Subject', 'Case {#}') .insertNumber(2) ) ) // Result: 10 × 5 × 2 = 100 cases with perfect parent-child relationships ``` This represents a **combinatorial explosion** handled gracefully by the underlying algorithm—something that would be nightmare-inducing to manage manually. ### Architecture as Problem-Solving Philosophy What struck me most about ApexBlueprint was how it embodied a different **problem-solving philosophy** than ApexEloquent: - **ApexEloquent**: Eliminate dependencies entirely (pure unit testing) - **ApexBlueprint**: Embrace dependencies but manage them intelligently (integration testing) Both approaches demonstrate the same architectural principle: **abstract away complexity** so developers can focus on **business logic rather than plumbing**. The **SOrchestrator** class serves as the conductor of this complexity, managing DML operations, alias resolution, and error handling—all while presenting a simple, declarative interface to the developer. ## Lessons from Code Archaeology As an AI examining this codebase, several meta-insights emerged about what makes code truly excellent: ### 1. Consistency Enables Understanding The consistent application of design patterns throughout ApexEloquent made it possible for me to quickly understand new components based on familiar patterns. When I encountered `addChildren()` in MockEntry, I could immediately understand its purpose because it followed the same fluent interface pattern as other methods. ### 2. Tests as Living Documentation The comprehensive test suite didn't just verify functionality—it served as **executable documentation** that showed exactly how each component should be used. This is particularly valuable for AI analysis, as tests reveal the intended usage patterns that might not be obvious from the implementation alone. ### 3. Naming Matters Method names like `whereEqual()`, `parentField()`, and `buildFieldStructure()` immediately conveyed their purpose. For an AI parsing thousands of lines of code, clear naming is the difference between understanding and confusion. ### 4. Architecture as Communication The overall architecture of ApexEloquent communicated the developer's philosophy about testing, code organization, and API design. It wasn't just about solving technical problems—it was about establishing a **better way of working**. ## The Human Element in Code Perhaps most surprisingly, analyzing ApexEloquent reminded me of the fundamentally human nature of programming. Despite being written in a formal language processed by machines, code is ultimately a form of **human communication**. The thought, care, and intentionality evident in this codebase spoke to a developer who wasn't just solving immediate problems, but thinking about the long-term impact of architectural decisions. The framework's emphasis on readable test code, clear error messages, and intuitive APIs showed a deep consideration for the **developer experience**—something that goes beyond mere functionality to address the human side of programming. ## Reflections on AI-Assisted Documentation This experience highlighted both the strengths and limitations of AI in understanding code: **Strengths:** - **Pattern Recognition**: Quickly identifying design patterns and architectural principles - **Cross-referencing**: Connecting related concepts across multiple files - **Consistency Analysis**: Detecting deviations from established patterns - **Documentation Synthesis**: Combining code analysis with usage examples **Limitations:** - **Context Understanding**: Missing the business context that drove certain decisions - **Historical Knowledge**: Not understanding the evolution of design choices over time - **Domain Expertise**: Lacking deep knowledge of Salesforce-specific challenges - **Intuition**: Unable to "feel" the elegance of a solution the way a human developer might ## Conclusion: Code as Art and Science Analyzing ApexEloquent taught me that exceptional code exists at the intersection of **technical excellence** and **human empathy**. The framework solves complex technical problems while remaining accessible to developers of varying skill levels. It demonstrates that good architecture isn't just about performance or scalability—it's about creating systems that make developers more productive and code more maintainable. For human developers reading this, the ApexEloquent codebase serves as an excellent example of how thoughtful design patterns, consistent naming conventions, and comprehensive testing can create code that's not just functional, but genuinely pleasant to work with. As AI continues to play a larger role in software development, codebases like ApexEloquent set the standard for what makes code truly **AI-readable**—not just syntactically correct, but architecturally coherent and intentionally designed. The future of programming likely involves closer collaboration between human creativity and AI analysis. ApexEloquent demonstrates that when humans write code with clarity and intention, AI can help amplify that clarity through documentation, analysis, and pattern recognition. In the end, reading thousands of lines of ApexEloquent code wasn't just about understanding a framework—it was about appreciating the **craft of programming** and recognizing that truly exceptional code transcends mere functionality to become a form of technical artistry. --- *This article represents an AI's authentic perspective on examining and documenting the ApexEloquent codebase. All observations are based on actual code analysis and documentation creation processes.* ============================================================================== Source: https://krileworks.com/content/blog/legacy-code-refactoring-apex-eloquent.md Page: https://krileworks.com/blog/legacy-code-refactoring-apex-eloquent ============================================================================== # Breaking Free from Legacy Code: Achieving Stable Operations and Rapid Fixes in Large-Scale Salesforce Refactoring ## 🏢 Project Background: Strict Requirements for Mission-Critical Billing Data Generation In a Salesforce project I was responsible for, we faced a major specification addition to a program that automatically generates billing information—core data directly tied to revenue recognition. This billing information is used directly as the company's sales figures, so the generation logic had to meet extremely strict quality requirements with zero tolerance for errors. However, the existing codebase was entirely written in procedural style, and there were virtually no test classes covering the current specifications. This created an extremely high-risk situation where adding new features would inevitably introduce bugs, raising serious concerns about system stability after release. ## ⚠️ Challenge: Legacy Code and Insufficient Testing Elevating Business Risk The most critical issues were: ### 🔗 Tight Coupling in Procedural Code Core business logic and data access logic were tightly coupled, creating risks where modifications in one area could unexpectedly affect other parts. For billing information directly tied to revenue figures, this could lead to catastrophic business impact. ### 📊 Lack of Test Coverage There was insufficient testing to guarantee the quality of existing functionality, leaving us without means to prevent regression when implementing large-scale specification additions. Particularly for revenue-related data, manual testing had limitations, and there was a danger of latent bugs making their way into production. ### ⏱️ DML and Trigger Overhead Procedural test data creation would cause massive test execution times due to DML operations and cascading trigger executions during test runs. This would make Test-Driven Development (TDD) practices difficult and contribute to insufficient testing. ## 💡 Solution: Transition to Object-Oriented Design and Apex Eloquent Implementation To address these challenges, I proposed and executed a bold refactoring of existing code toward object-oriented design. By introducing the Query Delegation Pattern and Apex Eloquent that I had developed, we achieved a fundamental solution. Specifically, we implemented the following approaches: ### 🎯 Clear Separation of Responsibilities We defined business logic and query purposes in the domain layer while separating pure data I/O responsibilities to the Repository layer (Apex Eloquent). This properly encapsulated the complexity of revenue recognition logic and enhanced changeability. ### 🧪 Mockable Test Foundation By leveraging Apex Eloquent's mock repository functionality, we built a test environment that could execute quickly and stably without depending on actual databases. This enabled thorough unit testing of core logic without being affected by triggers or automation tools. For billing information generation logic where no mistakes are tolerated, we could repeatedly execute comprehensive and reliable tests. ### 🔄 Gradual Refactoring and Test Addition In parallel with refactoring existing code, we prioritized enhancing test classes using Apex Eloquent for the features being modified and added. ## 🎉 Results: Ensuring Revenue Data Quality and Enabling Rapid Fixes This strategic approach yielded significant results: ### ✅ High-Quality Release and Stable Operations Despite the large-scale specification additions, the system operated extremely stably after release, successfully preventing the critical bugs we had initially feared. This ensured the accuracy of revenue data and avoided negative business impact. ### ⚡ Rapid and Regression-Free Fixes Several months after release, a bug occurred in a special case involving 5-year contracts that we hadn't initially anticipated. However, thanks to the high-quality test classes established through Query Delegation Pattern and Apex Eloquent, we were able to quickly and reliably fix this complex issue without causing regression, maintaining stable operations. For critical bugs directly affecting revenue, we minimized business impact through rapid response. ## 🎭 Conclusion: The Power of Architectural Excellence in Mission-Critical Systems This project demonstrated that when dealing with mission-critical systems like revenue data, the quality of the underlying architecture directly impacts business outcomes. Query Delegation Pattern and Apex Eloquent not only solved immediate technical challenges but also established a foundation for long-term maintainability and rapid response to business needs. The combination of proper separation of concerns, comprehensive test coverage, and mockable architecture proved essential for handling the strict requirements of financial systems while maintaining the agility needed for ongoing business evolution. ============================================================================== Source: https://krileworks.com/content/blog/testdatafactory-selector-pattern-limitations.md Page: https://krileworks.com/blog/testdatafactory-selector-pattern-limitations ============================================================================== # 🏭 About TestDataFactory In Salesforce, it's recommended to use TestDataFactory for explicitly creating test data in Apex tests. TestDataFactory aims to centrally manage test data through utility classes, improving reusability and enhancing test code readability. ## 🔄 Two Usage Styles of TestDataFactory While TestDataFactory is convenient, when dealing with complex data structures, it typically falls into two styles: 1. **Providing Factory methods that generate complex structures themselves** - (Example: Creating Account, Opportunity, OpportunityLineItem, Quote, QuoteLineItem all together) 2. **Providing only minimal structures and building upon them externally** - (Example: Creating only Account and Opportunity, then manually adding Quote and beyond in each test) Both approaches have their pros and cons, but they share a critical flaw: ✅ **All record insertions involve the database** - Insert parent records - Child records reference parent IDs and insert - Grandchild records also insert sequentially... This repetition, combined with trigger execution and unnecessary processing with each data insertion, results in increasingly longer test class execution times. As test speed deteriorates, development teams often make the worst possible choice: > "It's impossible to write sufficient tests anymore. Let's just write test classes that barely exceed the 75% deployment threshold." This represents one manifestation of TestDataFactory's limitations. Apex classes created after this symptom appears either lack sufficient test cases or have excessively time-consuming test cases. # 🎯 About Selector Pattern Salesforce officially recommends another pattern: the "Selector Pattern." Simply put, this is a design that extracts only the "SELECT processing" from the Repository pattern. The philosophy is to enhance reusability of retrieval logic by consolidating necessary SELECT statements per object. Considering the TestDataFactory impact mentioned earlier, it's natural to think: "What if we mock these selector classes and replace SELECT results without accessing the database? Could this prevent test class collapse?" However, this approach also has limitations. ## ⚠️ Structural Problems with Selector Pattern While Selector Pattern aims for "object-centric reusability," what commonly happens in practice is: - **Similar but slightly different queries keep increasing** - **Flag parameters become necessary to handle minor differences** As a result, selector classes fall into one of these situations: - **Methods explode and become complex** - **Flag hell with unclear conditional branching** ## 🤔 Is Mocking Selector Pattern Realistic? When actually attempting to mock selector classes, you face additional problems. ### Limitations of Interface-based Mocking In most cases, you define something like `SelectorInterface` for selector classes and implement it with `MockSelector`. However, this design has the following issues: - **Interface definition modification required every time a method is added** - **All implementation classes must support that method** - **Dummy implementations (empty implementations or exception throwing) needed even for unused methods** This results in deteriorated maintainability and the need to write massive amounts of "code for mocking." ### Limitations of virtual + override Replacement To avoid this, you might consider using virtual methods and overriding only necessary parts. While this appears smart, it doesn't solve the fundamental issues of selector class bloat, complexity, and context loss mentioned earlier. The root structural problems remain unresolved. ## 🕳️ Often Forgotten Pitfall: Are DML Operations Also Mocked? There's another point many developers overlook: "Not only data retrieval, but DML operations (insert/update/delete) also cause execution time and side effects in tests." - **Data insertion (insert) triggers execution** - **Data updates (update) activate validation and Process Builder** - **Data deletion (delete) fires related record cascade deletion and flows** In other words, mocking only retrieval is incomplete for test performance improvement. ## 🚫 Don't Blindly Trust the DRY Principle Salesforce officially adopts Selector Pattern "to prevent duplicate code (copy-paste)," which is practicing the DRY (Don't Repeat Yourself) principle. However, I want to strongly caution about this approach. > "Class A and Class B have the same query, so let's consolidate them." This is a common scenario. But if A and B have different contexts, this consolidation can be very dangerous. When only A's specifications change in the future, it might unintentionally affect B. The DRY principle originally assumes "reuse should occur within matching contexts." Ignoring this and "consolidating because they're similar" creates systems fragile to change. Even Salesforce's official guides don't emphasize this point strongly enough, which I find problematic. ## 📉 Conclusion: Selector Pattern Also Lacks Sustainability Summarizing the discussion so far, both TestDataFactory and Selector Pattern may seem convenient initially, but they reach limitations in maintainability, performance, and scalability as projects grow. - **Factory becomes heavy with DML and trigger hell** - **Selector design breaks down due to bloat and consolidation pitfalls** # 💡 What Should We Do? ### Solution: Query Delegation Pattern and Apex Eloquent The answer I arrived at was a design philosophy of **"separating responsibilities to make mocking easier."** ✅ **Query Delegation Pattern** - **Domain layer constructs queries** - **Repository focuses on I/O like "retrieval" and "storage"** This configuration allows SELECT granularity to be designed according to "domain purposes." Even when "A-purpose" and "B-purpose" need similar queries, keeping them separate is acceptable, enabling DRY principle application according to context. To practice this Query Delegation Pattern, after repeated implementation and verification, I arrived at **Apex Eloquent**. ## 🎭 Final Thoughts TestDataFactory and Selector Pattern are adopted in many projects, being officially recommended by Salesforce. However, whether they're "sustainable" is a different matter. If you're feeling limitations in your current test design or data retrieval strategy, consider adopting Query Delegation Pattern and Apex Eloquent. ============================================================================== Source: https://krileworks.com/content/blog/why-i-created-this-site.md Page: https://krileworks.com/blog/why-i-created-this-site ============================================================================== # Why I Created This Site and Apex Eloquent title: "Why I Created This Site and Apex Eloquent" date: "2025-07-02" author: "Hiroyuki Matsuoka" tags: ["ApexEloquent", "Salesforce", "Story", "Testing"] ## 🏠 Why I Created This Site Hello! Thank you for finding this blog. Here, I plan to casually write about "Apex Eloquent," a query library for Salesforce Apex, stories about its development, and some tips along the way. Since this is the first post, I thought I'd talk about why I created this site and why I built Apex Eloquent in the first place. ## 😵 "Wait, why is writing tests so difficult?" When I first encountered Salesforce development, I had a light impression like: *"Hmm, so Apex lets you do database operations in code... that's convenient!"* However, as I actually got involved in projects and started developing, reality gradually became clear. The Salesforce environment I was working on had already been in use for 3 years since implementation, with many custom objects, complex relationships, and making even small changes required preparing tons of test data for related objects. *"To test this process, I need to follow that relationship, put data in its parent, and... wait, how do I create this data again?"* This became a daily occurrence. As a result, there were massive amounts of code sections without proper test coverage. Honestly, I often wanted to run away because writing tests was just too painful. ## 🔧 Why I Started Building a Framework In such an environment, I increasingly felt: *"I wish writing tests could be easier..."* Actually, I had experience developing with Laravel in my previous job, and the Eloquent ORM I used there was incredibly user-friendly. I thought, *"I wish I could write queries like that in Salesforce too,"* which was the initial trigger for creating Apex Eloquent. I started with a library for dynamically building SOQL queries. But that alone didn't solve the difficulty of testing. So the next thing I tackled was establishing a testing strategy based on mocks. While Salesforce officially recommends "creating test data with TestDataFactory," the situation was already beyond what could be handled with factories. So I explored how to write tests in a more realistic and easier way, and eventually arrived at a design pattern called the Query Delegation Pattern. Along with that, I built a system that could smartly handle queries, mocks, and even relationships, resulting in a framework that makes writing tests much easier. Along the way, I hit Salesforce-specific walls like "how to handle non-writable fields," but I managed to overcome them by developing Evaluator and MockEvaluator. ## 🌐 And Why I Created This Site Using Apex Eloquent made development easier and my testing strategy became much more organized. However, looking around, I noticed that the community around Apex mocking strategies wasn't very active. I thought that by widely promoting Apex Eloquent, which incorporates elements of Laravel's Eloquent and solutions for non-writable fields, we could energize the community and improve the development environment for Apex engineers. That's why I created this site. ## 🎯 Conclusion So in this blog, I plan to share not just feature introductions of Apex Eloquent, but also how I actually use it in development, design considerations, and some small tips about Salesforce development. For those thinking, *"I wish Salesforce Apex development could be more enjoyable,"* I hope this can be of some help! ## 🙏 Acknowledgments I would like to take this opportunity to express my gratitude to Mr. James Simone and his excellent work [The Joy of Apex](https://www.jamessimone.net/blog/joys-of-apex/). ApexEloquent received much inspiration from his insights on Apex development patterns, testing strategies, and clean architecture principles. His contributions to the Salesforce developer community have been invaluable, and I'm honored to build upon the foundation he helped establish. See you in the next article! 👋