# IHttpRequestHandler: 公式 HttpCalloutMock のやりづらさと ApexTools の答え

Salesforce の外部連携をテストするとき、応答の差し替えには標準の `HttpCalloutMock` を使います。ApexTools の `IHttpRequestHandler` は、**コールアウトを DI で差し替え可能にし、応答を宣言だけで組み立てられる**ようにします。

> 📌 **標準にも組み込みのモックはあります。** `StaticResourceCalloutMock` / `MultiStaticResourceCalloutMock` を使えば、実装クラスを書かずに応答を返せます。ただし**応答ボディを静的リソース (メタデータ) として用意する必要があり**、しかも 1 エンドポイントにつき 1 応答なので、**順序を持つ応答 (リトライ・ページネーション) は表現できません**。そこに踏み込んだ時点で `HttpCalloutMock` の実装クラスが必要になります。

## 何ができるか

- 本番は `HttpRequestHandler` (標準 `Http` の薄いラッパー)、テストは `MockHttpRequestHandler` を注入する
- 応答は `MockResponse.of('GET').respond(body, 200)` の 1 行で宣言する
- **順序つきの応答でも、`HttpCalloutMock` の実装クラスを書かなくてよい**
- 送ったリクエストを後から検証できる (Spy)

## 公式 HttpCalloutMock の 3 つのやりづらさ

以下は、`HttpCalloutMock` を自分で実装することになったときに踏む 3 つです。

### やりづらさ 1: JSON 文字列の手動作成

応答ボディを文字列リテラルで書くことになり、エスケープと可読性の両方が犠牲になります (静的リソースに逃がす手もありますが、今度はテストを読むのにファイルを開く必要が出ます)。

```apex
// ❌ 階層が深いほど破綻する
String body = '{"records":[{"Id":"001xx","Name":"Acme","Contacts":{"totalSize":2}}]}';
```

### やりづらさ 2: URL 文字列に依存した if-else 分岐

1 つのモッククラスが全エンドポイントを引き受けるため、`respond()` の中が URL 判定の分岐で膨らみます。

```apex
// ❌ エンドポイントが増えるたびに分岐が伸びる
public HttpResponse respond(HttpRequest req) {
  if (req.getEndpoint().contains('/accounts')) { ... }
  else if (req.getEndpoint().contains('/contacts')) { ... }
  ...
}
```

### やりづらさ 3: リトライ・ページネーションの再現が困難

「1 回目は 500、2 回目も 500、3 回目に 200」のような**順序を持つ応答**を表現するには、モック側にカウンタを持たせることになります。

## ApexTools の答え

### 応答は MockResponse で宣言する

```apex
MockResponse.of('GET').respond('{"message":"not found"}', 404)          // String
MockResponse.of('post').respond(new Map<String, Object>{ ... }, 200)    // Map / List は JSON 化。メソッド名は大小どちらでも
MockResponse.of('GET').respond(imageBytes, 200).header('Content-Type', 'image/jpeg')  // Blob + ヘッダ
MockResponse.of('GET').respond(ok, 200).repeat()                        // キュー末尾に置くと以降ずっとこれ
```

`respond` は `String` / `Map<String, Object>` / `List<Object>` / `Blob` を受けます。Map と List は自動で JSON 化されるので、**やりづらさ 1 は「Apex のコレクションで書く」だけで解消**します。

> ⚠️ **メソッド名はルーティングキーではなく「配信時の契約」です。** キューの次の応答が宣言したメソッドと実リクエストで食い違うと、期待 / 実際 / キュー状態を含むエラーで即座に落ちます。黙って違う応答が配られることはありません。

## 2 つのモード

**🎯 親指ルール: テストの主張に順序が含まれるなら台本モード、含まれないなら label モード。迷ったら label。**

### 台本モード (label なし): 順序が仕様であるとき

コンストラクタに渡したリストが、そのままフローの台本になります。上から読めば期待するコールアウト列そのものです。

```apex
MockHttpRequestHandler mock = new MockHttpRequestHandler(new List<MockResponse>{
  MockResponse.of('GET').respond(notFound, 404),    // 1 手目: 存在確認
  MockResponse.of('POST').respond(created, 201),    // 2 手目: 作成
  MockResponse.of('GET').respond(found, 200)        // 3 手目: 再取得
});
```

順序やメソッドから逸脱すると詳細なエラーになります。**やりづらさ 3 は、リトライを「同じメソッドを並べるだけ」で表現できる**ようになります。

```apex
// 1 回目 500、2 回目 500、3 回目に成功。モック側にカウンタは不要
new List<MockResponse>{
  MockResponse.of('POST').respond(err, 500),
  MockResponse.of('POST').respond(err, 500),
  MockResponse.of('POST').respond(ok, 200)
}
```

### label モード: サイト間の順序に縛られたくないとき

呼び出しサイトごとに名前付きキューを持たせます (`MockEloquent` の `attach` / `label` と同じ操作感)。**やりづらさ 2 は、URL 判定ではなく呼び出しサイトの名前で仕分ける**ことで解消します。

