跳到主要内容

如何 Debug Move 模块和排查问题

Move 编程有两种测试方式,分别是单元测试(unit test)和集成测试(integration test)。这两种测试方式足以覆盖几乎 90% 的使用场景。单元测试可以用来对一些功能模块进行简单的验证。然而,很多测试场景是在区块链上的,需要从一些初始的交易状态开始,单元测试无法满足此类需求。因此,我们需要更强大的测试功能来模拟真实区块链上的交易,才能覆盖大部分用例,从而让代码更加健壮。

简单示例

我们从一个简单的例子开始。
这是一个非常简单的 Token 模块,我将留下一些小错误来展示整个 debug 过程。

你可以使用下面的地址来测试

address:0xf2aa2eae4ceaae88b308fc904975e4ae  
public_key:0x98826ab91a9a5d85dec536418090aa6342991bc8f947613721c8165e7102b132
private_key:0xa5ead1fb25114b335ad21a07ed5cee8cecba8763309ec78656e7c4ccaf5735e7
  • 使用 mpm 命令创建项目
mpm package new MyCake
  • 进入目录并编辑 mycake.move 文件
cd MyCake
vi sources/mycake.move

在文件中输入以下代码:

address Chef{
module Cake{
use StarcoinFramework::Signer;
use StarcoinFramework::Token;
use StarcoinFramework::Account;

struct Cake has copy, drop, store { }

public fun admin():address{
@Chef
}

public fun init(account :&signer){
assert!( admin() == Signer::address_of( account ) , 10000);
Token::register_token<Cake>( account, 9 );
}

public fun make_cake( account: &signer , amount : u128): Token::Token<Cake> {
assert!( admin() == Signer::address_of( account ) , 10000);
Token::mint<Cake>( account , amount )
}

public fun destroy_cake( account: &signer , cake: Token::Token<Cake>) {
assert!( admin() == Signer::address_of( account ) , 10000);
Token::burn<Cake>( account , cake );
}

public fun send_cake( _to :address , cake: Token::Token<Cake> ){
Account::deposit<Cake>(@Chef, cake);
}

public fun add (x:u128, y:u128 ):u128{
x - y
}

}
}

  • 编辑 Move.toml 文件
vi Move.toml
[package]
name = "MyCake"
version = "0.0.0"

[addresses]
Chef = "0xf2aa2eae4ceaae88b308fc904975e4ae"

[dependencies]
StarcoinFramework = { git = "https://github.com/starcoinorg/starcoin-framework.git", rev = "01c84198819310620f2417413c3c800df8292ae5" }

单元测试

代码编写完成后,应首先进行单元测试,保证代码的正确性。 单元测试通常用来测试一些函数或者功能模块的正确性。

测试 add 函数的返回值

  • 添加单元测试代码
address Chef{
module Cake{
use StarcoinFramework::Signer;
use StarcoinFramework::Token;
use StarcoinFramework::Account;

struct Cake has copy, drop, store { }

public fun admin():address{
@Chef
}

public fun init(account :&signer){
assert!( admin() == Signer::address_of( account ) , 10000);
Token::register_token<Cake>( account, 9 );
}

public fun make_cake( account: &signer , amount : u128): Token::Token<Cake> {
assert!( admin() == Signer::address_of( account ) , 10000);
Token::mint<Cake>( account , amount )
}

public fun destroy_cake( account: &signer , cake: Token::Token<Cake>) {
assert!( admin() == Signer::address_of( account ) , 10000);
Token::burn<Cake>( account , cake );
}

public fun send_cake( _to :address , cake: Token::Token<Cake> ){
Account::deposit<Cake>(@Chef, cake);
}

public fun add (x:u128, y:u128 ):u128{
x - y
}

#[test]
public fun add_test(){
assert!( add(10 , 1) == 11, 101);
}
}
}

  • 运行测试命令
mpm package test
  • 获取测试结果
BUILDING UnitTest
BUILDING StarcoinFramework
BUILDING MyCake
Running Move unit tests
[ FAIL ] 0xf2aa2eae4ceaae88b308fc904975e4ae::Cake::add_test

Test failures:

Failures in 0xf2aa2eae4ceaae88b308fc904975e4ae::Cake:

┌── add_test ──────
│ error[E11001]: test failure
│ ┌─ ./sources/mycake.move:38:13
│ │
│ 37 │ public fun add_test(){
│ │ -------- In this function in 0xf2aa2eae4ceaae88b308fc904975e4ae::Cake
│ 38 │ assert!( add(10 , 1) == 11, 101);
│ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Test was not expected to abort but it aborted with 101 here


└──────────────────

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

可以看到 add 函数的返回结果不是我们预期的

我们检查一下 add 函数,可以看到其内部实现是错误的。
修复这个错误:

    public fun add (x:u128, y:u128 ):u128{
x + y
}
  • 重新运行单元测试
mpm package test
  • 获取测试结果
CACHED UnitTest
CACHED StarcoinFramework
BUILDING MyCake
Running Move unit tests
[ PASS ] 0xf2aa2eae4ceaae88b308fc904975e4ae::Cake::add_test
Test result: OK. Total tests: 1; passed: 1; failed: 0

恭喜,所有测试通过了!

通过这种方式可以发现函数或功能模块里的问题。你可以在单元测试中打印变量,也可以调用其他函数,但是一定记住,单元测试是非常局限的。如果你需要 signature ,那你可以使用集成测试。

集成测试

单元测试只能满足一小部分的测试需求。 更多情况下,我们在测试阶段需要模拟代码在区块链上运行的情况,很多问题只有在区块链上运行时才能暴露出来。

集成测试最适合完成这项需求。

  • 创建一个 integration-tests 的工作目录,并添加 mycake_test.move 文件
mkdir integration-tests
vi integration-test/mycake.move

在 mycake.move 文件中添加以下代码:

//# init -n test --public-keys Chef=0x98826ab91a9a5d85dec536418090aa6342991bc8f947613721c8165e7102b132 

//# faucet --addr Chef --amount 10000000000000000

//# faucet --addr guest --amount 10000000000000000

//# run --signers Chef
script {
use Chef::Cake;
fun init(signer: signer) {
Cake::init(&signer);
}
}
// check: EXECUTED

//# run --signers Chef
script {
use StarcoinFramework::Signer;
use StarcoinFramework::Account;
use Chef::Cake;
fun make_cake(signer: signer) {
let cake = Cake::make_cake(&signer , 1 * 1000 * 1000 * 1000);
Account::deposit<Cake::Cake>( Signer::address_of(&signer) , cake );
assert!( Account::balance<Cake::Cake>(@Chef) == 1 * 1000 * 1000 * 1000 , 1001);
}
}
// check: EXECUTED

//# run --signers Chef
script {
use Chef::Cake;
fun destroy_cake(signer: signer) {
let cake = Cake::make_cake(&signer , 1 * 1000 * 1000 * 1000);
Cake::destroy_cake(&signer, cake);
}
}
// check: EXECUTED

//# run --signers Chef
script {
use StarcoinFramework::Account;
use Chef::Cake;
fun send_cake(signer: signer) {
let cake = Cake::make_cake(&signer , 1 * 1000 * 1000 * 1000);
Cake::send_cake(@guest, cake);
assert!( Account::balance<Cake::Cake>(@guest) == 1 * 1000 * 1000 * 1000 , 1001);
}
}
// check: EXECUTED
  • 运行集成测试
mpm integration-test

命令行中将输出如下结果

可以看到,大部分测试结果是符合预期的。但是最后一个测试的结果是错误的,我们需要仔细检查最后一项测试代码,理清其背后的逻辑。

BUILDING StarcoinFramework
BUILDING MyCake

running 1 tests

test transactional-test::mycake_test.move ... FAILED
Error: Expected errors differ from actual errors:
processed 7 tasks

task 3 'run'. lines 7-14:
{
"gas_used": 97115,
"status": {
"Keep": "Executed"
}
}

task 4 'run'. lines 16-26:
{
"gas_used": 128354,
"status": {
"Keep": "Executed"
}
}

task 5 'run'. lines 28-36:
{
"gas_used": 90747,
"status": {
"Keep": "Executed"
}
}

task 6 'run'. lines 38-48:
{
"gas_used": 90471,
"status": {
"Keep": {
"MoveAbort": [
"Script",
1001
]
}
}
}



failures:
transactional-test::mycake_test.move

test result: FAILED. 0 passed; 1 failed; 0 filtered out
  • 问题分析

首先看一下代码,我们获取了一些 cake 代币,然后想发送给 guest 账户;但是执行 send_cake 后,guest 账户里并没有收到 cake 代币。

//# run --signers Chef
script {
use StarcoinFramework::Account;
use Chef::Cake;
fun send_cake(signer: signer) {
let cake = Cake::make_cake(&signer , 1 * 1000 * 1000 * 1000);
Cake::send_cake(@guest, cake);
assert!( Account::balance<Cake::Cake>(@guest) == 1 * 1000 * 1000 * 1000 , 1001);
}
}
// check: EXECUTED

通过上面的单元测试,我们知道 make_cake 函数是正常的,那么问题一定出在 send_cake 上面

我们就来检查一下吧。

    public fun send_cake( _to :address , cake: Token::Token<Cake> ){
Account::deposit<Cake>(@Chef, cake);
}

仔细查看 send_cake 函数,可以看到 Account::deposit<Cake>(@Chef, cake) 的参数错误写成了管理员的地址,而实际上应该是接受代币的账户地址。

修复该问题:

    public fun send_cake( to :address , cake: Token::Token<Cake> ){
Account::deposit<Cake>(to, cake);
}

好了,我们重新运行 integration-test

mpm integration-test

这下测试项中没有出现错误了,但是我们的测试结果仍然失败了。这又是什么原因?

BUILDING StarcoinFramework
BUILDING MyCake

running 1 tests

test transactional-test::mycake_test.move ... FAILED
Error: Expected errors differ from actual errors:
processed 7 tasks

task 3 'run'. lines 7-14:
{
"gas_used": 97115,
"status": {
"Keep": "Executed"
}
}

task 4 'run'. lines 16-26:
{
"gas_used": 128354,
"status": {
"Keep": "Executed"
}
}

task 5 'run'. lines 28-36:
{
"gas_used": 90747,
"status": {
"Keep": "Executed"
}
}

task 6 'run'. lines 38-48:
{
"gas_used": 141989,
"status": {
"Keep": "Executed"
}
}



failures:
transactional-test::mycake_test.move

test result: FAILED. 0 passed; 1 failed; 0 filtered out
  • 更新测试基准

这是因为我们需要更新测试基准

mpm integration-test --ub

该命令会在 integration-tests 目录下生成和测试文件同名,后缀为 exp 的文件。命令运行结束后,所有测试通过!

BUILDING StarcoinFramework
BUILDING MyCake

running 1 tests

test transactional-test::mycake_test.move ... ok

test result: ok. 1 passed; 0 failed; 0 filtered out

当修改测试项后,记得一定要在在测试命令中加上 "--ub" 选项:mpm integration-test --ub

正确代码

Move.toml

[package]
name = "MyCake"
version = "0.0.0"

[addresses]
Chef = "0xf2aa2eae4ceaae88b308fc904975e4ae"

[dependencies]
StarcoinFramework = { git = "https://github.com/starcoinorg/starcoin-framework.git", rev = "01c84198819310620f2417413c3c800df8292ae5" }

mycake.move

address Chef{
module Cake{
use StarcoinFramework::Signer;
use StarcoinFramework::Token;
use StarcoinFramework::Account;

struct Cake has copy, drop, store { }

public fun admin():address{
@Chef
}

public fun init(account :&signer){
assert!( admin() == Signer::address_of( account ) , 10000);
Token::register_token<Cake>( account, 9 );
}

public fun make_cake( account: &signer , amount : u128): Token::Token<Cake> {
assert!( admin() == Signer::address_of( account ) , 10000);
Token::mint<Cake>( account , amount )
}

public fun destroy_cake( account: &signer , cake: Token::Token<Cake>) {
assert!( admin() == Signer::address_of( account ) , 10000);
Token::burn<Cake>( account , cake );
}

public fun send_cake( to :address , cake: Token::Token<Cake> ){
Account::deposit<Cake>(to, cake);
}

public fun add (x:u128, y:u128 ):u128{
x + y
}

#[test]
public fun add_test(){
assert!( add(10 , 1) == 11, 101);
}
}
}

mycake_test.move

//# init -n test --public-keys Chef=0x98826ab91a9a5d85dec536418090aa6342991bc8f947613721c8165e7102b132 

//# faucet --addr Chef --amount 10000000000000000

//# faucet --addr guest --amount 10000000000000000

//# run --signers Chef
script {
use Chef::Cake;
fun init(signer: signer) {
Cake::init(&signer);
}
}
// check: EXECUTED

//# run --signers Chef
script {
use StarcoinFramework::Signer;
use StarcoinFramework::Account;
use Chef::Cake;
fun make_cake(signer: signer) {
let cake = Cake::make_cake(&signer , 1 * 1000 * 1000 * 1000);
Account::deposit<Cake::Cake>( Signer::address_of(&signer) , cake );
}
}
// check: EXECUTED

//# run --signers Chef
script {
use Chef::Cake;
fun destroy_cake(signer: signer) {
let cake = Cake::make_cake(&signer , 1 * 1000 * 1000 * 1000);
Cake::destroy_cake(&signer, cake);
}
}
// check: EXECUTED

//# run --signers Chef
script {
use StarcoinFramework::Account;
use Chef::Cake;
fun send_cake(signer: signer) {
let cake = Cake::make_cake(&signer , 1 * 1000 * 1000 * 1000);
Cake::send_cake(@guest, cake);
assert!( Account::balance<Cake::Cake>(@guest) == 1 * 1000 * 1000 * 1000 , 1001);
}
}
// check: EXECUTED