Nested Traces and the Two 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.
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 (changeModeToandTraceModeare bothprivate).
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.
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.
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 returns 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.
@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: the four methods and the three exit paths
- Verifying execution paths with TraceFlow: the inconsistencies Strict mode surfaces
- ApexTrace guide: back to the guide index