レッスン10 — alloy 消費者コードのテスト
問い
Provider / Network / Signer に依存するコード(Building tier の MEV searcher / indexer / wallet backend / swap aggregator)をどうユニットテストするか? 実 RPC エンドポイントを立てずにテストできないと、test gate は機能しない — どのパターンが本番標準か?
原理(最小モデル)
- 3 種テスト. プログラム制御 anvil(ほぼ常時)+ Forked anvil(実 mainnet 状態)+ トレイト差し替え(稀、ロジックがチェーンセマンティクスに依存しない)。
Anvil::new().spawn()で 50ms 起動. alloy 同梱、テスト内インプロセス、10 アカウント prefund + RPC URL 返す。AnvilApiトレイト拡張.anvil_set_balance/anvil_set_storage_at/anvil_set_code/anvil_impersonate_account/anvil_mine/anvil_snapshot+anvil_revert。同じ Provider トレイトの拡張、本番で使うプロバイダがテストでも走る。- Forked anvil でブロック pin 必須. pin しなければ非決定的、CI 意味なし。pin すれば assertion 厳しく可能。
- 手書きモックは稀. 純粋決定関数を秒間数百バリエーションで回したいときのみ。実務では anvil が十分速くスキップ不要。
- Building tier 接続. 全アプリの test gate スケッチが
forked_provider_at(...)の 1 行ラッパ、本レッスンが Building の前提。 - 「表面同等性」が mock 中心アプローチに勝つ理由. 本番プロバイダ + cheat = テストでも本物の Provider トレイトが走る、cheat は同表面に重なる。
具体例
3 種テスト:
| テスト種別 | 使う Provider | コスト | いつ使うか |
|---|---|---|---|
| プログラム制御 anvil | anvil インプロセス | 起動 ~50ms | ほぼ常時、ユニットテストに十分速い |
| Forked anvil | anvil --fork-url <RPC> + ブロック pin | ~200ms + RPC クォータ | 実 mainnet コントラクト状態必要 |
| トレイト差し替え | impl Provider for ... の自作 struct | なし | 稀、チェーンセマンティクス非依存 |
プログラム制御 anvil:
use alloy::node_bindings::Anvil;
use alloy::providers::ProviderBuilder;
use alloy::primitives::U256;
#[tokio::test]
async fn user_balance_round_trips() {
let anvil = Anvil::new().spawn(); // インプロセスで ~50 ms
let provider = ProviderBuilder::new().connect_http(anvil.endpoint().parse().unwrap());
let addr = anvil.addresses()[0];
let balance = provider.get_balance(addr).await.unwrap();
assert_eq!(balance, U256::from(10_000) * U256::from(10).pow(U256::from(18)));
drop(anvil); // drop で anvil プロセスは終了
}
AnvilApi トレイト拡張:
use alloy::providers::ext::AnvilApi;
#[tokio::test]
async fn impersonates_a_real_address() {
let anvil = Anvil::new().spawn();
let provider = ProviderBuilder::new().connect_http(anvil.endpoint().parse().unwrap());
let vitalik: Address = "0xab5801a7d398351b8be11c439e05c5b3259aec9b".parse().unwrap();
// Cheat 1: vitalik に 100 ETH 与える
provider.anvil_set_balance(vitalik, U256::from(100) * U256::from(10).pow(U256::from(18))).await.unwrap();
// Cheat 2: vitalik になりすます
provider.anvil_impersonate_account(vitalik).await.unwrap();
// 署名不要で vitalik 発の tx を送る
let tx = TransactionRequest::default().from(vitalik).to(BOB).value(U256::from(1));
let receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap();
assert!(receipt.status());
provider.anvil_stop_impersonating_account(vitalik).await.unwrap();
}
cheat の全体像:
anvil_set_balance/anvil_set_storage_at/anvil_set_code— 状態書き換えanvil_impersonate_account— なりすましanvil_mine— 強制 mineanvil_snapshot/anvil_revert— 状態チェックポイントとロールバック
MEV / wallet / indexer のテストを実現可能にする道具立て。
Forked anvil(実 mainnet 状態):
#[tokio::test]
async fn quotes_against_real_uniswap_v3() {
let anvil = Anvil::new()
.fork("https://eth.merkle.io")
.fork_block_number(18_500_000)
.spawn();
let provider = ProviderBuilder::new().connect_http(anvil.endpoint().parse().unwrap());
// ブロック 18_500_000 時点の USDC/WETH 0.3% pool
let pool: Address = "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".parse().unwrap();
let slot0 = provider.call(/* IUniswapV3Pool::slot0Call */).await.unwrap();
// slot0 は決定的(実行間で)
}
ブロック pin 必須: pin しないと non-deterministic、CI 意味なし。pin すれば assertion 厳しく(QuoterV2 出力との 5 bps 以内など)。
手書きモック(稀):
struct CannedProvider {
balance_responses: Vec<U256>,
call_count: AtomicUsize,
}
#[async_trait]
impl Provider for CannedProvider {
fn root(&self) -> &RootProvider { /* ... */ }
fn get_balance(&self, _addr: Address) -> ProviderCall<NoParams, U256, U256> {
let idx = self.call_count.fetch_add(1, Ordering::SeqCst);
ProviderCall::ready(Ok(self.balance_responses[idx]))
}
// 他メソッドはデフォルト実装(呼ばれたら panic)
}
実務でレア = anvil 十分速い + cheat 充実で大半スキップ不要。
Building tier 接続:
let svc = test_service().await;
let provider = forked_provider_at(FORK_RPC, PINNED_BLOCK).await;
forked_provider_at(...) = §3 の Anvil::new().fork(...).spawn() パターン 1 行ラッパ。
失敗例(誤解)
「Forked anvil のブロック pin なしで OK」— 間違い。ブロックなしだと pool price が動く → assertion 許容幅広く必要 → 何を担保したか不明。pin で決定的テスト。
「手書き mock のほうが速いから本番向き」— 間違い。anvil ~50ms 起動、十分速い。mock = Provider トレイトを部分実装 → 本番経路と乖離 + cheat 使えない。anvil 表面同等性が勝つ。
「AnvilApi は独立トレイト」— 間違い。Provider のトレイト拡張、本番で使うのと同じ Provider がテストでも走る。基底トランスポートが anvil のときに(そのときに限り)プロバイダ上のメソッド呼び出しになる。
ステップで組み立てる
Step 1: 3 種テストを選ぶ
ほぼ常時 = プログラム制御 anvil / 実 mainnet 状態 = Forked anvil / 稀 = トレイト差し替え。
Step 2: Anvil::new().spawn() パターン
50ms 起動、10 prefund アカウント、anvil.endpoint() で URL。
Step 3: AnvilApi cheats
anvil_set_balance + anvil_impersonate_account で MEV / wallet テスト実現。
Step 4: Forked anvil でブロック pin
.fork(rpc).fork_block_number(N) で決定的、実 mainnet コントラクト状態。
Step 5: 手書きモックは稀
純粋決定関数を秒間数百バリエーション = mock、それ以外 = anvil。
Step 6: Building tier 接続
全アプリ test gate が forked_provider_at(...) 1 行、本レッスンが前提。
答え合わせ
Anvil::new().spawn()が手書きMockProviderより忠実度が高い理由: 本番で使う Provider トレイトがテストでも走る + cheat はその同じ表面に重なる → 表面同等性。Mock は Provider トレイトを部分実装 → 本番経路と乖離 + cheat 使えない + 手書きコスト。anvil ~50ms 起動で速度差ほぼなし。- Forked anvil でブロック pin 必須な理由: pin しないと pool price がブロックごとに動く → assertion 許容幅広く必要(QuoterV2 出力との 5%? 10%?)→ 何を担保したか不明。pin = 決定的 → 厳しい assertion 可能(5 bps 以内)→ CI で意味ある。
AnvilApiがトレイト拡張で別型でない理由: 本番で使う Provider をテストでも使う → cheat はその同じ Provider 上のメソッド呼び出しになる(基底トランスポートが anvil のときのみ動く)。別型だとテスト用プロバイダと本番用プロバイダで API 表面が違う → mock 中心アプローチに退化。表面同等性がパターンの肝。
合格基準
- 3 種テスト(プログラム制御 anvil / Forked anvil / トレイト差し替え)を即答できる。
Anvil::new().spawn()の 50ms 起動 + 10 prefund アカウントを言える。AnvilApicheats(anvil_set_balance/anvil_impersonate_account等)を即答できる。- Forked anvil のブロック pin 必須理由を 1 文で説明できる。
- 表面同等性が mock 中心に勝つ理由を 1 文で説明できる。
まとめ(3行)
- 3 種テスト = プログラム制御 anvil(ほぼ常時、50ms 起動)/ Forked anvil(実 mainnet、ブロック pin 必須)/ トレイト差し替え(稀)。
AnvilApicheats(set_balance / impersonate_account / snapshot / revert)= MEV / wallet / indexer テストを実現可能にする道具立て、Provider のトレイト拡張で本番と同表面。- 「表面同等性」が mock 中心アプローチに勝つ理由 = 本番で使う Provider がテストでも走る + cheat 同表面に重なる、anvil ~50ms で速度差なし、Building tier 全アプリ test gate の前提。