From 8017c20f1851532dbfd2afac8a135d8a6acc45e7 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Fri, 22 Dec 2023 14:28:51 +0800 Subject: [PATCH] docs: add unit testing guide (#1255) * docs: add unit test guide * add unit test example * update unitest example * complete unit test guide * translate the unit test guide to en * clean up redundant codes * fix --- .../docs/developer-guides/unit-test.en-US.mdx | 172 ++++++++++++++++++ .../docs/developer-guides/unit-test.zh-CN.mdx | 172 ++++++++++++++++++ examples/unit_test/Move.toml | 20 ++ examples/unit_test/sources/counter.move | 37 ++++ 4 files changed, 401 insertions(+) create mode 100644 docs/website/pages/docs/developer-guides/unit-test.en-US.mdx create mode 100644 docs/website/pages/docs/developer-guides/unit-test.zh-CN.mdx create mode 100644 examples/unit_test/Move.toml create mode 100644 examples/unit_test/sources/counter.move diff --git a/docs/website/pages/docs/developer-guides/unit-test.en-US.mdx b/docs/website/pages/docs/developer-guides/unit-test.en-US.mdx new file mode 100644 index 0000000000..f723c4c1a5 --- /dev/null +++ b/docs/website/pages/docs/developer-guides/unit-test.en-US.mdx @@ -0,0 +1,172 @@ +# Unit test + +In Move, writing unit tests is basically the same as writing normal code. The only difference is that the following annotation is used above the test code: + +- `#[test]` +- `#[test_only]` +- `#[expected_failure]` + +The first annotation marks the function as a test. The second annotation marks the module or module member (use statement, function, or structure) as being used only for testing. The third line marks code that is expected to fail the test. + +These annotations can be placed on functions with any visibility. Whenever a module or module member is annotated as `#[test_only]` or `#[test]`, it will not be included in the compiled bytecode unless it is compiled for testing. + +The `#[test]` and `#[expected_failure]` annotations can be used with or without parameters. + +The `#[test]` annotation without parameters can only be placed on functions without parameters. + +```move +#[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) { ... } +``` + +Tests can also be annotated with `#[expected_failure]`. This annotation indicates that the test should throw an error. You can ensure that a test aborts with a specific abort code by annotating it with `#[expected_failure(abort_code = code)]` and if it subsequently fails with a different abort code or a non-abort error, the test will fail. Only functions annotated with `#[test]` can also be annotated with `#[expected_failure]`. + +```move +#[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 } +``` + +## Test example + +```move +module unit_test::unit_test { + use moveos_std::signer; + use moveos_std::context::{Self, Context}; + #[test_only] + use moveos_std::context::drop_test_context; + + struct Counter has key { + count_value: u64 + } + + fun init(ctx: &mut Context, account: &signer) { + context::move_resource_to(ctx, account, Counter { count_value: 0 }); + } + + entry fun increase(ctx: &mut Context, account: &signer) { + let account_addr = signer::address_of(account); + let counter = context::borrow_mut_resource(ctx, account_addr); + counter.count_value = counter.count_value + 1; + } + + #[test(account = @0x42)] + fun test_counter(account: &signer) { + let account_addr = signer::address_of(account); + let ctx = context::new_test_context(account_addr); + context::move_resource_to(&mut ctx, account, Counter { count_value: 0 }); + + let counter = context::borrow_resource(&ctx, account_addr); + assert!(counter.count_value == 0, 999); + + increase(&mut ctx, account); + let counter = context::borrow_resource(&ctx, account_addr); + assert!(counter.count_value == 1, 1000); + + drop_test_context(ctx); + } +} +``` + +We use the counter example in the [Quick start](./quick-start.en-US.mdx) to demonstrate. In the quick start, we have written a counter program, but after we finish writing, there is no guarantee that all functions will work as we expected. Therefore, we write a unit test to check whether the function of the current module can achieve the expected effect. + +The function `test_counter` is the unit test function of the current program. The `#[test]` annotation is used and an `account` parameter is passed. + +When testing, we do not call the command line and will not generate a normal context, so we need to create a context for this test. + +Once the address and context are available, we can construct the counter, build it and move the counter resource to the `0x42` address. + +1. Test whether the counter is created normally: + +```move +let counter = context::borrow_resource(&ctx, account_addr); +assert!(counter.count_value == 0, 999); +``` + +2. Check the execution logic of the `increase` function and determine whether it can be incremented normally: + +```move +increase(&mut ctx, account); +let counter = context::borrow_resource(&ctx, account_addr); +assert!(counter.count_value == 1, 1000); +``` + +3. After the test context is used, it needs to be released. + +> Since the context is created by testing, `init` and `increase` cannot be placed in two unit tests, so testing needs to be completed within a function scope in unit testing. + +## Run unit test + +```shell +rooch move test +``` + +```shell +[joe@mx unit_test]$ rooch move test + +INCLUDING DEPENDENCY MoveStdlib +INCLUDING DEPENDENCY MoveosStdlib +INCLUDING DEPENDENCY RoochFramework +BUILDING unit_test +Running Move unit tests +2023-12-21T13:57:07.014787Z INFO moveos_common::utils: set max open fds 45056 +[ PASS ] 0x42::unit_test::test_counter +Test result: OK. Total tests: 1; passed: 1; failed: 0 +Success +``` + +As you can see, the unit test we wrote passed! Prove that our counter logic is correct. + +Next, let’s modify it to see what happens when the assertion fails: + +```move +let counter = context::borrow_resource(&ctx, account_addr); +assert!(counter.count_value == 2, 999); +``` + +```shell +[joe@mx unit_test]$ rooch move test + +INCLUDING DEPENDENCY MoveStdlib +INCLUDING DEPENDENCY MoveosStdlib +INCLUDING DEPENDENCY RoochFramework +BUILDING unit_test +Running Move unit tests +2023-12-21T14:10:07.413084Z INFO moveos_common::utils: set max open fds 45056 +[ FAIL ] 0x42::unit_test::test_counter + +Test failures: + +Failures in 0x42::unit_test: + +┌── test_counter ────── +│ error[E11001]: test failure +│ ┌─ ./sources/counter.move:28:9 +│ │ +│ 22 │ fun test_counter(account: &signer) { +│ │ ------------ In this function in 0x42::unit_test +│ · +│ 28 │ assert!(counter.count_value == 2, 999); +│ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Test was not expected to error, but it aborted with code 999 originating in the module 0000000000000000000000000000000000000000000000000000000000000042::unit_test rooted here +│ +│ +└────────────────── + +Test result: FAILED. Total tests: 1; passed: 0; failed: 1 +``` + +As you can see, the Move compiler clearly indicates the location of the assertion program, so we can easily locate a certain location in our test program and know that the execution result of a certain function does not meet our expectations. diff --git a/docs/website/pages/docs/developer-guides/unit-test.zh-CN.mdx b/docs/website/pages/docs/developer-guides/unit-test.zh-CN.mdx new file mode 100644 index 0000000000..415fe4fa72 --- /dev/null +++ b/docs/website/pages/docs/developer-guides/unit-test.zh-CN.mdx @@ -0,0 +1,172 @@ +# 单元测试 + +在 Move 中,编写单元测试跟编写正常的代码基本一样,区别只是在测试代码的上方使用下面的标注: + +- `#[test]` +- `#[test_only]` +- `#[expected_failure]` + +第一条标注将函数标记为测试。第二条标注将模块或模块成员(导入语句、函数或结构体)标记为仅用于测试。第三条标注预期测试失败的代码。 + +这些注释可以放置在具有任何可见性的函数上。每当一个模块或模块成员被注释为 `#[test_only]` 或 `#[test]` 时,它不会包含在编译的字节码中,除非它被编译用于测试。 + +使用 `#[test]` 和 `#[expected_failure]` 标注时,可以带参数或不带参数。 + +没有参数的 `#[test]` 标注只能放在没有参数的函数上。 + +```move +#[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]`。 + +```move +#[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 } +``` + +## 测试例子 + +```move +module unit_test::unit_test { + use moveos_std::signer; + use moveos_std::context::{Self, Context}; + #[test_only] + use moveos_std::context::drop_test_context; + + struct Counter has key { + count_value: u64 + } + + fun init(ctx: &mut Context, account: &signer) { + context::move_resource_to(ctx, account, Counter { count_value: 0 }); + } + + entry fun increase(ctx: &mut Context, account: &signer) { + let account_addr = signer::address_of(account); + let counter = context::borrow_mut_resource(ctx, account_addr); + counter.count_value = counter.count_value + 1; + } + + #[test(account = @0x42)] + fun test_counter(account: &signer) { + let account_addr = signer::address_of(account); + let ctx = context::new_test_context(account_addr); + context::move_resource_to(&mut ctx, account, Counter { count_value: 0 }); + + let counter = context::borrow_resource(&ctx, account_addr); + assert!(counter.count_value == 0, 999); + + increase(&mut ctx, account); + let counter = context::borrow_resource(&ctx, account_addr); + assert!(counter.count_value == 1, 1000); + + drop_test_context(ctx); + } +} +``` + +我们使用[快速入门](./quick-start.zh-CN.mdx)中的计数器例子来演示。在快速入门中,我们已经编写了一个计数器程序,但是我们编写完成后,并不能保证所有的功能都如我们预期的那样工作。因此我们编写一个单测来检查当前模块的函数是否能达到预期效果。 + +函数 `test_counter` 就是当前这个程序的单元测试函数。使用了 `#[test]` 标注,并传递了一个 `account` 参数。 + +测试的时候,我们不经过命令行调用,不会产生正常情况下的上下文,因此我们需要为这个测试创建一个上下文。 + +当地址和上下文都有了之后,我们就可以构建计数器了,构建好并将计数器资源移到 `0x42` 地址。 + +1. 测试计数器是否正常创建出来: + +```move +let counter = context::borrow_resource(&ctx, account_addr); +assert!(counter.count_value == 0, 999); +``` + +2. 检查 `increase` 函数的执行逻辑,并判断是否能正常递增: + +```move +increase(&mut ctx, account); +let counter = context::borrow_resource(&ctx, account_addr); +assert!(counter.count_value == 1, 1000); +``` + +3. 测试用的上下文使用完后,需要释放掉。 + +> 由于上下文是测试创建的,不能将 `init` 和 `increase` 放在两个单测里,因此在单元测试中需要在一个函数作用域内完成测试。 + +## 运行单元测试 + +```shell +rooch move test +``` + +```shell +[joe@mx unit_test]$ rooch move test + +INCLUDING DEPENDENCY MoveStdlib +INCLUDING DEPENDENCY MoveosStdlib +INCLUDING DEPENDENCY RoochFramework +BUILDING unit_test +Running Move unit tests +2023-12-21T13:57:07.014787Z INFO moveos_common::utils: set max open fds 45056 +[ PASS ] 0x42::unit_test::test_counter +Test result: OK. Total tests: 1; passed: 1; failed: 0 +Success +``` + +可以看到,我们编写的单元测试通过了!证明我们的计数器逻辑是正确的。 + +接下来,我们修改一下,看看断言失败的情形: + +```move +let counter = context::borrow_resource(&ctx, account_addr); +assert!(counter.count_value == 2, 999); +``` + +```shell +[joe@mx unit_test]$ rooch move test + +INCLUDING DEPENDENCY MoveStdlib +INCLUDING DEPENDENCY MoveosStdlib +INCLUDING DEPENDENCY RoochFramework +BUILDING unit_test +Running Move unit tests +2023-12-21T14:10:07.413084Z INFO moveos_common::utils: set max open fds 45056 +[ FAIL ] 0x42::unit_test::test_counter + +Test failures: + +Failures in 0x42::unit_test: + +┌── test_counter ────── +│ error[E11001]: test failure +│ ┌─ ./sources/counter.move:28:9 +│ │ +│ 22 │ fun test_counter(account: &signer) { +│ │ ------------ In this function in 0x42::unit_test +│ · +│ 28 │ assert!(counter.count_value == 2, 999); +│ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Test was not expected to error, but it aborted with code 999 originating in the module 0000000000000000000000000000000000000000000000000000000000000042::unit_test rooted here +│ +│ +└────────────────── + +Test result: FAILED. Total tests: 1; passed: 0; failed: 1 +``` + +可以看到,Move 编译器很清楚地指明了断言程序的位置,因此我们就能很容易地定位到我们测试程序的某个位置,进而知道某个函数的执行结果没有达到我们的预期。 diff --git a/examples/unit_test/Move.toml b/examples/unit_test/Move.toml new file mode 100644 index 0000000000..0f98fc0bea --- /dev/null +++ b/examples/unit_test/Move.toml @@ -0,0 +1,20 @@ +[package] +name = "unit_test" +version = "0.0.1" + +[dependencies] +#MoveStdlib = { git = "https://github.com/rooch-network/rooch.git", subdir = "moveos/moveos-stdlib/move-stdlib", rev = "main" } +#MoveosStdlib = { git = "https://github.com/rooch-network/rooch.git", subdir = "moveos/moveos-stdlib/moveos-stdlib", rev = "main" } +#RoochFramework = { git = "https://github.com/rooch-network/rooch.git", subdir = "crates/rooch-framework", rev = "main" } +MoveStdlib = { local = "../../moveos/moveos-stdlib/move-stdlib" } +MoveosStdlib = { local = "../../moveos/moveos-stdlib/moveos-stdlib" } +RoochFramework = { local = "../../crates/rooch-framework" } + +[addresses] +unit_test = "_" +std = "0x1" +moveos_std = "0x2" +rooch_framework = "0x3" + +[dev-addresses] +unit_test = "0x42" \ No newline at end of file diff --git a/examples/unit_test/sources/counter.move b/examples/unit_test/sources/counter.move new file mode 100644 index 0000000000..a4dbeacff7 --- /dev/null +++ b/examples/unit_test/sources/counter.move @@ -0,0 +1,37 @@ +module unit_test::unit_test { + use moveos_std::signer; + use moveos_std::context::{Self, Context}; + #[test_only] + use moveos_std::context::drop_test_context; + + struct Counter has key { + count_value: u64 + } + + fun init(ctx: &mut Context, account: &signer) { + context::move_resource_to(ctx, account, Counter { count_value: 0 }); + } + + entry fun increase(ctx: &mut Context, account: &signer) { + let account_addr = signer::address_of(account); + let counter = context::borrow_mut_resource(ctx, account_addr); + counter.count_value = counter.count_value + 1; + } + + #[test(account = @0x42)] + fun test_counter(account: &signer) { + let account_addr = signer::address_of(account); + let ctx = context::new_test_context(account_addr); + context::move_resource_to(&mut ctx, account, Counter { count_value: 0 }); + + let counter = context::borrow_resource(&ctx, account_addr); + assert!(counter.count_value == 0, 999); + // assert!(counter.count_value == 2, 999); + + increase(&mut ctx, account); + let counter = context::borrow_resource(&ctx, account_addr); + assert!(counter.count_value == 1, 1000); + + drop_test_context(ctx); + } +}