ネスト Trace と 2 つのモード

Apex Stem ドキュメント
Apex StemApexTraceInternalsSalesforceApex
Usecase が Usecase を呼ぶときの LIFO 順序ルールと、テストの Strict モード / 本番の Relaxed モードの違いをまとめます。

ネスト Trace と 2 つのモード

Usecase の中から別の Usecase を呼ぶような場面では、Traceネスト します。ApexTrace はネスト構造を LIFO スタックで厳密に管理し、不整合があれば例外を投げます。

Outer / Inner の順序ルール

ネスト Trace は「Inner を先に閉じてから Outer を閉じる」が原則です。

APEX
Trace outer = Trace.of('Outer'); outer.start();
  Trace inner = Trace.of('Inner'); inner.start();
    inner.log('inside inner');     // ✅
  inner.finish();                  // ✅ Inner を先に閉じる
outer.finish();                    // ✅

Inner が start した後、finish / skip / abort のいずれかで閉じる前に Outer の log / finish を呼ぶと、Strict モードでは即例外が投げられます。

Strict モードと Relaxed モード

TraceFlow には 2 つのモードがあり、実行コンテキストで自動切替されます。

モードデフォルトの切替挙動
Strictテスト実行時 (Test.isRunningTest() == true)ネスト不整合・順序違反で即例外。バグを早期検出する
Relaxed本番実行時多少の不整合は 自動で abort して吸収。本番が落ちにくくなる

「テストで通った = 本番でも通る」と思いがちですが、モード差で挙動が変わることを意識しておきます。

⚠️ 切替は Test.isRunningTest() による自動判定のみです。 利用者から明示的に切り替える API はありません (changeModeToTraceMode はどちらも private)。

Trace.of() は instance field の初期化で 1 回だけ

Trace.of(...) は Usecase クラスの instance field の 初期化時に一度だけ 呼びます。invoke() 内で呼び直すと、毎回新しい Trace インスタンスができ、ネスト・集約のロジックが破綻します。

APEX
public with sharing class YourUsecase {
  private Trace t = Trace.of('処理名');  // ✅ instance field で 1 回だけ
 
  public void invoke() {
    // private Trace t = Trace.of(...);  // ❌ invoke 内で呼び直さない
    this.t.start();
    // ...
  }
}

テストの冒頭で Trace.of('...').start() を書く理由

これまでに見たテスト例では、メソッドの冒頭に必ず次の 2 行が並びます。

APEX
Trace t = Trace.of('正常系: ...');
t.start();

そして末尾で t.finish(); を呼びます。これには 第一の理由副次効果 があります。

第一の理由: Usecase 側の Trace ライフサイクル不整合を事前に防ぐため。Usecase の内側で finish / skip / abort を呼ばずに return してしまうと、開始と終了がミスマッチした「開きっぱなしの Trace」がコンテキストに残ります。テスト側でアウター Trace に包んでおくと、Strict モード (テスト実行時の default) がそのミスマッチを テストの段階で検出 してくれます。本番に出る前に、Usecase 側のライフサイクル抜けに気づける仕組みです。

副次効果: 直前の Trace 状態をリセットする。前のテストが残した Trace コンテキストを、新しいテストの冒頭で明示的に巻き直せます。

テストクラス全体の形

すべてのテストメソッドで、最初と最後に置きます。 クラス全体では次の形になります。

APEX
@isTest(seeAllData=false)
private class ResummarizeUsecase_T {
  @isTest
  static void testInvoke_WhenOpportunitiesExist_ThenSummaryUpdated() {
    Trace t = Trace.of('正常系: 商談があるとき取引先のサマリが更新されること');
    t.start();
 
    // Arrange
    ...
 
    // Act
    ...
 
    // Assert
    ...
    Assert.isTrue(TraceFlow.isLastFinish());
 
    t.finish();
  }
 
  @isTest
  static void testInvoke_WhenNoOpportunities_ThenSkipped() {
    Trace t = Trace.of('正常系: 商談が 1 件も無いときスキップされること');
    t.start();
 
    // Arrange
    ...
 
    // Act
    ...
 
    // Assert
    Assert.isTrue(TraceFlow.isLastSkip());
 
    t.finish();
  }
}

t.start()t.finish()メソッドの最初と最後を挟むのがポイントです。この形になっていれば、内側の Usecase が終了を呼び忘れた瞬間に Strict モードが検出します。

もう 1 つの効能: 検証内容を命名規則の外に書ける

テストメソッド名は test{メソッド}_When{条件}_Then{結果} のような形式に縛られます。grep できる識別子である以上これは正しい制約ですが、識別子の制約の中では、書ける説明の量も語彙も限られます

Trace.of(...) の引数はただの文字列なので、その制約の外にあります。

役割制約
メソッド名機械可読。grep と実行結果の識別子命名規則・識別子として妥当な文字
Trace.of の引数人間可読。何を検証しているかの説明無し

英語圏以外のチームでは、これが可読性に直結します。 メソッド名は規約どおり英語のまま置き、説明は母語で書くという分担ができるからです。「何を検証しているのか」を母語で読めるほうが、レビューでも障害調査でも確実に速く、しかもメソッド名の規約を崩さずに済みます。

この文字列はデバッグログにも出力されるので、テストが落ちたときのログにも説明がそのまま載ります。

📌 Apex Stem の規約では、この文字列を 正常系: / 異常系: / エッジケース: のいずれかで始めます。区分ラベルの後ろに、人間可読の説明文を続けてください。

関連ドキュメント