mcpField
G
generate-tests

generate-tests

github.com/aptos-labsテスト・品質2097 回インストール24 週間前に更新
リポジトリ

generate-tests について

Aptos Move コントラクトのテストを作成し、正常動作、アクセス制御、不正な入力、境界ケースを扱うスキルです。カバレッジレポートを基にテストを追加し、記載された行カバレッジ 100% の要件を目指します。

主な機能

  1. 01コントラクトの正常動作をテスト
  2. 02権限とアクセス拒否を確認
  3. 03入力検証と境界ケースを網羅
  4. 04期待される失敗でエラーパスをテスト
  5. 05Aptos コマンドでカバレッジを測定

活用シーン

  1. 01デプロイ前に新しい Move コントラクトをテスト
  2. 02未認可呼び出しや不正入力のテストを追加
  3. 03未カバー行を特定してテストを拡充
  4. 04状態変化とエラーコードを検証

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/aptos-labs/aptos-agent-skills --skill generate-tests

インストール後、Claude Code で/generate-testsを入力して読み込まれたか確認します。

詳細

テスト生成スキル

概要

このスキルは、100%の行カバレッジを必須とする、Move コントラクト向けの包括的なテストスイートを生成します。テストでは、以下を検証します。

  • ✅ 正常系(機能が動作する)
  • ✅ アクセス制御(権限のないユーザーがブロックされる)
  • ✅ 入力検証(無効な入力が拒否される)
  • ✅ エッジケース(境界値、上限、空の状態)

重要なルール: テストカバレッジが100%になるまで、絶対にデプロイしないでください。

基本的なワークフロー

ステップ1:テストモジュールを作成する
#[test_only]
module my_addr::my_module_tests {
    use my_addr::my_module::{Self, MyObject};
    use aptos_framework::object::{Self, Object};
    use std::string;
    use std::signer;

    // Test constants
    const ADMIN_ADDR: address = @0x100;
    const USER_ADDR: address = @0x200;
    const ATTACKER_ADDR: address = @0x300;

    // ========== Setup Helpers ==========
    // (Reusable setup functions)

    // ========== Happy Path Tests ==========
    // (Basic functionality)

    // ========== Access Control Tests ==========
    // (Unauthorized access blocked)

    // ========== Input Validation Tests ==========
    // (Invalid inputs rejected)

    // ========== Edge Case Tests ==========
    // (Boundaries and limits)
}
ステップ2:正常系のテストを記述する

基本機能が正しく動作することをテストします。

#[test(creator = @0x1)]
public fun test_create_object_succeeds(creator: &signer) {
    // Execute
    let obj = my_module::create_my_object(
        creator,
        string::utf8(b"Test Object")
    );

    // Verify
    assert!(object::owner(obj) == signer::address_of(creator), 0);
}

#[test(owner = @0x1)]
public fun test_update_object_succeeds(owner: &signer) {
    // Setup
    let obj = my_module::create_my_object(owner, string::utf8(b"Old Name"));

    // Execute
    let new_name = string::utf8(b"New Name");
    my_module::update_object(owner, obj, new_name);

    // Verify (if you have view functions)
    // assert!(my_module::get_object_name(obj) == new_name, 0);
}

#[test(owner = @0x1, recipient = @0x2)]
public fun test_transfer_object_succeeds(
    owner: &signer,
    recipient: &signer
) {
    let recipient_addr = signer::address_of(recipient);

    // Setup
    let obj = my_module::create_my_object(owner, string::utf8(b"Object"));
    assert!(object::owner(obj) == signer::address_of(owner), 0);

    // Execute
    my_module::transfer_object(owner, obj, recipient_addr);

    // Verify
    assert!(object::owner(obj) == recipient_addr, 1);
}
ステップ3:アクセス制御のテストを記述する

権限のないアクセスがブロックされることをテストします。

#[test(owner = @0x1, attacker = @0x2)]
#[expected_failure(abort_code = my_module::E_NOT_OWNER)]
public fun test_non_owner_cannot_update(
    owner: &signer,
    attacker: &signer
) {
    let obj = my_module::create_my_object(owner, string::utf8(b"Object"));

    // Attacker tries to update (should abort)
    my_module::update_object(attacker, obj, string::utf8(b"Hacked"));
}

#[test(owner = @0x1, attacker = @0x2)]
#[expected_failure(abort_code = my_module::E_NOT_OWNER)]
public fun test_non_owner_cannot_transfer(
    owner: &signer,
    attacker: &signer
) {
    let obj = my_module::create_my_object(owner, string::utf8(b"Object"));

    // Attacker tries to transfer (should abort)
    my_module::transfer_object(attacker, obj, @0x3);
}

