Apex Stem · Lifecycle Tracing
v1.4.0·リリースノート

ApexTrace

そのログが、そのままテストの検証項目になる。「通った経路」を、副作用とは別の軸で縛る。

本番のデバッグログのために書いた 1 行が、そのまま「この Usecase は本当にその経路を通ったのか」 を確かめるアサーションになります。ログとテストのために二重に計装する必要はありません。

CopyIndustryToOpp.cls — usecase
private Trace t = Trace.of('商談に親取引先の業種をコピー'); public void invoke() {  this.t.start();   if (this.opportunityIds.isEmpty()) {    this.t.skip('対象の商談がないため終了。');    return;  }  // ... 業務ロジック ...  this.t.finish(entries.size() + ' 件にコピー。');}// この 3 行が、本番のログであり、テストの検証項目でもある。

skip で抜けたのかfinish まで走ったのかを、 テスト側から TraceFlow が読み取れます。

Three blind spots, one log

機能を 3 つ並べたリストではありません。1 本のライフサイクルログを置くことで、 同時に塞がる 3 つの死角です。

The blind spot

「本番で動いてない。どこまで進んだ?」

What changes

System.debug を書き散らすと、 書式も粒度も人によってばらつき、後から追える形になりません。Trace.of('...') のラベルは 全出力に自動で付くので、どの Usecase がどこで止まったかが ログを見るだけで分かります。

The blind spot

「正常にスキップした。でも、確かめる手段がない」

What changes

戻り値が void だと、これを検証するには 戻り値を歪めるか @TestVisible の変数を足すしかなく、テストのためのコードが本番側に滲み出しますt.skip() を 1 行置けば、そのままTraceFlow.isLastSkip() で縛れます。将来のデバッグのために残すログが、そのまま検証ルートになります。

The blind spot

「バルクで回したら、SOQL が 101 本」

What changes

件数に比例してクエリが増える実装は、1 件のテストでは素通りします。 ApexTrace は Usecase 単位でガバナ消費を記録するので、 「バルクで爆発していないこと」を緩い上限で縛る保険を、 結合テストに 1 行で置けます。

The test that proves nothing

Your tests are green.That is not the same as verified.

void を返す Usecase は、何もしなかったときが一番テストを通りやすいという 性質を持っています。early return して DML が 0 件なら、「保存されたレコードを検証する」アサートは0 件どうしを突き合わせるだけになり、 何も確かめないまま緑になります。ApexTrace は「副作用」とは独立した「終了経路」という第 2 の軸を与えるので、 空振りしていたテストが、空振りした瞬間に落ちます。

How it reads

同じ「対象がないときスキップされること」のテストを、左は副作用だけで、右は経路も含めて検証します。 ピンクの行が何も証明していないアサート、 緑の行がそれを本物にする TraceFlow です。

Vacuously true

「0 件であること」は、スキップしても、処理して 0 件でも、例外で中断しても成立します。 つまり 3 つの違う結果を区別できていません

Path-verified

isLastSkip() は 3 つのうち 1 つだけを通します。理由メッセージまで縛れば、意図した分岐であることも確かめられます。

CopyIndustryToOpp_T.clsSide effects only
MockEloquent mock = new MockEloquent(); (new CopyIndustryToOpp(  new Set<Id>(), mock)).invoke(); // 保存が 0 件であることAssert.areEqual(  0,  mock.upsertedRecordsAt(LBL_UPDATE).size()); // スキップでも、0 件処理でも、// 中断でも、この assert は通ってしまう
CopyIndustryToOpp_T.clsPath-verified
MockEloquent mock = new MockEloquent(); (new CopyIndustryToOpp(  new Set<Id>(), mock)).invoke(); // 保存が 0 件であることAssert.areEqual(  0,  mock.upsertedRecordsAt(LBL_UPDATE).size()); // かつ、skip 経路を通ったことAssert.isTrue(TraceFlow.isLastSkip());Assert.isTrue(  TraceFlow.contains('対象の商談がないため終了'));

終了経路は finish (正常完了) /skip (何もせず正常に抜けた) /abort (完遂しなかった) の 3 つです。 この 3 つを書き分けておくと、「正常完了したつもりが skip していた」も 「skip するはずが処理を走らせていた」も、テストが検出します (TraceFlow で経路を検証する)。

A ceiling, not a budget

「この Usecase は SOQL を何本使うべきか」を決める道具ではありません。狙いは保険です。バルクで走ったときに1 件ごとにクエリを撃つ実装 (N+1) が混入していないこと、 それだけを緩い上限で縛ります。

ApexBlueprint で 本番サイズのバルクを流し、「レコードごとにクエリを撃つ」実装へ意図的に劣化させると、プラットフォーム上限 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

回数は 2 なのに SOQL が 32。 この 1 行だけで、配線が増えたのではなく中身のループが原因だと切り分けられます。usageOf(name) で Usecase を名前で引けるので、ハンドラが 3 つ呼んでいても狙った 1 本を縛れます。 Arrange と Act の境界に discardArrange() を 置けば、測る範囲も Act だけに揃います。

Where Limits can't reach

非同期 (バッチ / Queueable) は Limits では原理的に測れません。Test.stopTest() で初めて走り、その後カウンタはstartTest() 前の値に戻るためです。 TraceUsage はコンテキスト境界の差分を取るので、どちらの影響も受けません。

ガバナ IT の組み方と、しきい値の決め方を読む

One record. Three readers.

API は Trace の 4 メソッドだけ。 経路の検証もガバナの計測も、そこから新しく書き足すものではありません。start と終了を書いた時点で、 同じ 1 本の記録を 3 者が別々の目的で読みます。

Reader 1
Debug log

本番で読む人。ラベル付きの START / SKIP / FINISH と、その理由が並びます。

Reader 2
TraceFlow

テストで読む機構。同じ記録から「どの経路で閉じたか」を取り出します。

Reader 3
TraceUsage

同じ start / 終了の区間を、そのままガバナ消費の計測区間として使います。

この構造の副産物として、ライフサイクルの不整合そのものがテストで落ちます。 終了を呼ばずに return すると「開きっぱなしの Trace」が残り、 テスト実行時は自動で Strict モードになって即座に例外になります。本番では Relaxed モードが吸収するので、 ログの不備が業務を止めることはありません。厳しさをテストに、寛容さを本番に置き分けています (ネストと 2 つのモード)。

The log stops lying

普通のログが嘘をつくのは、読み手がいないからです。 書きっぱなしで、次に読まれるのは障害のとき。その頃には何度もリファクタされていて、 「スキップしました」と出しているのに実際は処理している、が平気で起きます。何も壊れないので、誰も気づきません。アサートを 1 本刺した瞬間に、そのログ行に消費者ができます。 消費者がいれば、ズレは壊れる形で表面化します。規律ではなく、構造で担保されます。

Trace / TraceFlow / TraceUsage の使い方を読む

Position in Apex Stem

ApexTrace は Apex Stem を構成する 4 つの OSS のうち Lifecycle Logging を担います。Handler-Usecase Architecture の Usecase 層に常駐し、単体テストでも結合テストでもテスト戦略 の 「経路」と「バルク時のガバナ余白」の軸を引き受けます。

ApexEloquent
Data Access: SOQL / DML + モック
ApexBlueprint
Test Data Factory: 実 DML の結合テストデータ
ApexTrace
Lifecycle Logging: Usecase の経路追跡とテスト検証
ApexTools
Foundation: TriggerHandler 基底 + HTTP DI ラッパー
Related Documents

Start with the Developer Guide

Usecase へのログの仕込み方、TraceFlow による経路の検証、TraceUsage でのガバナ計測までを、 実コードで辿ります。