Hello, World!
与侧边栏目录对应:3.1 Hello World — 编写、编译与测试
每位开发者学习新语言的第一步,几乎都是编写一个 “Hello, World!” 程序。在 Move 中,我们将创建一个完整的 Move 包,编写模块和测试,并通过 Sui CLI 完成构建与测试。本节将带你体验从零开始创建 Move 项目的完整流程。
创建 Move 包
使用 sui move new 命令创建一个新的 Move 项目:
sui move new hello_world
该命令会生成以下目录结构:
hello_world/
├── Move.toml
├── sources/
│ └── hello_world.move
└── tests/
└── hello_world_tests.move
各文件和目录的作用:
| 文件/目录 | 说明 |
|---|---|
Move.toml | 包的清单文件,定义包名、依赖和地址等 |
sources/ | 存放 Move 源代码 |
tests/ | 存放测试代码 |
理解 Move.toml
打开生成的 Move.toml 文件:
[package]
name = "hello_world"
edition = "2024"
[addresses]
hello_world = "0x0"
依赖:Sui 1.45 起 Framework 为隐式依赖,
Move.toml中无需再写Sui = { git = ... }。若你使用较旧工具链,可按第六章 §6.11补全[dependencies]。
rev说明:与第六章 §6.11 · Move 2024 Edition中的约定一致,默认使用framework/mainnet。若你仅用 testnet/devnet 且希望与 CLI 模板习惯一致,可改为framework/testnet,并与当前sui client环境匹配。
各字段含义:
[package]
- name:包名称,在依赖引用时使用
- edition:Move 语言版本。建议设为
"2024"以使用最新语法
[dependencies]
新版本工具链下 Sui Framework 为隐式依赖;若显式声明,则与清单文件一致,指向 Mysten 仓库的 framework/mainnet 等分支。Framework 提供 object::UID、TxContext、transfer 等。
[addresses]
定义地址别名。hello_world = "0x0" 表示本包在本地开发时使用 0x0 作为地址占位符,发布到链上后会被替换为实际的包地址。
编写模块
打开 sources/hello_world.move,将内容替换为:
/// 与本书 3.1「Hello World」对应:创建一个可上链的 `Hello` 对象并转移给交易发送者。
module hello_world::hello_world;
use std::string::String;
/// 链上可拥有的问候对象;发布后调用 `mint_hello` 可在钱包或区块浏览器里看到。
public struct Hello has key, store {
id: object::UID,
greeting: String,
}
public fun greeting(hello: &Hello): &String {
&hello.greeting
}
/// 构造 `Hello`(供测试或其它模块组合使用)。
public fun new_hello(ctx: &mut TxContext): Hello {
Hello {
id: object::new(ctx),
greeting: b"Hello, World!".to_string(),
}
}
/// 铸造 `Hello` 并转移给当前交易发送者(链上会产生新对象 ID)。
entry fun mint_hello(ctx: &mut TxContext) {
let hello = new_hello(ctx);
transfer::public_transfer(hello, ctx.sender());
}
模块声明与导入
module hello_world::hello_world;
use std::string::String;
hello_world(地址别名)与hello_world(模块名)与Move.toml中[addresses]对应;分号结尾为 Move 2024 文件级模块语法。- 仅显式导入
String;object、transfer、TxContext等由 Sui 预导入,可直接写object::new、TxContext等。
对象类型 Hello
public struct Hello has key, store {
id: object::UID,
greeting: String,
}
has key:表示这是 Sui 对象,首字段必须是id: UID。has store:对象可被转移,也可作为字段嵌入其它类型(后续章节会深入)。greeting:本例在链上可读的一段 UTF-8 文本,便于在浏览器里看出「这是 Hello 示例」。
构造函数与 entry
public fun new_hello(ctx: &mut TxContext): Hello { /* ... */ }
entry fun mint_hello(ctx: &mut TxContext) {
let hello = new_hello(ctx);
transfer::public_transfer(hello, ctx.sender());
}
new_hello:用object::new(ctx)分配唯一id,在内存 / 测试中也可单独调用。entry fun mint_hello:入口函数,可从钱包或 CLI 作为一笔交易的唯一调用直接执行(无需被其它 Move 模块再包装)。内部把新建的Hellopublic_transfer给ctx.sender(),即当前交易的发送地址,因此发布后你用自己的地址调用,就会在自己的名下出现一个新的Hello对象。- 这样不再只是「返回一个字符串」的纯函数,而是在链上创建可查询的对象,与真实 DApp 的「铸造 NFT / 道具」是同一类模式的最小版。
注意:Move 函数中最后一个表达式会自动作为返回值;无返回值的函数体以分号或控制流结束。
编写测试
打开 tests/hello_world_tests.move,将内容替换为:
#[test_only]
module hello_world::hello_world_tests;
use hello_world::hello_world::{Self, Hello};
use std::unit_test::destroy;
#[test]
fun test_greeting() {
let ctx = &mut tx_context::dummy();
let hello: Hello = hello_world::new_hello(ctx);
assert!(hello_world::greeting(&hello) == b"Hello, World!".to_string());
destroy(hello);
}
测试模块属性
#[test_only]
module hello_world::hello_world_tests;
#[test_only] 标注表示这个模块仅在测试时编译,不会包含在发布的包中。
测试函数
#[test]:标记该函数为测试函数。- 使用
tx_context::dummy()模拟交易上下文,调用new_hello得到带key的对象;测试结束用std::unit_test::destroy回收对象(仅测试环境可用)。 - 测试函数不需要
public修饰符。
构建项目
在项目根目录下运行:
cd hello_world
sui move build
成功构建的输出:
UPDATING GIT DEPENDENCY https://github.com/MystenLabs/sui.git
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING hello_world
如果代码有错误,编译器会给出详细的错误信息和位置提示:
error[E01002]: unexpected token
┌─ sources/hello_world.move:5:1
│
5 │ public fun hello_world() String {
│ ^^^^^^
│ Expected ':'
提示:第一次构建会下载 Sui Framework 依赖,可能需要一些时间。后续构建会使用缓存,速度会快很多。
运行测试
sui move test
成功输出:
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING hello_world
Running Move unit tests
[ PASS ] hello_world::hello_world_tests::test_greeting
Test result: OK. Total tests: 1; passed: 1; failed: 0
运行特定测试
如果项目中有多个测试,可以通过名称过滤:
# 只运行名称包含 "hello" 的测试
sui move test hello
查看详细输出
# 显示测试过程中的调试信息
sui move test --verbose
测试覆盖率
sui move test --coverage
# 查看覆盖率报告
sui move coverage summary
链上铸一枚 Hello(可选)
本地 sui move test 通过后,若已按钱包与测试币配置好 sui client,可将本包发布到 devnet/testnet,再调用入口函数 mint_hello,这样浏览器与 CLI 里都会出现真实对象 ID(而非仅控制台打印字符串):
# 在 hello_world 包根目录,发布(详见 §3.2)
sui client publish
# 将输出中的 Package ID 设为环境变量后,一笔交易只调 mint_hello:
sui client ptb --move-call $PACKAGE_ID::hello_world::mint_hello
成功后在交易回执的 Created Objects 中可看到类型为 ...::hello_world::Hello 的新对象;用 sui client object <对象ID> 可查看其中的 greeting 字段。PTB 与更多调用方式见 §3.3。
项目结构最佳实践
随着项目增长,建议采用以下结构:
my_project/
├── Move.toml
├── sources/
│ ├── module_a.move
│ ├── module_b.move
│ └── utils.move
└── tests/
├── module_a_tests.move
└── module_b_tests.move
约定:
- 每个
.move文件包含一个模块 - 文件名与模块名一致
- 测试文件名以
_tests后缀命名 - 将相关功能组织在同一个包中
小结
本节我们完成了第一个 Move 程序的完整开发流程:使用 sui move new 创建项目,理解 Move.toml,编写带 Hello 对象与 entry fun mint_hello 的模块(转移给自己对应 ctx.sender()),编写测试,再通过 sui move build / sui move test 验证。这样你既练习了纯函数式的单元测试路径,又具备了链上可查询输出(对象 ID 与字段)的最小闭环;下一节将以另一示例深入发布交易的细节,§3.3 则系统讲解 PTB 调用。
若你希望先了解全书章节如何递进、哪些章可以跳读,可穿插阅读导读 — 本书结构与阅读方式。