Sui智能合约单元测试:编写可靠的测试用例
引言:为什么智能合约测试至关重要
在区块链开发中,智能合约的安全性和正确性直接关系到用户资产安全和系统稳定性。Sui作为新一代智能合约平台,采用Move语言提供了强大的资产模型和类型安全保障,但这并不意味着开发者可以忽视测试环节。根据区块链安全报告显示,超过70%的智能合约漏洞源于逻辑错误,而完善的单元测试体系能有效预防这些问题。
本文将系统讲解Sui智能合约单元测试的完整流程,从基础语法到高级测试策略,帮助开发者构建健壮的测试用例。通过本文,你将掌握:
- Move单元测试框架的核心组件与使用方法
- 测试场景模拟(Test Scenario)的高级应用
- 边界条件与异常处理的测试技巧
- 测试覆盖率分析与优化方法
- 测试自动化与CI/CD集成实践
一、Move单元测试基础
1.1 测试环境搭建
Sui智能合约的单元测试通过#[test]注解标识,使用move命令行工具执行。在Sui项目中,测试文件通常位于tests目录下,或与被测试代码共存于同一模块中(使用#[test_only]修饰)。
# 克隆Sui仓库
git clone https://gitcode.com/GitHub_Trending/su/sui.git
cd sui
# 运行单个测试文件
sui move test --path examples/move/locked_stake
# 运行指定测试函数
sui move test --path examples/move/locked_stake --filter test_unlock_correct_epoch
1.2 基本测试结构
一个标准的Move测试包含三个核心部分:环境准备、操作执行和结果验证。以下是来自first_package示例的基础测试模板:
#[test]
fun test_sword_create() {
// 1. 环境准备:创建测试上下文和对象
let mut ctx = tx_context::dummy();
let sword = Sword {
id: object::new(&mut ctx),
magic: 42,
strength: 7,
};
// 2. 操作执行:调用被测试函数(此处直接构造对象)
// 3. 结果验证:使用assert!宏检查条件
assert!(sword.magic() == 42 && sword.strength() == 7, 1);
// 4. 清理:测试对象处理(可选)
let dummy_address = @0xCAFE;
transfer::public_transfer(sword, dummy_address);
}
1.3 测试注解与修饰符
Move提供了丰富的测试相关注解,用于控制测试行为:
| 注解 | 作用 | 示例 |
|---|---|---|
#[test] | 标记测试函数 | #[test] fun test_name() {} |
#[test_only] | 标记仅测试时可见的代码 | #[test_only] module my_module::tests {} |
#[expected_failure] | 标记预期失败的测试 | #[test] #[expected_failure(abort_code = 1)] fun test_error() {} |
#[sim_test] | 标记需要模拟器的复杂测试 | #[sim_test] fun test_parallel_execution() {} |
二、测试场景模拟(Test Scenario)
2.1 单交易场景
Sui提供的test_scenario模块允许开发者模拟区块链交易环境,包括账户、对象所有权和交易执行流程。基础用法如下:
#[test]
fun test_sword_transactions() {
use sui::test_scenario;
// 创建测试地址
let initial_owner = @0xCAFE;
let final_owner = @0xFACE;
// 开始第一个交易(由initial_owner执行)
let mut scenario = test_scenario::begin(initial_owner);
{
let sword = sword_create(42, 7, scenario.ctx());
transfer::public_transfer(sword, initial_owner);
};
// 开始第二个交易(仍由initial_owner执行)
scenario.next_tx(initial_owner);
{
let sword = scenario.take_from_sender<Sword>();
transfer::public_transfer(sword, final_owner);
};
// 验证最终状态
scenario.next_tx(final_owner);
{
let sword = scenario.take_from_sender<Sword>();
assert!(sword.magic() == 42 && sword.strength() == 7, 1);
scenario.return_to_sender(sword);
};
scenario.end();
}
2.2 多角色交互场景
对于涉及多个参与者的复杂合约(如NFT租赁、交易市场),test_scenario能够模拟不同地址之间的交互:
#[test]
fun test_nft_rental_flow() {
use sui::test_scenario as ts;
// 定义角色地址
let owner = @0xOWNER;
let renter = @0xRENTER;
// 初始化场景
let mut scenario = ts::begin(owner);
// 1. 所有者创建NFT并放置到租赁市场
let nft = create_nft(ts.ctx());
let market_id = create_rental_market(owner, ts.ctx());
scenario.place_in_market(owner, market_id, nft);
// 2. 租户租用NFT
scenario.next_tx(renter);
scenario.rent_nft(renter, market_id, nft.id(), 1000, ts.ctx());
// 3. 验证租赁状态
scenario.next_tx(owner);
let rental_info = scenario.get_rental_info(market_id, nft.id());
assert!(rental_info.status == RENTED && rental_info.renter == renter, 1);
scenario.end();
}
2.3 时间与Epoch模拟
许多Sui合约依赖时间或Epoch信息(如锁仓释放、定期奖励),测试时需要精确控制这些变量:
#[test]
fun test_unlock_correct_epoch() {
let mut scenario = test_scenario::begin(@0x0);
// 设置初始Epoch
set_up_sui_system_state(vector[@0x1, @0x2, @0x3]);
// 创建锁仓对象(要求Epoch 2后解锁)
let mut ls = locked_stake::new(2, test_scenario::ctx(scenario));
ls.deposit_sui(balance::create_for_testing(100 * 1_000_000_000));
// 推进Epoch(模拟区块链时间流逝)
advance_epoch(scenario); // Epoch 1
advance_epoch(scenario); // Epoch 2
advance_epoch(scenario); // Epoch 3(已满足解锁条件)
// 执行解锁操作
let (staked_sui, sui_balance) = ls.unlock(ls, test_scenario::ctx(scenario));
// 验证结果
assert_eq!(balance::value(&sui_balance), 90 * 1_000_000_000);
assert_eq!(vec_map::length(&staked_sui), 1);
}
三、高级测试策略
3.1 边界条件测试
优秀的单元测试需要覆盖各种边界情况。以代币转账为例,应测试:
- 正常转账(金额等于余额)
- 零金额转账
- 超额转账(预期失败)
- 地址有效性检查
#[test]
#[expected_failure(abort_code = 1)] // 预期因余额不足失败
fun test_transfer_exceed_balance() {
let mut ctx = tx_context::dummy();
let mut balance = balance::create_for_testing(100); // 100枚代币
// 尝试转账200枚(超出余额)
coin::transfer(&mut balance, @0xDEAD, 200, &mut ctx);
}
3.2 异常处理测试
Move使用abort指令终止异常流程,测试时需验证特定错误码是否正确抛出:
#[test]
#[expected_failure(abort_code = epoch_time_lock::EEpochNotYetEnded)]
fun test_unlock_too_early() {
let mut scenario = test_scenario::begin(@0x0);
set_up_sui_system_state(vector[@0x1, @0x2, @0x3]);
// 创建要求Epoch 5解锁的对象
let ls = locked_stake::new(5, test_scenario::ctx(scenario));
// 仅推进到Epoch 3就尝试解锁
advance_epoch(scenario); // Epoch 1
advance_epoch(scenario); // Epoch 2
advance_epoch(scenario); // Epoch 3
// 此操作应失败并返回EEpochNotYetEnded错误
let (_, _) = ls.unlock(ls, test_scenario::ctx(scenario));
}
3.3 覆盖率分析与优化
Sui提供测试覆盖率工具,帮助识别未测试代码:
# 生成覆盖率报告
sui move coverage --path examples/move/locked_stake --html
# 查看报告(在浏览器中打开)
open target/coverage/index.html
覆盖率优化策略:
- 确保每个public函数至少有一个测试用例
- 为条件分支(if/else)提供正反测试
- 测试所有abort路径
- 关注核心业务逻辑的全覆盖
四、测试自动化与最佳实践
4.1 测试组织方式
推荐采用"一个模块一个测试文件"的组织方式,复杂场景可拆分为多个测试函数:
move_project/
├── sources/
│ └── locked_stake.move
└── tests/
├── locked_stake_basic_tests.move # 基础功能测试
├── locked_stake_epoch_tests.move # Epoch相关测试
└── locked_stake_edge_tests.move # 边界条件测试
4.2 CI/CD集成
Sui项目使用GitHub Actions实现测试自动化,典型配置如下(.github/workflows/move-tests.yml):
name: Move Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Sui
uses: MystenLabs/setup-sui@main
with:
sui-version: 'latest'
- name: Run tests
run: |
sui move test --path crates/sui-framework
sui move test --path examples/move
4.3 测试性能优化
大型项目的测试套件可能耗时较长,可通过以下方式优化:
- 并行测试:使用
sui move test --jobs 4启用多线程测试 - 测试过滤:使用
--filter只运行相关测试 - 轻量级模拟:对复杂依赖使用mock对象
- 测试数据复用:在测试间共享静态测试数据
五、案例研究:NFT租赁合约测试
以下是Sui生态中典型的复杂合约测试案例,展示了如何测试包含权限控制、状态转换和经济逻辑的非同质化代币租赁系统:
#[test]
fun test_rental_lifecycle() {
let mut ts = test_scenario::begin(RENTER);
// 1. 环境初始化
let item = T { id: object::new(ts.ctx()) };
let item_id = object::id(&item);
let clock = clock::create_for_testing(ts.ctx());
// 2. 创建测试角色与市场
let witness = WITNESS {};
let publisher = package::test_claim(witness, ts.ctx());
let renter_kiosk_id = create_kiosk(RENTER, ts.ctx());
let borrower_kiosk_id = create_kiosk(BORROWER, ts.ctx());
// 3. 上架NFT
ts.setup(RENTER, &publisher, 50);
ts.place_in_kiosk(RENTER, renter_kiosk_id, item);
ts.install_ext(RENTER, renter_kiosk_id);
ts.list_for_rent(RENTER, renter_kiosk_id, item_id, 10, 10);
// 4. 租用NFT
ts.install_ext(BORROWER, borrower_kiosk_id);
ts.rent(BORROWER, renter_kiosk_id, borrower_kiosk_id, item_id, 100, &clock);
// 5. 验证状态变化
let rental_info = ts.get_rental_info(renter_kiosk_id, item_id);
assert!(rental_info.status == RENTED, 1);
assert!(rental_info.renter == BORROWER, 2);
// 6. 归还NFT
let promise = ts.borrow_val(BORROWER, borrower_kiosk_id, item_id);
ts.return_val(promise, BORROWER, borrower_kiosk_id);
// 7. 验证最终状态
let final_info = ts.get_rental_info(renter_kiosk_id, item_id);
assert!(final_info.status == AVAILABLE, 3);
clock.destroy_for_testing();
publisher.burn_publisher();
ts.end();
}
这个测试覆盖了NFT租赁的完整生命周期:从创建、上架、租用、使用到归还,验证了每个环节的状态转换和权限控制。特别注意对时钟对象的使用,确保租赁期限逻辑正确。
六、总结与展望
Sui智能合约的单元测试是保障代码质量的关键环节,通过本文介绍的测试方法和最佳实践,开发者可以构建全面的测试套件,有效降低生产环境中的风险。随着Sui生态的发展,测试工具链也在不断完善,未来将支持更复杂的场景测试和形式化验证。
测试 checklist:
- 每个public函数至少有一个测试用例
- 覆盖所有条件分支和异常路径
- 验证对象所有权和状态转换
- 测试时间和Epoch相关逻辑
- 进行边界条件和压力测试
- 确保测试覆盖率>80%
- 集成到CI/CD流程
通过持续改进测试策略,开发者不仅能提升代码质量,还能在开发早期发现潜在问题,降低修复成本。记住:在区块链世界,一个未测试的合约比没有合约更危险。
附录:常用测试工具与资源
测试框架
- Sui Test Scenario:
sui::test_scenario- 模拟区块链交易环境 - Move Unit Test: 基础测试框架,提供
#[test]注解和断言宏 - Sui Simulator: 用于测试并行执行和复杂场景的高级工具
辅助库
sui::balance- 测试余额操作sui::coin- 代币相关测试sui::clock- 时间相关测试sui::kiosk_test_utils- Kiosk合约测试工具
官方示例
- Locked Stake Example - Epoch锁仓测试
- NFT Rental Example - 复杂状态机测试
- Token Example - 代币经济模型测试
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



