Builder 模式测试
Builder 模式用于以灵活、可读的方式构造具有多个参数的复杂对象。它通过方法调用逐步积累配置,在调用 build() 时产生最终对象。这一模式在测试中尤为有用——你经常需要创建仅有细微差异的对象,同时保持大多数字段使用合理的默认值。
Builder 模式在发布代码中可能因中间结构体和多次函数调用而增加 Gas 成本。此模式最适合测试场景,其中可读性和可维护性比 Gas 消耗更重要。
定义 Builder
Builder 结构体镜像目标对象的字段,但使用 Option 类型包装。典型的 Builder 提供:
new()创建空 Builder- Setter 方法配置各字段并返回 Builder 用于链式调用
build()使用默认值填充未设置的字段,构造最终对象
module book::user;
public struct User has copy, drop {
name: String,
balance: u64,
is_active: bool,
level: u8,
}
对应的 Builder:
#[test_only]
module book::user_builder;
use book::user::User;
public struct UserBuilder has copy, drop {
name: Option<String>,
balance: Option<u64>,
is_active: Option<bool>,
level: Option<u8>,
}
public fun new(): UserBuilder {
UserBuilder {
name: option::none(),
balance: option::none(),
is_active: option::none(),
level: option::none(),
}
}
public fun name(mut self: UserBuilder, name: String): UserBuilder {
self.name = option::some(name);
self
}
public fun balance(mut self: UserBuilder, balance: u64): UserBuilder {
self.balance = option::some(balance);
self
}
public fun is_active(mut self: UserBuilder, is_active: bool): UserBuilder {
self.is_active = option::some(is_active);
self
}
public fun level(mut self: UserBuilder, level: u8): UserBuilder {
self.level = option::some(level);
self
}
public fun build(self: UserBuilder): User {
User {
name: self.name.destroy_or!(b"default".to_string()),
balance: self.balance.destroy_or!(0),
is_active: self.is_active.destroy_or!(true),
level: self.level.destroy_or!(1),
}
}
使用示例
没有 Builder 时
每个测试必须指定所有字段,即使只有一个字段与测试相关:
#[test]
fun inactive_user_without_builder() {
let user = User {
name: b"Alice".to_string(),
balance: 0,
is_active: false, // 只关心这个字段
level: 1,
};
assert!(!user.is_active);
}
使用 Builder 后
测试变得聚焦且自文档化:
#[test]
fun inactive_user_with_builder() {
let user = user_builder::new()
.is_active(false)
.build();
assert!(!user.is_active);
}
#[test]
fun high_level_user() {
let user = user_builder::new()
.name(b"Hero".to_string())
.level(99)
.build();
assert_eq!(user.level, 99);
}
每个测试清楚地展示了哪个字段是关键的。向 User 添加新字段时只需更新 Builder 的 build() 函数添加默认值——现有测试无需修改。
方法链
流畅 Builder 语法的关键是方法链。每个 setter 方法通过值取得 mut self 的所有权,修改后返回修改过的 Builder:
public fun is_active(mut self: UserBuilder, is_active: bool): UserBuilder {
self.is_active = option::some(is_active);
self
}
链式调用的每个方法消耗前一个 Builder 并返回新的 Builder,最终 build() 消耗 Builder 产生目标对象:
let user = user_builder::new()
.name(b"Alice".to_string())
.balance(1000)
.is_active(true)
.build();
系统包中的使用
Sui Framework 和 Sui System 包广泛使用 Builder 模式进行测试:
ValidatorBuilder
use sui_system::validator_builder;
#[test]
fun validator_operations() {
let validator = validator_builder::preset()
.name("My Validator")
.gas_price(1000)
.commission_rate(500) // 5%
.initial_stake(100_000_000)
.build(ctx);
// 测试验证器操作...
}
TxContextBuilder
use sui::test_scenario as ts;
#[test]
fun epoch_dependent_logic() {
let mut test = ts::begin(@0x1);
let ctx = test
.ctx_builder()
.set_epoch(100)
.set_epoch_timestamp(1000000)
.build();
// 测试依赖 epoch 的逻辑...
test.end();
}
小结
- Builder 模式通过 setter 方法积累配置,通过
build()产生最终对象 - 使用
Option字段使配置可选,在build()中提供合理默认值 - 方法链(
fun method(mut self, ...): Self)创建流畅的 API - Builder 减少测试样板代码,将测试与目标结构体的变更隔离
- 此模式最适合用于测试工具,可读性比 Gas 成本更重要的场景