```apex
// Usecase 側: this.http.label(LBL_EXISTS).send(req);
MockHttpRequestHandler mock = new MockHttpRequestHandler()
  .attach(LBL_EXISTS, MockResponse.of('GET').respond(notFound, 404))
  .attach(LBL_CREATE, MockResponse.of('POST').respond(created, 201))
  .attach(LBL_UPDATE, MockResponse.of('PUT').respond(updated, 200));   // 通らない分岐も宣言してよい

new KintoneUpsertUsecase(input, mock).invoke();

Assert.areEqual(1, mock.sentRequestsAt(LBL_CREATE).size());   // create 分岐を通った
Assert.areEqual(0, mock.sentRequestsAt(LBL_UPDATE).size());   // update は未消費
```

分岐フローでは、**両方の分岐を attach しておき、どちらが消費されたかで通った経路をアサートする**のが定石です。

- `attach` を使ったら、`send` ごとに `label()` が必須です (1 回で消費)
- ラベルの typo は、登録済みラベルの一覧つきでエラーになります
- 同一 label への `attach` はキューに追記されます (= そのサイトのリトライ系列)
- **`attach` を使わなければ `label()` は無視されます**。label 付きの本番コードを、素の台本モックでもテストできます

## 検証ヘルパー (Spy)

| メソッド | 用途 |
|---|---|
| `sentRequestsAt(label)` | そのラベルで送られたリクエスト |
| `countByMethod('POST')` | メソッド別の送信回数 |
| `requestsTo(endpointPart)` | エンドポイントの部分一致で絞る |
| `lastRequest()` | 最後に送ったリクエスト |
| `describe()` | キューの現在状態 (デバッグ用) |

## 🛡 Content-Type ガード (実事故由来の定石)

HTTP 200 でも Content-Type が想定外なら、たいていは**エンドポイントの間違い**です。

> 実例: `/bizCards/{id}/image` を叩いたつもりが `/bizCards/{id}` を叩いており、返ってきた JSON を base64 して壊れた画像を画面に流していた。

バイナリを取得するときは必ずガードを入れてください。

```apex
this.http.label(LBL_CARD_IMAGE).send(req);
String contentType = this.http.getHeader('Content-Type');
if (this.http.getStatusCode() == 200 && (contentType == null || !contentType.startsWith('image/'))) {
  throw new CalloutException('Expected an image response but got Content-Type=' + contentType);
}
Blob image = this.http.getBodyAsBlob();
```

## Apex Stem との統合: Usecase で DI する

`IHttpRequestHandler` は、Apex Stem の Usecase 層と [Layered Constructor Pattern](/ja/apex-stem/docs/layered-constructor-pattern) にそのままはまります。**v1.0.0 以降は 1 本の handler を label で多重化する**のが推奨です (`IEloquent` の `label` と同じ考え方)。

```apex
public with sharing class KintoneUpsertUsecase {
  @TestVisible static final String LBL_EXISTS = 'kintoneExists';
  @TestVisible static final String LBL_CREATE = 'kintoneCreate';

  private final Input input;
  private final IHttpRequestHandler http;
  private Trace t = Trace.of('kintone へレコードを upsert');

  // 🚪 本番用
  public KintoneUpsertUsecase(Input input) {
    this(input, null);
  }

  // 🧪 テスト用 (DI 対応)
  @TestVisible
  private KintoneUpsertUsecase(Input input, IHttpRequestHandler http) {
    this.input = input;
    this.http = http ?? new HttpRequestHandler();
  }

  public void invoke() {
    this.t.start();
    this.http.label(LBL_EXISTS).send(existsReq);
    // ...
  }
}
```

役割ごとに複数の handler を DI する旧スタイルも引き続き使えますが、**ラベル多重化のほうがコンストラクタが太りません**。

`MockEloquent` (ApexEloquent) と `MockHttpRequestHandler` (ApexTools) を独立して DI すれば、**副作用 (DML) と外部呼び出し (HTTP) を別々の軸で検証**できます。

## ⚠️ v1.0.0 の破壊的変更

タグ以前の `main` から上げる場合は、次の 3 点の対応が必要です。

| 変更 | 対応 |
|---|---|
| 旧コンストラクタ (`Map` / `List<Map>` / `String` + `Integer`) を削除 | `MockResponse.of(method).respond(body, statusCode)` に書き換える |
| `IHttpRequestHandler` に `label` / `getBodyAsBlob` / `getHeader` を追加 | 独自実装クラスがあればメソッドを追加する |
| 枯渇エラーのメッセージが複数行の診断形式に変更 | 完全一致の assert は `contains` に緩める |

## 次に読む

- [Layered Constructor Pattern](/ja/apex-stem/docs/layered-constructor-pattern): `IHttpRequestHandler` の DI 設計を支える基本パターン
- [TriggerHandler](/ja/apex-stem/docs/apex-tools-trigger-handler): ApexTools のもう 1 つの柱
- [ApexTools ガイド](/ja/apex-stem/docs/apex-tools-guide): ガイド目次に戻る