#[test(admin = @0x1, user = @0x2)]
#[expected_failure(abort_code = my_module::E_NOT_ADMIN)]
public fun test_non_admin_cannot_configure(
    admin: &signer,
    user: &signer
) {
    my_module::init_module(admin);

    // Regular user tries admin function (should abort)
    my_module::update_config(user, 100);
}
ステップ4:入力検証のテストを記述する

無効な入力が拒否されることをテストします。

#[test(user = @0x1)]
#[expected_failure(abort_code = my_module::E_ZERO_AMOUNT)]
public fun test_zero_amount_rejected(user: &signer) {
    my_module::deposit(user, 0); // Should abort
}

#[test(user = @0x1)]
#[expected_failure(abort_code = my_module::E_AMOUNT_TOO_HIGH)]
public fun test_excessive_amount_rejected(user: &signer) {
    my_module::deposit(user, my_module::MAX_DEPOSIT_AMOUNT + 1); // Should abort
}

#[test(owner = @0x1)]
#[expected_failure(abort_code = my_module::E_EMPTY_NAME)]
public fun test_empty_string_rejected(owner: &signer) {
    let obj = my_module::create_my_object(owner, string::utf8(b"Initial"));
    my_module::update_object(owner, obj, string::utf8(b"")); // Empty - should abort
}

#[test(owner = @0x1)]
#[expected_failure(abort_code = my_module::E_NAME_TOO_LONG)]
public fun test_string_too_long_rejected(owner: &signer) {
    let obj = my_module::create_my_object(owner, string::utf8(b"Initial"));

    // String exceeding MAX_NAME_LENGTH
    let long_name = string::utf8(b"This is an extremely long name that exceeds the maximum allowed length");

    my_module::update_object(owner, obj, long_name); // Should abort
}

#[test(owner = @0x1)]
#[expected_failure(abort_code = my_module::E_ZERO_ADDRESS)]
public fun test_zero_address_rejected(owner: &signer) {
    let obj = my_module::create_my_object(owner, string::utf8(b"Object"));
    my_module::transfer_object(owner, obj, @0x0); // Should abort
}
ステップ5:エッジケースのテストを記述する

境界条件をテストします。

#[test(user = @0x1)]
public fun test_max_amount_allowed(user: &signer) {
    my_module::init_account(user);

    // Exactly MAX_DEPOSIT_AMOUNT should work
    my_module::deposit(user, my_module::MAX_DEPOSIT_AMOUNT);

    // Verify
    assert!(my_module::get_balance(signer::address_of(user)) == my_module::MAX_DEPOSIT_AMOUNT, 0);
}

#[test(user = @0x1)]
public fun test_max_name_length_allowed(user: &signer) {
    // Create string exactly MAX_NAME_LENGTH long
    let max_name = string::utf8(b"12345678901234567890123456789012"); // 32 chars if MAX = 32

    // Should succeed
    let obj = my_module::create_my_object(user, max_name);
}

#[test(user = @0x1)]
public fun test_empty_collection_operations(user: &signer) {
    let collection = my_module::create_collection(user, string::utf8(b"Collection"));

    // Should handle empty collection gracefully
    assert!(my_module::get_collection_size(collection) == 0, 0);
}
ステップ6:カバレッジを確認する

カバレッジ付きでテストを実行します。

# Run all tests
aptos move test

# Run with coverage
aptos move test --coverage

# Generate detailed coverage report
aptos move coverage source --module <module_name>

# Verify 100% coverage
aptos move coverage summary

カバレッジレポートの例:

module: my_module
coverage: 100.0% (150/150 lines covered)

カバレッジが100%未満の場合:

  1. レポートでカバーされていない行を確認する
  2. 不足しているパスのテストを記述する
  3. 100%になるまで繰り返す

テストテンプレートの構成

#[test_only]
module my_addr::module_tests {
    use my_addr::module::{Self, Type};

    // ========== Setup Helpers ==========

    fun setup_default(): Object<Type> {
        // Common setup code
    }

    // ========== Happy Path Tests ==========

    #[test(user = @0x1)]
    public fun test_basic_operation_succeeds(user: &signer) {
        // Test happy path
    }

    // ========== Access Control Tests ==========

    #[test(owner = @0x1, attacker = @0x2)]
    #[expected_failure(abort_code = E_NOT_OWNER)]
    public fun test_unauthorized_access_fails(
        owner: &signer,
        attacker: &signer
    ) {
        // Test access control
    }

    // ========== Input Validation Tests ==========

    #[test(user = @0x1)]
    #[expected_failure(abort_code = E_INVALID_INPUT)]
    public fun test_invalid_input_rejected(user: &signer) {
        // Test input validation
    }

    // ========== Edge Case Tests ==========

    #[test(user = @0x1)]
    public fun test_boundary_condition(user: &signer) {
        // Test edge cases
    }
}

テストチェックリスト

各コントラクトについて、以下のテストがあることを確認してください。

正常系:

  • オブジェクトを作成できる
  • 状態を更新できる
  • 転送できる
  • すべての主要機能が動作する

