スマートコントラクトのテスト

はじめに

CasperのRustコントラクトのローカル開発環境として、フルノードを使用せずに新しいコントラクトのテストを実施できるテストフレームワークを提供しています。フレームワークは、アサーションを使用してグローバルステートへの変更をモニタリングしたり、スマートコントラクトのデプロイ成功時の確認ができるCasper実行エンジンのインスタンスを作成します。Casperのテストクレートは、検証の為にWasmを提供するクレートのRustワークスペース内に含まれる必要があります。

💡NOTE

提供しているテストサポートクレートは、Casperネットワークへの送信前にテストを行える唯一のオプションです。ご自身のテストフレームワークをお好きなように作成いただけます。

Cargo.tomlに依存関係を定義

このガイドでは、プロジェクト構造とここで概要を述べたサンプルのコントラクトをテスト作成に使用しています。

始めるにあたり、/tests/Cargo.tomlファイルにある必要なテスト依存関係について概要をお伝えします。同様に、テスト用に以前関係を指定し、クレートのバージョンを更新してください。依存関係は各プロジェクト毎に異なります。カウンターのテストの場合は、以下の依存関係を持っています。

[dependencies]
casper-execution-engine = "2.0.1"
casper-engine-test-support = { version = "2.2.0", features = ["test-support"] }
casper-types = "1.5.0"
  • casper-execution-engine – このクレートは、実行エンジン機能をインポートし、テストフレームワークでのWasmの実行を有効にします。各ノードには、実行エンジンが組み込まれており、テストフレームワークが動作をシミュレートします。
  • casper-engine-test-support – テストを書くインターフェイスの提供と実行エンジンインスタンスとのやり取りを行うhelperクレートです。
  • casper-types – Casperネットワーク仕様のCasperクレートが共有する型です。

テストの書き方

コントラクト用のテストは、通常、testsディレクトリに置かれます。カウンターコントラクト用のテストは、tests/src/integration-tests.rsファイルに置かれます。このファイルには、空のメインメソッドがテストプログラムを初期化する為に組み込まれているはずです。このように、代替案としてファイルのトップに#![no_main]の注釈を用いることも可能です。

fn main() {
 panic!("Execute \"cargo test\" to test the contract, not \"cargo run\".");
}

属性 #[cfg(test)] は、Rustコンパイラに、cargo test を呼び出す時のみ実行し、デバッグやリリースの時には実行しないように伝えます。テスト機能全て、mod tests のグループメカニズム内に置かれます。

#[cfg(test)]
mod tests {
    // The entire test program resides here
}

ビルダーと固定値のインポート

あらゆるデフォルト値と一連のテストで使われるhelperメソッドを持つ外部のテストサポートをインポートします。

更に、コントラクトコード内でテストする際に使えるCLTypesをインポートしておく必要があります。

    // Outlining aspects of the Casper test support crate to include.
    use casper_engine_test_support::{
        ExecuteRequestBuilder, InMemoryWasmTestBuilder, DEFAULT_ACCOUNT_ADDR,
        DEFAULT_RUN_GENESIS_REQUEST,
    };
    // Custom Casper types that will be used within this test.
    use casper_types::{runtime_args, ContractHash, RuntimeArgs};

次に、グローバル変数もしくは固定値をテスト用に定義しなくてはなりません。

    const COUNTER_V1_WASM: &str = "counter-v1.wasm"; // The first version of the contract
    const COUNTER_V2_WASM: &str = "counter-v2.wasm"; // The second version of the contract
    const COUNTER_CALL_WASM: &str = "counter-call.wasm"; // Session code that calls the contract

    const CONTRACT_KEY: &str = "counter"; // Named key referencing this contract
    const COUNT_KEY: &str = "count"; // Named key referencing the value to increment/decrement
    const CONTRACT_VERSION_KEY: &str = "version"; // Key maintaining the version of a contract package

    const ENTRY_POINT_COUNTER_DECREMENT: &str = "counter_decrement"; // Entry point to decrement the count value
    const ENTRY_POINT_COUNTER_INC: &str = "counter_inc"; // Entry point to increment the count value

