Build
Unit test

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.

#[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].

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

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);
    }
}

We use the counter example in the Quick start 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.

First build a Counter structure with its fields initialized to 0. Get the Counter resource under the account through the account::borrow_resource function, and check whether the initial value in the resource is 0. If not, the assertion fails and 999 is returned.

Then execute the increase function to increase the counter value by 1, and check again whether the value in the account's Counter resource has become 1. If not, the assertion fails and 1000 is returned.

  1. Test whether the counter is created normally:
let counter = account::borrow_resource<Counter>(account_addr);
assert!(counter.count_value == 0, 999);
  1. Check the execution logic of the increase function and determine whether it can be incremented normally:
increase(account);
assert!(counter.count_value == 1, 1000);

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

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:

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

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.