アクセス制御:

  • 所有者以外はオブジェクトを変更できない
  • 管理者以外は管理者用関数を呼び出せない
  • 権限のないユーザーがブロックされる

入力検証:

  • ゼロの金額が拒否される
  • 過剰な金額が拒否される
  • 空の文字列が拒否される
  • 長すぎる文字列が拒否される
  • ゼロアドレスが拒否される

エッジケース:

  • 最大値で動作する
  • 最小値で動作する
  • 空の状態が適切に処理される

カバレッジ:

  • 行カバレッジ100%を達成している
  • すべてのエラーコードをテストしている
  • すべての関数をテストしている

必ず守るルール

  • ✅ 必ずテストカバレッジ100%を達成する
  • ✅ 必ず #[expected_failure(abort_code = E_CODE)] を使ってエラーパスをテストする
  • ✅ 必ず複数の署名者を使ってアクセス制御をテストする
  • ✅ 必ず無効な入力を使って入力検証をテストする
  • ✅ 必ずエッジケース(境界値、上限、空の状態)をテストする
  • ✅ 必ず明確なテスト名を使う:test_feature_scenario
  • ✅ 必ずテストで状態変更をすべて検証する
  • ✅ 必ずデプロイ前に aptos move test --coverage を実行する

絶対にしてはいけないこと

  • ❌ カバレッジ100%を達成せずにデプロイしない
  • ❌ エラーパスのテストを省略しない
  • ❌ アクセス制御のテストを省略しない
  • ❌ わかりにくいテスト名を使わない
  • ❌ 各ケースを検証せずにテストをまとめて実行しない
  • ❌ テストコードに実際の秘密鍵やアカウントアドレスをハードコードしない — @0x1、@0x100、@0xCAFE などのテスト用アドレスを使う
  • ❌ テスト用アドレスを取得するために .env や ~/.aptos/config.yaml を読み込まない

よくある落とし穴

モジュール間での構造体フィールドへのアクセス

問題: テストモジュールから、他のモジュールの構造体フィールドに直接アクセスできません。

// ❌ WRONG - Will NOT compile
let listing = marketplace::get_listing(nft_addr);
assert!(listing.price == 1000, 0);  // ERROR: field access not allowed

解決策: メインモジュールのパブリックなビュー用アクセサー関数を使います。

// ✅ CORRECT - Use accessor function
let (seller, price, timestamp) = marketplace::get_listing_details(nft_addr);
assert!(price == 1000, 0);

モジュールにアクセサーがない場合は、追加します。

// In main module
#[view]
public fun get_listing_details(nft_addr: address): (address, u64, u64) acquires Listings {
    let listing = table::borrow(&listings.items, nft_addr);
    (listing.seller, listing.price, listing.listed_at)
}
エスクローパターンでのエラー期待値

問題: NFT をエスクローに出品すると、出品者はその NFT の所有者ではなくなります。

// ❌ WRONG expectation
#[expected_failure(abort_code = marketplace::E_ALREADY_LISTED)]
public fun test_cannot_list_twice(seller: &signer) {
    list_nft(seller, nft, 1000);  // NFT transfers to marketplace
    list_nft(seller, nft, 2000);  // Fails with E_NOT_OWNER, not E_ALREADY_LISTED!
}

解決策: 検証の順序を理解してください。所有権は出品状態より先に確認されます。

// ✅ CORRECT expectation
#[expected_failure(abort_code = marketplace::E_NOT_OWNER)]
public fun test_cannot_list_twice(seller: &signer) {
    list_nft(seller, nft, 1000);  // NFT transfers to marketplace
    list_nft(seller, nft, 2000);  // Seller doesn't own it -> E_NOT_OWNER
}
Acquires 注釈のエラー

問題: フレームワーク関数が借用するリソースに対して acquires を追加すると、エラーが発生します。

// ❌ WRONG - framework handles its own acquires
public entry fun stake(...) acquires VaultConfig, Stakes, StakeTokenRefs {
    primary_fungible_store::transfer(...);  // Don't list what framework borrows
}

解決策: 自分のコードが借用するリソースだけを列挙します。

// ✅ CORRECT
public entry fun stake(...) acquires VaultConfig, Stakes {
    let config = borrow_global<VaultConfig>(...);  // You borrow this
    primary_fungible_store::transfer(...);          // Framework handles its own
}

参考資料

パターンのドキュメント:

  • ../../../patterns/move/TESTING.md - 包括的なテストガイド(モジュール間の問題についてはパターン8を参照)
  • ../../../patterns/move/SECURITY.md - セキュリティテストの要件

公式ドキュメント:

関連スキル:

  • write-contracts - テスト対象のコードを生成する
  • security-audit - テスト後にセキュリティを検証する

忘れないでください: カバレッジ100%は必須です。正常系、エラーパス、アクセス制御、エッジケースをテストしてください。

コメント