FABRKNT
Inside Alloy — Rust Ethereum ライブラリを読む
Alloyの内側
レッスン 14 / 15·CONTENT22 分45 XP
コース
Inside Alloy — Rust Ethereum ライブラリを読む
レッスンの役割
CONTENT
順序
14 / 15

レッスン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コストいつ使うか
プログラム制御 anvilanvil インプロセス起動 ~50msほぼ常時、ユニットテストに十分速い
Forked anvilanvil --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 — 強制 mine
  • anvil_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 アカウントを言える。
  • AnvilApi cheats(anvil_set_balance / anvil_impersonate_account 等)を即答できる。
  • Forked anvil のブロック pin 必須理由を 1 文で説明できる。
  • 表面同等性が mock 中心に勝つ理由を 1 文で説明できる。

まとめ(3行)

  • 3 種テスト = プログラム制御 anvil(ほぼ常時、50ms 起動)/ Forked anvil(実 mainnet、ブロック pin 必須)/ トレイト差し替え(稀)。
  • AnvilApi cheats(set_balance / impersonate_account / snapshot / revert)= MEV / wallet / indexer テストを実現可能にする道具立て、Provider のトレイト拡張で本番と同表面。
  • 「表面同等性」が mock 中心アプローチに勝つ理由 = 本番で使う Provider がテストでも走る + cheat 同表面に重なる、anvil ~50ms で速度差なし、Building tier 全アプリ test gate の前提。