关于 generate-tests
此技能用于为 Aptos Move 合约编写测试,涵盖正常行为、访问控制、无效输入和边界情况。它通过覆盖率报告指导补充测试,以达到文中要求的 100% 行覆盖率。
主要功能
- 01测试合约的正常行为
- 02检查授权和未授权访问
- 03覆盖输入验证与边界情况
- 04用预期失败测试错误路径
- 05使用 Aptos 命令测量覆盖率
适用场景
- 01部署前测试新的 Move 合约
- 02为未授权调用和无效输入补充测试
- 03查找未覆盖代码并扩展测试套件
- 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%:
- 检查报告中未覆盖的代码行
- 为缺失的执行路径编写测试
- 重复上述步骤,直到达到 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