开发
单元测试

单元测试

在 Move 中,编写单元测试跟编写正常的代码基本一样,区别只是在测试代码的上方使用下面的标注:

  • #[test]
  • #[test_only]
  • #[expected_failure]

第一条标注将函数标记为测试。第二条标注将模块或模块成员(导入语句、函数或结构体)标记为仅用于测试。第三条标注预期测试失败的代码。

这些注释可以放置在具有任何可见性的函数上。每当一个模块或模块成员被注释为 #[test_only]#[test] 时,它不会包含在编译的字节码中,除非它被编译用于测试。

使用 #[test]#[expected_failure] 标注时,可以带参数或不带参数。

没有参数的 #[test] 标注只能放在没有参数的函数上。

#[test] // OK
fun this_is_a_test() { ... }

#[test] // Will fail to compile since the test takes an argument
fun this_is_not_correct(arg: signer) { ... }

测试也可以标注为 #[expected_failure]。这个标注标志着测试应该会引发错误。可以通过使用 #[expected_failure(abort_code = code)] 对其进行注释来确保测试使用特定的中止代码中止,如果它随后因不同的中止代码或非中止错误而失败,则测试将失败。只有具有 #[test] 标注的函数也可以标注为 #[expected_failure]

#[test]
#[expected_failure]
public fun this_test_will_abort_and_pass() { abort 1 }

#[test]
#[expected_failure]
public fun test_will_error_and_pass() { 1/0; }

#[test]
#[expected_failure(abort_code = 0)]
public fun test_will_error_and_fail() { 1/0; }

#[test, expected_failure] // Can have multiple in one attribute. This test will pass.
public fun this_other_test_will_abort_and_pass() { abort 1 }

测试例子

module unit_test::unit_test {
    use moveos_std::account;
    use moveos_std::signer;

    struct Counter has key {
        count_value: u64
    }

    fun init() {
        let signer = signer::module_signer<Counter>();
        account::move_resource_to(&signer, Counter { count_value: 0});
    }

    entry fun increase(account: &signer) {
        let account_addr = signer::address_of(account);
        let counter = account::borrow_mut_resource<Counter>(account_addr);
        counter.count_value = counter.count_value + 1;
    }

    #[test(account = @0x42)]
    fun test_counter(account: &signer) {
        let account_addr = signer::address_of(account);
        account::move_resource_to(account, Counter { count_value: 0});

        let counter = account::borrow_resource<Counter>(account_addr);
        assert!(counter.count_value == 0, 999);

        increase(account);
        assert!(counter.count_value == 1, 1000);
    }
}

我们使用快速入门中的计数器例子来演示。在快速入门中,我们已经编写了一个计数器程序,但是我们编写完成后,并不能保证所有的功能都如我们预期的那样工作。因此我们编写一个单测来检查当前模块的函数是否能达到预期效果。

函数 test_counter 就是当前这个程序的单元测试函数。使用了 #[test] 标注,并传递了一个 account 参数。

首先构建一个 Counter 结构,其字段初始化为 0。通过 account::borrow_resource 函数获取账户下的 Counter 资源,并检验资源中的初始值是否为 0,如果不是,则断言失败并返回 999

接着执行 increase 函数,使 Counter 的计数值增加 1,再次检查账户的 Counter 资源中的值是否变成了 1,如果不是,则断言失败并返回 1000

  1. 测试计数器是否正常创建出来:
let counter = account::borrow_resource<Counter>(account_addr);
assert!(counter.count_value == 0, 999);
  1. 检查 increase 函数的执行逻辑,并判断是否能正常递增:
increase(account);
assert!(counter.count_value == 1, 1000);

运行单元测试

rooch move test
$ rooch move test
 
INCLUDING DEPENDENCY MoveStdlib
INCLUDING DEPENDENCY MoveosStdlib
INCLUDING DEPENDENCY RoochFramework
BUILDING unit_test
Running Move unit tests
2024-06-28T01:35:08.397013Z  INFO moveos_common::utils: set max open fds 8192
[ PASS    ] 0xfc3c1fa4f1538deee1048fa066a1b0029f2cf428e21667e5a7d4d570626c112e::unit_test::test_counter
Test result: OK. Total tests: 1; passed: 1; failed: 0

可以看到,我们编写的单元测试通过了!证明我们的计数器逻辑是正确的。

接下来,我们修改一下,看看断言失败的情形:

increase(account);
assert!(counter.count_value == 2, 1000);
[joe@mx unit_test]$ rooch move test
INCLUDING DEPENDENCY MoveStdlib
INCLUDING DEPENDENCY MoveosStdlib
INCLUDING DEPENDENCY RoochFramework
BUILDING unit_test
Running Move unit tests
2024-06-28T01:55:09.601009Z  INFO moveos_common::utils: set max open fds 8192
[ FAIL    ] 0xfc3c1fa4f1538deee1048fa066a1b0029f2cf428e21667e5a7d4d570626c112e::unit_test::test_counter
 
Test failures:
 
Failures in 0xfc3c1fa4f1538deee1048fa066a1b0029f2cf428e21667e5a7d4d570626c112e::unit_test:
 
┌── test_counter ──────
 error[E11001]: test failure
    ┌─ ./sources/unit.move:29:9
    
 21      fun test_counter(account: &signer) {
             ------------ In this function in 0xfc3c1fa4f1538deee1048fa066a1b0029f2cf428e21667e5a7d4d570626c112e::unit_test
    ·
 29          assert!(counter.count_value == 2, 1000);
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Test was not expected to error, but it aborted with code 1000 originating in the module fc3c1fa4f1538deee1048fa066a1b0029f2cf428e21667e5a7d4d570626c112e::unit_test rooted here


└──────────────────
 
Test result: FAILED. Total tests: 1; passed: 0; failed: 1

可以看到,Move 编译器很清楚地指明了断言程序的位置,因此我们就能很容易地定位到我们测试程序的某个位置,进而知道某个函数的执行结果没有达到我们的预期。