テスト機能の作成

各テスト機能によって、コントラクトがインストールされ、コントラクトの動作が期待通りであることを認定するエントリーポイントを呼び出されます。テストは、InMemoryWasmTestBuilderを用いて、効率よくチェーン上でのコントラクトのインストールを行うプロセスをシミュレートする実行エンジンインスタンスを呼び出します。

プロセスの一環として、DEFAULT_RUN_GENESIS_REQUESTを用いて、MintAuctionHandlePaymentcontractsを含むテストに必要なシステムコントラクトをインストールし、デフォルトアカウント設定と紐づいたお財布への資金投入も同様に行います。

    #[test]
    /// Install version 1 of the counter contract and check its available entry points. ...
    fn install_version1_and_check_entry_points() {
        let mut builder = InMemoryWasmTestBuilder::default();
        builder.run_genesis(&*DEFAULT_RUN_GENESIS_REQUEST).commit();

        // See the repository for the full function.
    }

コントラクトのインストール

テスト機能は、ExecuteRequestBuilderを用いてテスト用のコントラクトをインストールします。カウンターテストでは、標準の依存間kウィとカウンターコントラクトを使用します。実行リクエストでは、Deployを送信するアカウントとして生成ビルダーにて作成された DEFAULT_ACCOUNT_ADDR を指定しています。

ExecuteRequestBuilder(この例でいう contract_installation_request)をビルド後、builder.exec を介してリクエストを処理し、必要に応じて他のリクエストの追加と処理を行います。

    // Install the contract.
    let contract_v1_installation_request = ExecuteRequestBuilder::standard(
        *DEFAULT_ACCOUNT_ADDR,
        COUNTER_V1_WASM,
        runtime_args! {},
    )
    .build();

    builder
        .exec(contract_v1_installation_request)
        .expect_success()
        .commit();

ハッシュでのコントラクトの呼び出し

インストールしたコントラクトを確認するには、そのコントラクトハッシュが必要です。テストは、contract_call_by_hash 関数を使用してエントリーポイントを呼び出します。以下のコードによって、インストール用のDeployを送信したDEFAULT_ACCOUNT_ADDRの名前付き鍵からコントラクトハッシュを取得します。

    // Check the contract hash.
    let contract_v1_hash = builder
        .get_expected_account(*DEFAULT_ACCOUNT_ADDR)
        .named_keys()
        .get(CONTRACT_KEY)
        .expect("must have contract hash key as part of contract creation")
        .into_hash()
        .map(ContractHash::new)
        .expect("must get contract hash");

次に、最初のバージョンのコントラクトに存在しないはずのエントリーポイントをテストします。

    // Call the decrement entry point, which should not be in version 1 before the upgrade.
    let contract_decrement_request = ExecuteRequestBuilder::contract_call_by_hash(
        *DEFAULT_ACCOUNT_ADDR,
        contract_v1_hash,
        ENTRY_POINT_COUNTER_DECREMENT,
        runtime_args! {},
    )
    .build();

    // Try executing the decrement entry point and expect an error.
    builder
        .exec(contract_decrement_request)
        .expect_failure()
        .commit();

セッションコードを使用したコントラクトの呼び出し

カウンターのサンプルでは、counter-call.wasmファイル内のセッションコードを使用します。セッションコードについての詳細情報やコントラクトコードとの違いについてなどは、このセクションを参照ください。

以下のセッションコードは、コントラクトハッシュを用いてコントラクトやデプロイ(DEFAULT_ACCOUNT_ADDR)を送信したアカウントと送信先 (COUNTER_CALL_WASM)のアカウント、そして必要な実行引数を識別します。再度、ExecuteRequestBuilderがセッションコードの実行のシミュレーションとcounter-inc エントリーポイントの呼び出しを行います。

    // Use session code to increment the counter.
    let session_code_request = ExecuteRequestBuilder::standard(
        *DEFAULT_ACCOUNT_ADDR,
        COUNTER_CALL_WASM,
        runtime_args! {
            CONTRACT_KEY => contract_v1_hash
        },
    )
    .build();

    builder.exec(session_code_request)
        .expect_success()
        .commit();

