ApexTrace
The log you already write becomes what your test asserts on.Pin the path taken, on an axis separate from side effects.
A line written for the production debug log becomes the assertion that answers "did this Usecase really take that path?". No instrumenting twice, once for logs and once for tests.
private Trace t = Trace.of('Copy the parent account industry'); public void invoke() { this.t.start(); if (this.opportunityIds.isEmpty()) { this.t.skip('No target opportunities, exiting.'); return; } // ... business logic ... this.t.finish('Copied onto ' + entries.size() + ' records.');}// These 3 lines are the production log AND the test assertion.Whether it left through skip or ran all the way tofinish is readable from the test side viaTraceFlow.
Three blind spots, one log
This is not a list of three features. It is three blind spots that one lifecycle log closes at once.
"It is not working in production. How far did it get?"
Scatter System.debug around and the format and granularity vary per author, leaving nothing you can follow afterwards. The label in Trace.of('...') is attached to every line automatically, so which Usecase stopped where is readable from the log alone.
"It skipped, correctly. But there is no way to confirm that."
With a void return, verifying this means either distorting the return type or adding a @TestVisible field, and test-only code bleeds into production. Place one t.skip() line and it is pinnable throughTraceFlow.isLastSkip().The log you leave for future debugging is itself the verification route.
"Ran it in bulk and hit 101 SOQL queries"
An implementation whose queries scale with record count sails through a one-record test. ApexTrace records governor usage per Usecase, so the insurance that "it does not explode in bulk" goes into an integration test as a single loose ceiling.
Your tests are green.That is not the same as verified.
A Usecase returning void has an awkward property:it passes tests most easily when it did nothing at all. Early-return with zero DML and the assertion that "verifies the saved records" is justmatching zero against zero, going green without confirming anything. ApexTrace adds a second axis, the exit path, independent of side effects, so a test that was swinging at air fails the moment it swings at air.
How it reads
The same test — "it skips when there are no targets" — verified on the left with side effects only, on the right with the path too. The pink lines are an assertion that proves nothing; the green lines are the TraceFlow that makes it real.
"It is zero" holds whether it skipped, processed and found zero, or aborted on an exception. It cannot tell three different outcomes apart.
isLastSkip() admits exactly one of the three. Pin the reason message too and you confirm it took the branch you meant.
MockEloquent mock = new MockEloquent(); (new CopyIndustryToOpp( new Set<Id>(), mock)).invoke(); // Nothing was savedAssert.areEqual( 0, mock.upsertedRecordsAt(LBL_UPDATE).size()); // Skipped, processed-zero, or aborted:// this assert passes either wayMockEloquent mock = new MockEloquent(); (new CopyIndustryToOpp( new Set<Id>(), mock)).invoke(); // Nothing was savedAssert.areEqual( 0, mock.upsertedRecordsAt(LBL_UPDATE).size()); // AND it took the skip pathAssert.isTrue(TraceFlow.isLastSkip());Assert.isTrue( TraceFlow.contains('No target opportunities'));There are three exit paths: finish (completed),skip (exited normally without doing anything) andabort (did not complete). Keep them distinct and your tests catch both "thought it completed but it skipped" and "should have skipped but it ran" (verifying execution paths with TraceFlow).
A ceiling, not a budget
This is not a tool for deciding how many SOQL queries a Usecase ought to use. It isinsurance: it pins one thing, that running in bulk has not let inan implementation firing a query per record (N+1), and it pins it with a loose ceiling.
Running production-sized bulk through ApexBlueprint and deliberately degrading the implementation to query per record made it firebefore 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: 0Invocations is 2 while SOQL is 32.That single line separates "extra wiring" from a loop in the body.usageOf(name) pulls a Usecase out by name, so you can pin the one you care about even when the handler calls three. Put discardArrange() on the Arrange / Act boundary and the measured window narrows to the Act alone.
Asynchronous work (batches / Queueables) cannot be measured by Limits in principle.It only starts running at Test.stopTest(), and the counters then revert to their startTest() values. TraceUsage diffs at context boundaries, so neither affects it.
One record. Three readers.
The API is four methods on Trace. Neither path verification nor governor measurement is something you add on top. Once you have writtenstart and an exit, three readers consume that same single record for three different purposes.
Debug logThe person reading in production. Labelled START / SKIP / FINISH lines, each with its reason.
TraceFlowThe mechanism reading in tests. It extracts which path the context closed on, from the same record.
TraceUsageIt reuses that same start-to-exit span as the measurement window for governor usage.
A by-product of this structure is that lifecycle inconsistencies themselves fail your tests. Return without calling an exit and an unbalanced "still open" trace is left behind; under test, Strict mode is on automatically and it raises immediately. In production, Relaxed mode absorbs it, so a logging slip never stops the business.Strictness in tests, tolerance in production(nested traces and the two modes).
Ordinary logs lie because nobody reads them. They are written and forgotten, and next read during an incident. By then the code has been refactored many times, and a line saying "skipped" while the code actually processed is entirely normal.Nothing breaks, so nobody notices.The moment you put one assertion on it, that log line acquires a consumer. With a consumer, drift surfaces as a failure. It is guaranteed by structure, not by discipline.
Position in Apex Stem
ApexTrace is the Lifecycle Logging member of the four OSS libraries that make upApex Stem. It lives in the Usecase layer of theHandler-Usecase Architecture, and across both unit and integration tests it takes on the "path" and "governor headroom in bulk" axes of thetest strategy.
- ApexTrace guide: the four Trace methods, TraceFlow, TraceUsage, nesting and the two modes
- Test Strategy: where path verification belongs, and how it divides between unit and integration
- False-positive detection guide: the other safety net (SELECT-omission detection on the ApexEloquent side)
- Handler-Usecase Architecture: the design of the Usecase layer where Trace lives
Start with the Developer Guide
Instrumenting a Usecase, verifying the path with TraceFlow, and measuring governors with TraceUsage, followed through in real code.