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. 05使用 Aptos 命令测量覆盖率

适用场景

  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确认已加载。

详细介绍

生成测试技能

概述

此技能用于为 Move 合约生成全面的测试套件,要求达到 100% 行覆盖率。测试验证:

  • ✅ 正常路径(功能正常运行)
  • ✅ 访问控制(阻止未授权用户)
  • ✅ 输入验证(拒绝无效输入)
  • ✅ 边界情况(边界值、限制、空状态)

关键规则: 未达到 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% 覆盖率是强制要求。测试正常路径、错误路径、访问控制和边界情况。

评论

支持 Markdown