Born from a legacy org
Apex Stem「テストが書けない」から始まりました
道具が先にあったのではなく、テストを書けるようにするために作りました。
Handler-Usecase という 1 本の幹と、そこから伸びた OSS 群です。
The Story
Salesforce 公式も「ほぼすべての成功した実装が、 初日にうまくいっていた開発パターンが崩れ始める地点に到達する」と書いています。 特殊な不運ではなく、うまくいった実装だからこそ通る道です。
The Challenge
▸5 年運用された Salesforce 組織。トリガーがトリガーを呼ぶ負債、実質何も検証していないアサーション、呼ぶだけでガバナ制限に達する TestDataFactory。
▸fflib は少人数のチームには重すぎる。かといってベタ書きの SOQL は、複雑なデータ構造を必要とするテストに耐えられない。
▸AI コーディングアシスタントが実際に従える何かが欲しかった。CLAUDE.md に収まるくらい小さな規約を。
What emerged
▸少しずつ立て直しました。Usecase 1 つ、モック可能なクエリ 1 本ずつ。
▸最初は ApexEloquent。必要が見えてくるたびに ApexBlueprint、ApexTrace、ApexTools が続きました。
▸このスタックは今、AI とともに安定した品質のコードを継続的に生み出すための土台になっています。
What We Believe
道具の設計はすべて、この 3 つから来ています。
01
テストが、本番より甘くならない
実際の SOQL で取ったレコードは、SELECT していない項目に触ると例外になります。 ところが、テスト用に自分で組み立てた SObject にはその検査がありません。 テストのときだけ緩くなり、本番で初めて落ちます。
モックが本番のクエリで何を SELECT しているかを知っています。 SELECT していない項目に触れば、数 ms のテストがその場で落ちます。
02
速いことは、品質の条件
遅いテストは、書かれないのではありません。実行されないのです。 人も AI も、重い環境では検証を省きます。
速さは生産性の話ではなく、信頼性の話です。 速いから気軽に実行でき、フィードバックのループが回り続けます。
03
構造が、そのまま見える
親子関係はインデントで書けます。クエリの組み立ても、テストデータの生成も、コードの形がデータの形になります。
Scribe.of(Account.class) .withChildren( Scribe.asChild(Contact.class) )どれもイミュータブルなので、組み立てた途中のものを使い回しても壊れません。
The Architecture
Handler-Usecase。2 つの層、層ごとに 1 つのルール。あとは現場に委ねます。
Handler Layer
Integration tests · ApexBlueprintエントリーポイント (Trigger / Batch / REST / Flow / Schedulable)。 各エントリーポイント固有の作法を吸収し、Usecase に渡すだけ。ここにビジネスロジックは置きません。
Usecase Layer
Unit tests · ApexEloquent (mock DB)ビジネスロジック。public は invoke() のみ。 コンストラクタで全依存を受け取り、生焼けオブジェクトを作りません。
Define the stem only
幹となるアーキテクチャ (Handler + Usecase) だけを定義します。 そこから伸びる枝葉のクラス (Reader や Validator のような部品) をどう切り出すかは強制しません。
Tests map 1-to-1
Handler → 実 DML の結合テスト。 Usecase → モック DB の単体テスト。 どこでも同じ規約です。
AI-friendly by design
シンプルな規約だから、AI に読み込ませる開発ルールとして 無理なく機能します。ただし分けている理由はテスト可能にするためで、 AI のコンテキストに収めるためではありません。 だからこの設計は、AI の性能が上がっても古びません。
なぜ 2 層なのか、各層の責務、部品クラスの扱いまでをじっくり読み解きます
What Grows From It
幹があって、そこから枝が伸びました。必要になった順に 1 つずつです。
それぞれ単体でも導入できます。
ApexEloquent
モックを前提に設計された SOQL/DML の ORM。Usecase 層の単体テストで使います。 SELECT していない項目を触ったら、数 ms のテストで例外になります。
ApexBlueprint
宣言的なテストデータファクトリ。 実 DML を流すHandler 層の結合テストで使います。 作りたいテストデータの形を宣言するだけ。Id のバケツリレーもバルク生成も、 フレームワークが吸収します。
さらに、LWC ↔ Apex 境界のための薄い Result DTO 規約 も。
Proof
体感で「うまく回っている」と言っても仕方がないので、自分で疑って測りました。
モックにすると、テストの品質は落ちるのでは?
モック
83.4%
vs
実 DML
79.3%
ミューテーションテスト (コードにわざとバグを埋め、テストが気づけるか調べる手法) で計測。 334 個のバグを埋めて、モック版と実 DML 版のどちらが多く捕まえるか比べました。落ちませんでした。
どれくらい速いのか?
モック
15ms
vs
実 DML
272〜1,062ms
1 メソッドあたりの中央値。実運用中の 2 組織で計測しました。
org が育つと遅くなるのでは?
定数時間
単体層は DML を流さないので、差が開くのは実 DML 側だけです。
いずれも実測ですが、規模は大きくありません (対象クラス 1 つ・各 1 ラン)。 「証明した」ではなく「測ってみたらこうだった」として読んでください。