Skip to main content

构建和测试包

如果你遵循了编写 Move 包,那么你有一个需要构建的基本模块。如果没有,请从那个主题开始,或者使用你的包,在适当的地方替代相应的信息。

构建你的包

确保你的终端或控制台在包含你的包的目录中(如果你一直在关注,那么是 my_first_package)。使用以下命令构建你的包:

sui move build

成功构建会返回类似以下的响应:

UPDATING GIT DEPENDENCY https://github.com/MystenLabs/sui.git
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING my_first_package

如果构建失败,你可以使用输出中的详细错误消息来排查和解决根本问题。

现在你已经设计了你的资产及其访问函数,是时候在发布之前测试包代码了。

测试包

Sui 包含对 Move 测试框架的支持,该框架使你能够编写分析 Move 代码的单元测试,类似于其他语言的测试框架(例如,内置的 Rust 测试框架或 Java 的 JUnit 框架)。

一个独立的 Move 单元测试封装在一个没有参数、没有返回值且具有 #[test] 注解的公共函数中。当你从包根目录(按当前运行示例的示例,即 my_move_package 目录)调用 sui move test 命令时,测试框架会执行这样的函数:

sui move test

如果你对Write a Package中创建的包执行此命令,你将看到以下输出。并不奇怪,测试结果具有 OK 状态,因为尚未编写任何测试来失败。

BUILDING Sui
BUILDING MoveStdlib
BUILDING my_first_package
Running Move unit tests
Test result: OK. Total tests: 0; passed: 0; failed: 0

要实际测试你的代码,你需要添加测试函数。首先在 my_module.move 文件中,在模块定义内添加一个基本的测试函数:

    #[test]
public fun test_sword_create() {

// Create a dummy TxContext for testing
let ctx = tx_context::dummy();

// Create a sword
let sword = Sword {
id: object::new(&mut ctx),
magic: 42,
strength: 7,
};

// Check if accessor functions return correct values
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
}

正如代码所示,单元测试函数(test_sword_create())创建了 TxContext 结构的虚构实例,并将其分配给 ctx。然后,该函数使用 ctx 创建一个剑对象,以创建唯一标识符,并将 42 分配给 magic 参数和 7 分配给 strength。最后,测试调用 magicstrength 访问函数,以验证它们返回正确的值。

该函数将虚构上下文 ctx 作为可变引用参数(&mut)传递给 object::new 函数,但将 sword 作为只读引用参数(&sword)传递给其访问函数。

现在你有了一个测试函数,再次运行测试命令:

sui move test

然而,在运行 test 命令后,你得到的是一个编译错误而不是测试结果:

error[E06001]: unused value without 'drop'
┌─ ./sources/my_module.move:60:65

