单元测试
在 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
。
- 测试计数器是否正常创建出来:
let counter = account::borrow_resource<Counter>(account_addr);
assert!(counter.count_value == 0, 999);
- 检查
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 编译器很清楚地指明了断言程序的位置,因此我们就能很容易地定位到我们测试程序的某个位置,进而知道某个函数的执行结果没有达到我们的预期。