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

はじめに

Casperの開発環境の一部として、フルノードを走らせる必要なく新しいコントラクトをテストするためのテストフレームを提供しています。フレームワークは、成功したトランザクションの確定とアサーションを使用したグローバルステートへの変更をモニターするCasperの実行エンジンのインスタンスを生成します。Casperのテストクレートは、検証用のWasmを生成するクレートと一緒にRustのワークスペース内に含まれる必要があります。

💡注意

Casperテストサポートのクレートは、Casper Networkにテスト用コントラクトを送信する前にそれらをテストする多くの選択肢の内の一つです。場合によっては、自身のテストフレームワークを作成することも可能です。

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 – テスト書くためのインターフェイスの提供と実行エンジンのインスタンスとの疎通を行うヘルプクレート
  • casper-types – Casper Network上での使用を目的とした多数のCasperクレートによって共有されている型

テスト作成

テスト用コントラクトは、通常、testsディレクトリに配置されます。カウンターコントラクトのテストは、tests/src/integration-tests.rsファイルにあります。このファイルは、空のmainメソッドを持っており、テストプログラムを初期化します。他には、この様に、ファイルのトップに#![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
}

ビルダーと定数のインポート

外部のテストサポートをインポートします。それには、あらゆる既定値やヘルプメソッドが含まれており、一連のテストを通して使用されます。更には、コントラクトコード内でテストされる際に使用するいずれの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を用いて、MintAuction(オークション)、HandlePaymentコントラクトが含まれており、デフォルトアカウントの作成にアソシエイトパース(紐づいたお財布)への資金投入も対象とするテストに必要なシステムコントラクトをインストールします。

    #[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を用いてテスト用のコントラクトをインストールします。カウンターテストの場合は、標準の依存関係とカウンターコントラクトを使用します。実行リクエストの内では、トランザクションを送信するアカウントとしてのジェネシスビルダーによって作られた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関数を用いて対象のエントリーポイントを呼び出します。以下のコードにて、インストールトランザクションを送信した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();

 

結果の評価と比較

コントラクトを呼び出した後、受け取った結果を検証し、コントラクトが意図していた通りに動作したかを確証するべきです。ビルダーメソッドは、必要な情報を取得し、それを必要な値の型に変換します。それから、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 Networkに対しても独立して存在しており、元のコントラクトインストレーションコードもしくは対象とするWasmにアクセスすることになります。

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

セッションコードとコントラクトコードのコントラクトの呼び出しにおける大きな違いは、ExecuteRequestBuilderに対して非標準依存関係が適用できるかどうかです。セッションコードは標準依存関係にてWasmファイルを指定しなくてはいけないが、コントラクトコードは他のコントラクトから呼び出す際に4つの選択肢の中から一つを選び使用できます。下記参照:

  • contract_call_by_hashContractHashを用いてコントラクトを呼び出す
  • contract_call_by_name – 署名者のAccountコンテキストにある名前付きキーによって参照されるコントラクトを呼び出す
  • versioned_contract_call_by_hash – 特定のコントラクトバージョンのContractHashを用いてそのコントラクトバージョンを呼び出す
  • versioned_contract_call_by_name – 署名者のAccountコンテキストにある名前付きキーによって参照される特定のコントラクトバージョンを呼び出す

コントラクトの呼び出しは、全てのケースにおいて、エントリーポイントと必要となる実行引数の提供も行わなくてはなりません。

テストの実行

テストを実行するには、カウンターのサンプル例ではMakefileを使用します。

make test

そうすると、Makefiletests/wasmフォルダーを生成し、Wasmファイルをそのフォルダーにコピーします。そして、cargo testを使用したテストを実行します。

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 Networkにテストコントラクトをインストールする前にテストを行う唯一の方法となります。コントラクトのユニットテスト後、NCTLを用いたローカルネットワークにおけるテストを実施することもあるかもしれません。それは、ローカルにて複数のCasperノードの設定とコントロールを可能とし、シミュレートされた別のネットワーク環境でのテスト実施が可能となります。

また、Casper Testnet上にて、コントラクトのテストも実施いただけます。

What’s Next?