TraceFlow でテストの経路を検証する

Apex Stem ドキュメント
Apex StemApexTraceTestingSalesforceApex
戻り値が void の Usecase を「副作用」と「経路」の 2 軸で縛る方法。経路の区別と、lastHistoryContains と contains の使い分けを実例で解説します。

TraceFlow でテストの経路を検証する

TraceFlow は、直前に実行された Trace の状態を覗くための静的ユーティリティです。テストで「Usecase がどの経路で終わったか」を区別するのに使います。

主な検証メソッド

メソッド検証内容
TraceFlow.isLastFinish()直前の Trace が finish() で終わったか
TraceFlow.isLastSkip()直前の Trace が skip() で終わったか
TraceFlow.isLastAbort()直前の Trace が abort() で終わったか
TraceFlow.lastHistoryContains(text)最後の 1 エントリに文字列が含まれるか
TraceFlow.contains(text)履歴全体のどこかに文字列が含まれるか
TraceFlow.lastHistory()直前の TraceHistory オブジェクトを取得
TraceFlow.usageOf(name)その名前のコンテキストのガバナ消費 (exclusive 合計、v1.2.0+)
TraceFlow.lastUsage()直近に閉じた1 コンテキストのガバナ消費 (バルク IT では usageOf を使う)

lastHistoryContainscontains の使い分け

lastHistoryContains が見るのは最後の 1 エントリだけです。そのため、あとから t.log(...) を 1 行足しただけでテストが落ちます。

  • finish / skip / abort の理由メッセージを縛るlastHistoryContains。直後のエントリなので順序が安定します
  • 途中の t.log(...) の内容を縛る / ログ追加に強くしたいcontains
APEX
Assert.isTrue(TraceFlow.contains('3 件の商談に業種をコピー'));  // 履歴のどこにあっても通る

どちらも null を渡すと false を返します。

invoke() が void でも経路で検証できる

戻り値が void の Usecase は、テストの観察対象が副作用 (DML や field 更新の中身) に偏りがちです。それでも TraceFlow を組み合わせれば、「副作用」と「経路」の 2 軸で挙動を縛れます。これは ApexTrace が Apex Stem に標準装備されている理由のひとつです。

コード例: skip と finish を区別する

「対象がなくてスキップされた」と「正常に完了した」を、別々のテストとして書きます。

APEX
@isTest
static void testInvoke_WhenOpportunityHasAccount_ThenIndustryCopied() {
  Trace t = Trace.of('正常系: 商談に親取引先の業種がコピーされること');
  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<IEntry>{ oppEntry });
 
  (new CopyAccountIndustryToOpportunityUsecase(
    new Set<Id>{ 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('正常系: 対象の商談がないときスキップされること');
  t.start();
 
  MockEloquent mock = new MockEloquent();
 
  (new CopyAccountIndustryToOpportunityUsecase(
    new Set<Id>(), mock
  )).invoke();
 
  Assert.isTrue(TraceFlow.isLastSkip());
 
  t.finish();
}

両方のテストとも、戻り値ではない「経路」を TraceFlow で観察しています。finish パスを通ったか skip パスを通ったかが、副作用 (upsertedRecords の中身) とは別の軸で検証できます。

ログ内容の検証も可能です。

APEX
Assert.isTrue(TraceFlow.lastHistoryContains('対象の商談がないため終了'));

「想定通りのメッセージで skip したか」までを縛れます。

観測窓を Act に揃える: discardArrange() (v1.4.0+)

isLastFinish() / isLastSkip() / isLastAbort() が見るのは「最後に閉じたコンテキスト」です。ところが Trace の履歴はテストメソッドの先頭から溜まり続けるため、Arrange でレコードを作った DML がトリガー経由で Usecase を走らせていると、その分も履歴に残ります。

問題になるのは、Act が何も起こさなかったときです。Act 側にコンテキストが 1 つも積まれないと、isLastSkip() が読むのは Arrange のコンテキストになります。つまり Act を検証したつもりで Arrange を検証している状態が起こり得ます。

TraceFlow.discardArrange() を Arrange と Act の境界に置くと、履歴がそこで区切られ、以降のアサートは Act だけを見ます。

APEX
setupAccountWithOpportunities();   // Arrange (トリガー経由で Usecase が走る)
 
TraceFlow.discardArrange();        // ← ここ
Test.startTest();                  // ← と、ここは同じ境界
 
new ResummarizeUsecase(ids).invoke();
Test.stopTest();
 
Assert.isTrue(TraceFlow.isLastSkip());   // Act の経路だけを見る

Test.startTest() を置くのと同じ判断なので、新しく覚える概念はありません。書く場所も隣です。

⚠️ Usecase のコンテキストが開いている最中には呼べません。 境界で呼んでいる限り、開いているのはテスト自身の Trace が最大 1 つなので問題になりませんが、それ以上開いていると TraceException になります。

関連ドキュメント