Sui move单测从入门到精通

本教程详细的阐述了 Sui Move 单元测试的相关知识与实践方法。在合约模块发布到链上之前通过单元测试不仅能保障代码指令,也能减少gas成本。

1. 概述

如何保障代码质量?

在涉及交易资金的程序开发中,代码质量的保障至关重要,因为任何细微的代码漏洞都可能引发不可挽回的巨大损失。对于开发者而言,单元测试验证无疑是代码发布前的关键环节。

对于 Sui Move 语言而言,其拥有一套完备的单元测试体系,借助特定的注解和命令,开发者可以高效地对代码进行测试与验证。

Move 语言的单元测试在 Move 源语言中使用了三种注解:

  • #[test]:将一个函数标记为测试函数;
  • #[expected_failure]:标记一个测试预计会失败;
  • #[test_only]:将一个模块或模块成员(使用、函数、结构体或常量)标记为仅用于测试的代码。

这些注解可以放置在任何具有合适形式及任何可见性的地方。每当一个模块或模块成员被标注为#[test_only]#[test]时,除非是为了测试而进行编译,否则它不会被包含在编译后的字节码中。

2. 测试注解

2.1 #[test]注解

  • #[test]注解只能放置在无参数的函数上。此注解将该函数标记为一个将由单元测试框架运行的测试函数。

    #[test]
    fun valid_test_function() {
        // 测试逻辑代码
    }
    #[test]
    fun invalid_test_function(arg: u64) {
        // 此函数因有参数,不符合#[test]注解要求,将无法正确编译
    }

2.2 #[expected_failure注解

#[expected_failure] 注解用于指定预期的错误条件,并确保测试符合这些预期的失败条件。可以通过不同的方式使用该注解,具体如下:

  1. #[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;
        }
    }
  2. #[expected_failure(arithmetic_error, location = <location>)]

    这个注解用于指定测试应该因为算术错误(如整数溢出、除以零等)而失败。<location> 参数必须是有效的模块路径,例如 Selfmy_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();
        }
    }
  3. #[expected_failure(out_of_gas, location = <location>)] 此注解指定测试应因“耗尽 gas”错误而失败。<location> 参数必须是有效的模块路径,例如 Selfmy_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();
        }
    }
  4. #[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(); 
        }
    }
  5. #[expected_failure]

    此注解用于指定测试应因任何错误代码而失败。使用时应小心,优先考虑使用上面描述的更具体的方式。

    #[test]
    #[expected_failure]
    fun test_pow_overflow() {
            math::pow(10, 100);
    }

2.3 #[test_only]注解

一个模块及其任何成员都可以被声明为仅用于测试。

  • #[test_only] 属性标记的代码仅在测试环境中有效,确保它不会出现在生产环境中。
  • 适用于模块、常量、use 语句、结构体和函数,旨在隔离测试代码和生产代码。

注意:标注为#[test_only]的函数只能从测试代码中调用,但它们本身不是测试函数,不会被单元测试框架作为测试来运行。

  1. 在模块上使用 #[test_only] 属性
```rust
#[test_only] // 仅限测试的属性可以附加到模块
module abc { ... }
```
  1. 在常量上使用 #[test_only] 属性

    #[test_only] // 仅限测试的属性可以附加到常量
    const MY_ADDR: address = @0x1;
  2. use 语句上使用 #[test_only] 属性

    #[test_only] // .. 附加到 `use` 语句
    use pkg_addr::some_other_module;
  3. 在结构体上使用 #[test_only] 属性

    #[test_only] // .. 附加到结构体
    public struct SomeStruct { ... }
  4. 在函数上使用 #[test_only] 属性

    #[test_only] // .. 附加到函数。只能从测试代码中调用,但这不是一个测试!
    fun test_only_function(...) { ... }

3. 如何运行单元测试?

可以使用sui move test命令来运行 Move 包的单元测试。运行测试时,每个测试的结果要么是通过(PASS)、失败(FAIL),要么是超时(TIMEOUT)。如果一个测试用例失败,尽可能会报告失败的位置以及导致失败的函数名称。

3.1 创建单元测试的模块

  1. 新建模块test_example
$ sui move new test_example
$ cd test_example
  1. 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);
    }
}

3.2 运行测试

在源码的根目录执行sui move test

image.png

3.3 使用测试标志,运行特定测试

可以使用sui move test <字符串> 来运行特定的测试或一组测试。

下面测试分别运行了包含non_zero 字符串和包含zero_coin 的所有测试方法。

image 1.png

3.4 限制每个测试的 gas 使用量

-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>

3.5 测试统计报告

-s--statistics 参数允许你收集有关测试运行的统计信息,并报告每个测试的运行时间和 Gas 使用情况。

image 2.png

如果需要导出csv格式也可以用 sui move test -s csv 命令。

4. FAQ

Q1. 直接运行 sui move test报错, Failed to resolve dependencies for package 'test_example' image 3.png A: 加上参数 sui move test --skip-fetch-latest-git-deps 运行成功

image 4.png

5. 参考文档

<!--StartFragment-->

Unit Tests

<!--EndFragment--> <!--StartFragment-->

move-book cn

<!--EndFragment--> <!--StartFragment-->

关注《HOH水分子》公众号,我们将持续分享和制作变成语言教程,让大家对编程产生化学反应。

b4AQIzN06730e45415811.webp <!--EndFragment-->

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
LeonDev1024
LeonDev1024
0x98dE...1DB4
江湖只有他的大名,没有他的介绍。