本教程详细的阐述了 Sui Move 单元测试的相关知识与实践方法。在合约模块发布到链上之前通过单元测试不仅能保障代码指令,也能减少gas成本。
如何保障代码质量?
在涉及交易资金的程序开发中,代码质量的保障至关重要,因为任何细微的代码漏洞都可能引发不可挽回的巨大损失。对于开发者而言,单元测试验证无疑是代码发布前的关键环节。
对于 Sui Move 语言而言,其拥有一套完备的单元测试体系,借助特定的注解和命令,开发者可以高效地对代码进行测试与验证。
Move 语言的单元测试在 Move 源语言中使用了三种注解:
#[test]
:将一个函数标记为测试函数;#[expected_failure]
:标记一个测试预计会失败;#[test_only]
:将一个模块或模块成员(使用、函数、结构体或常量)标记为仅用于测试的代码。这些注解可以放置在任何具有合适形式及任何可见性的地方。每当一个模块或模块成员被标注为#[test_only]
或#[test]
时,除非是为了测试而进行编译,否则它不会被包含在编译后的字节码中。
#[test]
注解#[test]
注解只能放置在无参数的函数上。此注解将该函数标记为一个将由单元测试框架运行的测试函数。
#[test]
fun valid_test_function() {
// 测试逻辑代码
}
#[test]
fun invalid_test_function(arg: u64) {
// 此函数因有参数,不符合#[test]注解要求,将无法正确编译
}
#[expected_failure
注解#[expected_failure]
注解用于指定预期的错误条件,并确保测试符合这些预期的失败条件。可以通过不同的方式使用该注解,具体如下:
#[expected_failure(abort_code = <constant>)]
如果测试在定义该常量的模块中以指定的常量值终止,则测试通过,否则失败。这是测试预期测试失败的推荐方法。
module test_example::my_module {
const SPECIFIC_ERROR_CODE: u64 = 100;
#[test]
#[expected_failure(abort_code = SPECIFIC_ERROR_CODE)]
fun test_specific_abort_code() {
// 执行某些操作,预期会以 SPECIFIC_ERROR_CODE 终止
abort SPECIFIC_ERROR_CODE;
}
}
#[expected_failure(arithmetic_error, location = <location>)]
这个注解用于指定测试应该因为算术错误(如整数溢出、除以零等)而失败。<location>
参数必须是有效的模块路径,例如 Self
或 my_package::my_module
。
module test_example::my_module {
#[test]
#[expected_failure(arithmetic_error, location = Self)]
fun test_self_arithmetic_error() {
let result: u64 = 1 / 0; // 引发算术错误
}
#[test]
#[expected_failure(arithmetic_error, location = my_package::other_module)]
fun test_other_module_arithmetic_error() {
// 调用其他模块中会引发算术错误的函数
my_package::other_module::arithmetic_error_function();
}
}
#[expected_failure(out_of_gas, location = <location>)]
此注解指定测试应因“耗尽 gas”错误而失败。<location>
参数必须是有效的模块路径,例如 Self
或 my_package::my_module
。
module test_example::my_module {
#[test]
#[expected_failure(out_of_gas, location = Self)]
fun test_self_out_of_gas() {
// 执行一个会耗尽 gas 的循环或操作
loop {
// 一些消耗 gas 的代码
}
}
#[test]
#[expected_failure(out_of_gas, location = my_package::other_module)]
fun test_other_module_out_of_gas() {
// 调用其他模块中会耗尽 gas 的函数
my_package::other_module::out_of_gas_function();
}
}
#[expected_failure(vector_error, minor_status = <u64_opt>, location = <location>)]
module test_example::my_module {
#[test]
#[expected_failure(vector_error, location = Self)]
fun test_self_vector_error() {
// 引发向量错误的操作,如访问空向量等
let empty_vector: vector<u64> = vector::empty();
let value: u64 = empty_vector[0];
}
#[test]
#[expected_failure(vector_error, minor_status = 1, location = Self)]
fun test_self_vector_error_with_status() {
// 引发特定次要状态向量错误的操作
// 假设这里有一个函数会返回具有特定 minor_status 的向量错误
vector_error_function_with_status();
}
}
#[expected_failure]
此注解用于指定测试应因任何错误代码而失败。使用时应小心,优先考虑使用上面描述的更具体的方式。
#[test]
#[expected_failure]
fun test_pow_overflow() {
math::pow(10, 100);
}
#[test_only]
注解一个模块及其任何成员都可以被声明为仅用于测试。
#[test_only]
属性标记的代码仅在测试环境中有效,确保它不会出现在生产环境中。use
语句、结构体和函数,旨在隔离测试代码和生产代码。注意:标注为
#[test_only]
的函数只能从测试代码中调用,但它们本身不是测试函数,不会被单元测试框架作为测试来运行。
- 在模块上使用
#[test_only]
属性
```rust
#[test_only] // 仅限测试的属性可以附加到模块
module abc { ... }
```
在常量上使用 #[test_only]
属性
#[test_only] // 仅限测试的属性可以附加到常量
const MY_ADDR: address = @0x1;
在 use
语句上使用 #[test_only]
属性
#[test_only] // .. 附加到 `use` 语句
use pkg_addr::some_other_module;
在结构体上使用 #[test_only]
属性
#[test_only] // .. 附加到结构体
public struct SomeStruct { ... }
在函数上使用 #[test_only]
属性
#[test_only] // .. 附加到函数。只能从测试代码中调用,但这不是一个测试!
fun test_only_function(...) { ... }
可以使用sui move test
命令来运行 Move 包的单元测试。运行测试时,每个测试的结果要么是通过(PASS)、失败(FAIL),要么是超时(TIMEOUT)。如果一个测试用例失败,尽可能会报告失败的位置以及导致失败的函数名称。
$ sui move new test_example
$ cd test_example
sources
目录下面创建my_module.move
模块, 测试代码如下module test_example::my_module {
// 定义一个名为 `Wrapper` 的公共结构体,包含一个 `u64` 类型的字段
public struct Wrapper(u64);
// 定义常量 `ECoinIsZero`,值为 0,用于表示“硬币为零”的错误代码
const ECoinIsZero: u64 = 0;
// 定义一个公共函数 `make_sure_non_zero_coin`,确保传入的硬币值大于零
public fun make_sure_non_zero_coin(coin: Wrapper): Wrapper {
// 如果硬币的值小于或等于零,则触发断言错误,使用 `ECoinIsZero` 作为错误代码
assert!(coin.0 > 0, ECoinIsZero);
// 如果断言通过,返回原始的 `Wrapper` 结构
coin
}
// 测试函数:验证 `make_sure_non_zero_coin` 在硬币值大于零时的正常工作
#[test]
fun make_sure_non_zero_coin_passes() {
// 创建一个硬币值为 1 的 `Wrapper` 结构
let coin = Wrapper(1);
// 调用 `make_sure_non_zero_coin` 函数,断言返回的值是包装过的硬币
let Wrapper(_) = make_sure_non_zero_coin(coin);
}
// 测试函数:验证 `make_sure_non_zero_coin` 在硬币值为零时的失败情况
#[test]
// 使用 `#[expected_failure]` 来标记测试预期会失败,如果我们不关心终止代码时
#[expected_failure(abort_code = ECoinIsZero)]
fun make_sure_zero_coin_fails() {
// 创建一个硬币值为 0 的 `Wrapper` 结构
let coin = Wrapper(0);
// 调用 `make_sure_non_zero_coin`,预期会触发断言错误
let Wrapper(_) = make_sure_non_zero_coin(coin);
}
// 仅用于测试的辅助函数:将硬币的值设为零
#[test_only]
fun make_coin_zero(coin: &mut Wrapper) {
coin.0 = 0;
}
// 测试函数:验证在硬币值为零时,`make_sure_non_zero_coin` 触发错误
#[test]
// 仍然使用 `#[expected_failure]` 来标记此测试预期会失败
#[expected_failure(abort_code = ECoinIsZero)]
fun make_sure_zero_coin_fails2() {
// 创建一个硬币值为 10 的 `Wrapper` 结构
let mut coin = Wrapper(10);
// 调用辅助函数 `make_coin_zero` 将硬币值设为 0
coin.make_coin_zero();
// 调用 `make_sure_non_zero_coin`,预期会触发断言错误
let Wrapper(_) = make_sure_non_zero_coin(coin);
}
}
在源码的根目录执行sui move test
可以使用sui move test <字符串>
来运行特定的测试或一组测试。
下面测试分别运行了包含non_zero
字符串和包含zero_coin
的所有测试方法。
-i <bound>
或 --gas_used <bound>
参数允许你设置测试的 Gas 限制,帮助防止在执行过程中测试消耗过多 Gas 或用尽 Gas。通过设置这个限制,你可以控制每个测试的 Gas 使用量,确保在消耗过多 Gas 时能够提前失败,从而避免不必要的执行或长时间等待。
PS D:\data\web3\move_course\test_example> sui move test -i 0 --skip-fetch-latest-git-deps
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING test_example
Running Move unit tests
[ TIMEOUT ] test_example::my_module::make_sure_non_zero_coin_passes
[ FAIL ] test_example::my_module::make_sure_zero_coin_fails
[ FAIL ] test_example::my_module::make_sure_zero_coin_fails2
Test failures:
Failures in test_example::my_module:
┌── make_sure_non_zero_coin_passes ──────
│ Test timed out
└──────────────────
┌── make_sure_zero_coin_fails ──────
│ error[E11001]: test failure
│ ┌─ .\sources\my_module.move:22:28
│ │
│ 21 │ fun make_sure_zero_coin_fails() {
│ │ ------------------------- In this function in test_example::my_module
│ 22 │ let coin = Wrapper(0);
│ │ ^ Test did not error as expected. Expected test to abort with code 0 originating in the module test_example::my_module but instead it ran out of gas in the module test_example::my_module rooted here
│
│
└──────────────────
┌── make_sure_zero_coin_fails2 ──────
│ error[E11001]: test failure
│ ┌─ .\sources\my_module.move:34:32
│ │
│ 33 │ fun make_sure_zero_coin_fails2() {
│ │ -------------------------- In this function in test_example::my_module
│ 34 │ let mut coin = Wrapper(10);
│ │ ^^ Test did not error as expected. Expected test to abort with code 0 originating in the module test_example::my_module but instead it ran out of gas in the module test_example::my_module rooted here
│
│
└──────────────────
Test result: FAILED. Total tests: 3; passed: 0; failed: 3
PS D:\data\web3\move_course\test_example>
-s
或 --statistics
参数允许你收集有关测试运行的统计信息,并报告每个测试的运行时间和 Gas 使用情况。
如果需要导出csv格式也可以用 sui move test -s csv 命令。
Q1. 直接运行 sui move test报错, Failed to resolve dependencies for package 'test_example' A: 加上参数 sui move test --skip-fetch-latest-git-deps 运行成功
<!--StartFragment-->
<!--EndFragment--> <!--StartFragment-->
<!--EndFragment--> <!--StartFragment-->
关注《HOH水分子》公众号,我们将持续分享和制作变成语言教程,让大家对编程产生化学反应。
<!--EndFragment-->
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!