結果の比較と評価

コントラクトを呼び出した後、受け取った結果から、コントラクトが意図した通りに動作したかを確認します。builderメソッドが必要な情報を取得し、必要な値の型にそれを変換します。そして、assert_eq!()が想定していた値と結果を比較します。

    // Verify the value of count is now 1.
    let incremented_count = builder
        .query(None, count_key, &[])
        .expect("should be stored value.")
        .as_cl_value()
        .expect("should be cl value.")
        .clone()
        .into_t::<i32>()
        .expect("should be i32.");

    assert_eq!(incremented_count, 1);

他のテストサンプルについては、casper-node GitHubレポジトリをご確認ください。

コントラクトを呼び出すコントラクトのテスト

テストされたコードが複数のコントラクトに属す場合、テスト内にインストールされる必要があります。DEFAULT_RUN_GENESIS_REQUESTの一部としてインストールされたシステムコントラクトは、例外です。テストフレームワークは、Casperネットワークから独立しているため、元のコントラクトのインストールコードもしくは組み込みたいWasmへのアクセスが必要となります。

各コントラクトのインストールは、ExecuteRequestBuilderを使ったDeployを介してインストールされた追加のWasmファイルが必要となります。スマートコントラクトの作成者としての必要条件次第で、return valuesを用いたコントラクトスタックとのやり取りが必要になることもあります。コントラクト間の疎通には、セッションコードが必要となります。コントラクトは自動でアクションを実行しないため、処理の初期化が必要です。

セッションコードからのコントラクトの呼び出しとコントラクトコードからのそれにおける主な違いは、ExecuteRequestBuilderの標準以外の依存関係を使用可能とすることです。セッションコードが標準依存関係内にWasmファイルの場所を指定すると、コントラクトコードは他のコントラクトを呼び出す4つの利用可能なオプション(以下参照)を使用できます。

  • contract_call_by_hash – 対象のContractHashでのコントラクトの呼び出し
  • contract_call_by_name – 署名者のアカウントコンテキスト内の名前付き鍵に参照されるコントラクトの呼び出し
  • versioned_contract_call_by_hash – 対象のContractHashを使って指定のコントラクトバージョンの呼び出し
  • versioned_contract_call_by_name – 署名者のアカウントコンテキスト内の名前付き鍵に参照されるコントラクトの指定したバージョンの呼び出し

コントラクトの呼び出しは、エントリーポイントと全てのケースで必要となる実行引数を提供する必要があります。

テストの実行

テストを実行するにあたり、サンプルのカウンターではMakefileをしようします。

make test

内部では、Makefiletests/wasm フォルダーを生成し、Wasmファイルをそのフォルダーにコピーした上で cargo test を使ってテスト(tests)を実行します。

test: build-contract
    mkdir -p tests/wasm
    cp contract-v1/target/wasm32-unknown-unknown/release/counter-v1.wasm tests/wasm
    cp contract-v2/target/wasm32-unknown-unknown/release/counter-v2.wasm tests/wasm
    cp counter-call/target/wasm32-unknown-unknown/release/counter-call.wasm tests/wasm
    cd tests && cargo test

Video Walkthrough

下記のビデオでは、サンプルコントラクトコードのテストについて説明しています。

ユニットテスト

ユニットテストが、Casperネットワークにコントラクトをインストールする前にテストを行う唯一の方法です。コントラクトのユニットテスト後は、NCTLを使ったローカルネットワークテストを実施できます。これで、複数のローカルCasperノードをセットアップし操作可能となるので、他のシミュレーションされたネットワーク環境下でのテスト実施が行えます。

もしくは、Casper Testnet 上でコントラクトのテストを行うこともできます。

What’s Next?