Born from a legacy org

Apex Stem「テストが書けない」から始まりました

道具が先にあったのではなく、テストを書けるようにするために作りました。
Handler-Usecase という 1 本の幹と、そこから伸びた OSS 群です。

Almost every successful org reaches this point

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 の性能が上がっても古びません。

Handler-Usecase Architecture を詳しく読む →

なぜ 2 層なのか、各層の責務、部品クラスの扱いまでをじっくり読み解きます

What Grows From It

幹があって、そこから枝が伸びました。必要になった順に 1 つずつです。
それぞれ単体でも導入できます。

Data Access

ApexEloquent

モックを前提に設計された SOQL/DML の ORM。Usecase 層の単体テストで使います。 SELECT していない項目を触ったら、数 ms のテストで例外になります。

Test Data Factory

ApexBlueprint

宣言的なテストデータファクトリ。 実 DML を流すHandler 層の結合テストで使います。 作りたいテストデータの形を宣言するだけ。Id のバケツリレーもバルク生成も、 フレームワークが吸収します。

Lifecycle Logging

ApexTrace

Usecase のライフサイクルログ。 戻り値のない処理でも、本当にその経路を通ったかをテストで縛れます。 「完了したつもりが途中で抜けていた」を捕まえます。

Foundation

ApexTools

TriggerHandler の基底クラスと、 DI 対応の HTTP リクエストラッパー。すべてのトリガーの下に敷かれ、どの Usecase からも使われます。 目立ちませんが、幹と枝を支えている土台です。

さらに、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 ラン)。 「証明した」ではなく「測ってみたらこうだった」として読んでください。

Get Started

すべてを書き直す必要はありません。
クエリ 1 本から取り入れて、そこから育てていけます。

最初の一歩を踏み出す

約 2 分で読めます · その先にコード付きのフルガイド