4 │ struct Sword has key, store {
│ ----- To satisfy the constraint, the 'drop' ability would need to be added here
·
27let sword = Sword {
│ ----- The local variable 'sword' still contains a value. The value does not have the 'drop' ability and must be consumed before the function returns
│ ╭─────────────────────'
28 │ │ id: object::new(&mut ctx),
29 │ │ magic: 42,
30 │ │ strength: 7,
31 │ │ };
│ ╰─────────' The type 'MyFirstPackage::my_module::Sword' does not have the ability 'drop'
· │
34 │ assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
│ ^ Invalid return

错误消息包含了调试代码所需的所有信息。有问题的代码是为了突出 Move 语言的一个安全特性。

Sword 结构表示数字化模拟现实世界物品的游戏资产。显然,真正的剑不可能简单地消失(尽管可以显式销毁),但对数字剑没有这样的限制。事实上,这正是 test 函数中发生的情况 - 你创建了一个 Sword 结构的实例,该实例在函数调用结束时简单地消失。如果你看到一些东西在你眼前消失,你也会感到惊讶。

其中一个解决方案(正如错误消息中建议的那样)是在 Sword 结构的定义中添加 drop 能力,这将允许该结构的实例消失(被丢弃)。在这种情况下,放弃有价值的资产是不可取的资产属性,因此需要另一种解决方案。解决这个问题的另一种方法是转移 sword 的所有权。

为了使测试正常工作,添加以下行到测试函数的开头,以导入 Transfer 模块:

use sui::transfer;

导入 Transfer 模块后,在测试函数的末尾(在 !assert 调用之后)添加以下行,将 sword 的所有权转移给一个新创建的虚构地址:

// Create a dummy address and transfer the sword
let dummy_address = @0xCAFE;
transfer::transfer(sword, dummy_address);

再次运行测试命令。现在输出显示运行了一个成功的测试:

BUILDING MoveStdlib
BUILDING Sui
BUILDING my_first_package
Running Move unit tests
[ PASS ] 0x0::my_module::test_sword_create
Test result: OK. Total tests: 1; passed: 1; failed: 0
tip

使用过滤字符串仅运行匹配的单元测试子集。提供了过滤字符串后,sui move test 会检查完全合格的(<address>::<module_name>::<fn_name>)名称是否匹配。

例子:

sui move test sword

上面的命令运行所有名称包含 sword 的测试。

你可以通过以下方式了解更多测试选项:

sui move test -h

Sui 特定的测试

前面的测试示例在很大程度上是纯 Move,并且除了使用一些 Sui 包(如 sui::tx_contextsui::transfer)之外,并不特定于 Sui。虽然这种测试风格已经对于为 Sui 编写 Move 代码很有用,但你可能还想测试其他 Sui 特定的功能。特别是,在 Sui 中的 Move 调用封装在 Sui 交易中,你可能想在单个测试中测试不同交易之间的交互(例如,一个交易创建一个对象,另一个交易转移它)。 Sui 特定的测试通过 test_scenario 模块进行支持,该模块提供了与纯 Move 及其测试框架中无法使用的 Sui 相关的测试功能。

test_scenario 模块提供了一个场景,模拟了一系列 Sui 交易,每个交易可能由不同的用户执行。使用此模块的测试通常使用 test_scenario::begin 函数启动第一笔交易。此函数将执行交易的用户的地址作为其参数,并返回表示场景的 Scenario 结构的实例。

Scenario 结构的实例包含一个模拟 Sui 对象存储的每地址对象池,并提供了用于操作池中对象的帮助函数。第一笔交易完成后,后续的测试交易从 test_scenario::next_tx 函数开始。此函数接受表示当前场景的 Scenario 结构的实例和一个用户地址作为参数。 更新你的 my_module.move 文件,以包含可从 Sui 调用的入口函数,实现 sword 的创建和转移。有了这些函数,然后你可以添加一个使用 test_scenario 模块测试这些新功能的多交易测试。将这些函数放在访问函数之后(注释中的 Part 5)。

    public fun sword_create(magic: u64, strength: u64, recipient: address, ctx: &mut TxContext) {
use sui::transfer;

// create a sword
let sword = Sword {
id: object::new(ctx),
magic: magic,
strength: strength,
};
// transfer the sword
transfer::transfer(sword, recipient);
}

public fun sword_transfer(sword: Sword, recipient: address, _ctx: &mut TxContext) {
use sui::transfer;
// transfer the sword
transfer::transfer(sword, recipient);
}

新函数的代码使用了结构创建和 Sui 内部模块(TxContextTransfer)的方式,类似于你在前面部分看到的。重要的是入口函数要有正确的签名。

在包含新入口函数的情况下,添加另一个测试函数,确保它们表现如预期。

    #[test]
fun test_sword_transactions() {
use sui::test_scenario;

// create test addresses representing users
let admin = @0xBABE;
let initial_owner = @0xCAFE;
let final_owner = @0xFACE;

// first transaction to emulate module initialization
let scenario_val = test_scenario::begin(admin);
let scenario = &mut scenario_val;
{
init(test_scenario::ctx(scenario));
};
// second transaction executed by admin to create the sword
test_scenario::next_tx(scenario, admin);
{
// create the sword and transfer it to the initial owner
sword_create(42, 7, initial_owner, test_scenario::ctx(scenario));
};
// third transaction executed by the initial sword owner
test_scenario::next_tx(scenario, initial_owner);
{
// extract the sword owned by the initial owner
let sword = test_scenario::take_from_sender<Sword>(scenario);
// transfer the sword to the final owner
sword_transfer(sword, final_owner, test_scenario::ctx(scenario))
};
// fourth transaction executed by the final sword owner
test_scenario::next_tx(scenario, final_owner);
{
// extract the sword owned by the final owner
let sword = test_scenario::take_from_sender<Sword>(scenario);
// verify that the sword has expected properties
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
// return the sword to the object pool (it cannot be simply "dropped")
test_scenario::return_to_sender(scenario, sword)
};
test_scenario::end(scenario_val);
}

要注意新测试函数的一些细节。代码首先做的是创建一些地址,代表参与测试场景的用户。假设有一个游戏管理员用户和两个代表玩家的常规用户。然后,测试通过代表管理员地址启动第一笔交易来创建一个场景。 管理员执行第二笔交易。该交易创建了一个 sword,其中 initial_owner 是接收者。

然后,初始所有者执行第三笔交易(作为参数传递给 test_scenario::next_tx 函数),然后将他们现在拥有的 sword 转移给最终所有者。在纯 Move 中,没有 Sui 存储的概念;因此,在模拟的 Sui 交易中没有简单的方法可以从存储中检索它。这就是 test_scenario 模块发挥作用的地方 - 它的 take_from_sender 函数允许执行当前交易的地址拥有的给定类型的对象(Sword)在 Move 代码中进行操作。目前,假设只有一个这样的对象。在这种情况下,测试将从存储中检索到的对象转移给另一个地址。

tip

交易效果,如对象创建和转移,只有在给定交易完成后才会可见。例如,在正在运行的示例中,如果第二笔交易创建了一个 sword 并将其转移到管理员的地址,它只会在第三笔交易中对管理员的地址(通过 test_scenariotake_from_sendertake_from_address 函数)可用。

最终所有者执行第四笔也是最后一笔交易,从存储中检索 sword 对象并检查它是否具有预期的属性。请记住,如 Testing a package 中所述,在纯 Move 测试场景中,一旦对象在 Move 代码中可用(创建或从模拟存储中检索),它就不能简单地消失。

在纯 Move 测试函数中,函数将 sword 对象转移给虚假地址以处理消失的问题。然而,test_scenario 包提供了一个更优雅的解决方案,更接近 Move 代码在 Sui 上下文中实际执行时发生的情况 - 该包只需使用 test_scenario::return_to_sender 函数将 sword 返回到对象池中。

再次运行测试命令,以查看我们的模块的两个成功的测试:

BUILDING Sui
BUILDING MoveStdlib
BUILDING my_first_package
Running Move unit tests
[ PASS ] 0x0::my_module::test_sword_create
[ PASS ] 0x0::my_module::test_sword_transactions
Test result: OK. Total tests: 2; passed: 2; failed: 0

现在你已经构建并测试了软件包,接下来的主题将指导你将软件包发布到 Sui 网络。