本文详细介绍了单元测试与集成测试的定义与区别,同时提供了使用 Rust 和 TypeScript 进行集成测试的工作流程与代码示例。文章还涵盖了如何使用 Anchor Manifest 来配置测试环境,以及如何在不同的时间段进行时间推进测试的方法,提供了实用的代码示例和技巧。
上下文 | 单元测试 | 集成测试 |
---|---|---|
目的 | 验证单个函数或小组件在隔离状态下的正确性。 | 测试多个组件之间或整个程序在 Solana 环境下的互动。 |
范围 | 狭窄,专注于特定函数或模块。 | 广泛,涵盖多个组件之间的交互。 |
复杂性 | 相对简单,一次测试一段代码。 | 更复杂,因为它测试程序的不同部分如何协同工作。 |
依赖性 | 较少,通常被模拟或与程序的其余部分隔离。 | 高,需要一个真实或接近真实的 Solana 环境及实际依赖项。 |
执行时间 | 快速,因为只测试小单元的代码。 | 较慢,因为涉及更多组件和可能的网络交互。 |
测试数据 | 简化,通常是硬编码或模拟以适应特定测试的函数。 | 真实,使用与程序在生产中可能遇到的数据相似的数据。 |
维护性 | 更容易维护,因为它们专注于特定代码段。 | 维护更复杂,因为程序的一个部分的变更可能会影响多个测试。 |
使用案例 | 确保特定逻辑正确工作(例如,计算余额、执行数学逻辑)。 | 确保整体程序行为正确(例如,提交多个事务,与其他程序交互)。 |
示例 | 测试特定指令处理程序是否正确处理输入。 | 测试一系列事务或账户之间的交互是否按预期工作。 |
[!NOTE] 剩余的材料将重点关注集成测试。
[!TIP] 有关单元测试的更多详细信息,请参见 Unit Tests。 你也可以查看 rust-unit-tests 示例。
tests
目录Cargo.toml
中指定所需的 dev-dependencies
tests/fixtures
目录中。cargo test-sbf
[!NOTE] 有多种存储转储程序的选项,但
tests/fixtures
是最简单的。欲了解更多信息,请查看 Docs。[!IMPORTANT] 在
add_program(...)
函数中提供的程序名称必须与转储的*.so
文件相同(见 示例)。[!TIP] 默认情况下,测试会话并行执行,意味着所有测试并行执行;要实现顺序行为,请使用:
cargo test-sbf -- --test-threads=1
[!TIP] 要从所需集群转储程序,请使用:
# "-u m" 表示主网 # solana program dump -u m <PROGRAM_ID> <PROGRAM_NAME>.so
[!IMPORTANT] 以下示例以及 rust-example 将 Metaplex Metadata Program 添加到程序测试环境中,以初始化 Mint 的元数据->创建可替代和不可替代代币的常见行为。
use solana_program_test::*;
// 其他所需的使用语句
use rust_tests::entry;
// 程序 ID,程序名称和相关程序的常量。
const PROGRAM_ID: Pubkey = rust_tests::ID_CONST; // 定义程序 ID 常量。
const PROGRAM_NAME: &str = "rust_tests"; // 定义程序名称。
const MPL_TOKEN_METADATA: &str = "mpl_token_metadata"; // 定义 MPL 代币元数据程序的名称。
mod instructions;
mod utils;
##[tokio::test]
async fn test_with_rust_1() {
// 1. 使用程序名称、程序 ID 和入口处理器初始化新的 ProgramTest 实例。
let mut program_test =
ProgramTest::new(PROGRAM_NAME, PROGRAM_ID, processor!(convert_entry!(entry)));
// 2. 将 MPL Token Metadata 程序添加到测试环境中。
program_test.add_program(MPL_TOKEN_METADATA, mpl_token_metadata::ID, None);
// 3. 为签名者和铸币者生成新的密钥对。
let signer = utils::generate_signer();
let mint = utils::generate_signer();
// 4. 向签名者的账户空投一些 SOL 以资助测试事务。
utils::airdrop(&mut program_test, signer.pubkey(), 5 * LAMPORTS_PER_SOL);
// 5. 启动程序测试上下文,模拟 Solana 运行时。
let mut program_test_context = program_test.start_with_context().await;
// 6. 在这里执行操作,例如构造指令
// 7. 在模拟环境中处理初始化指令。
let res = utils::process_instruction(
&mut program_test_context,
ix_initialize,
&signer.pubkey(),
signers,
)
.await;
// 8. 断言指令成功。
assert!(res.is_ok());
}
tests
文件夹。npm
或 yarn
安装与 Anchor 的 TypeScript 测试相关的必要依赖。tests
目录中创建或更新 TypeScript 测试文件。anchor test
[!TIP] 查看 Anchor 清单,获取关于设置
solana-test-validator
的更多详细信息,或查看 参考。
it
函数(或任何其他测试框架)编写独立的测试用例。[!IMPORTANT] 以下示例展示了 TypeScript 测试的结构。要查看完整示例,请查看 anchor-example。
import * as anchor from "@coral-xyz/anchor"; // 导入用于与 Solana 程序交互的 Anchor 库
import { Program } from "@coral-xyz/anchor"; // 从 Anchor 库导入 Program 类型
import { AnchorTests } from '../target/types/anchor_tests'; // 导入 AnchorTests 程序的类型定义
// "anchor-tests" 程序的测试套件描述块
describe("anchor-tests", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.AnchorTests as anchor.Program<AnchorTests>;
// 1. 生成一个新的密钥对,作为测试中的签名者
const signer = anchor.web3.Keypair.generate();
// 2.
before('Prepare', async () => {
// 向签名者的公钥空投 SOL,以支付事务费用和初始化费用
await provider.connection.confirmTransaction(
await provider.connection.requestAirdrop(signer.publicKey, 5_000_000_000), // 空投 5 SOL
'confirmed' // 等待交易确认
);
});
// 3.
it('Initialize', async () => {
// 4. 使用所需参数、账户和签名者调用程序的初始化方法
await program.methods.initialize(...).accounts({...}).signers([...]).rpc({ commitment: "confirmed" });
// 5. 从链上账户中获取初始化后账户数据
let accountData = await program.account.dataAccount.fetch(...);
// 断言获取的数据与预期数据匹配
assert.strictEqual(accountData.someField.toString(), expected_data.toString());
assert.strictEqual(accountData.someOtherField.toString(), some_other_expected_data.toString());
});
});
用于所有命令的钱包和集群。
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
测试脚本由 anchor test
执行。
[!TIP] 其他定义的脚本可以通过
anchor run <script>
运行。但请注意,此命令不会启动 solana-test-validator。
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
在与可验证构建相关的命令中使用的注册表(例如,使用 anchor publish
推送可验证构建时)。
[registry]
url = "https://api.apr.dev"
[!TIP] 在这里阅读有关注册表的更多内容 Publishing Source。
账户解析是指客户端能够在发送事务时无须手动指定账户的能力。
[features]
resolution = true
对使用 UncheckedAccount
和 AccountInfo
的文档要求。
[features]
skip-lint = false
添加一个目录,当运行 anchor build
或 anchor idl parse
时,该目录将复制 <idl>.ts
文件。
[!TIP] 当你希望将此文件保存在版本控制中时,这一点很有用,比如在前端使用时,前端可能无法访问由 anchor 生成的目标目录。
[workspace]
types = "app/src/idl/"
设置相对于 Anchor.toml 的路径,以定位本地工作区中所有程序,即与每个可以由 anchor CLI 编译的程序相关的 Cargo.toml 清单路径。
[!TIP] 对于使用标准 Anchor 工作流的程序,可以省略此项。对于未用 Anchor 编写但仍希望发布的程序,应添加此项。
[workspace]
members = [
"programs/*",
"other_place/my_program"
]
工作区中程序的地址。
[!TIP] 更多程序 = 更多地址。
[programs.localnet]
my_program = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
增加 anchor 等待 solana-test-validator
启动的时间。
[!TIP] 如果你正在进行克隆(请参见
test.validator.clone
),并且有许多账户,这将会很有用,因为这会增加验证者的启动时间。
[test]
startup_wait = 10000
使用 --upgradeable-program
将程序部署到 solana-test-validator。这使得测试只能由程序的升级授权者执行特定的指令成为可能。初始升级授权者将设置为 provider.wallet
。
如果未指定或明确设置为 false
,则测试程序将使用 --bpf-program
部署,从而禁用对其的升级。
[test]
upgradeable = true
这些选项将传递给 solana-test-validator
cli 中的相同名称的选项(见 solana-test-validator --help
),例如在执行 anchor test
时。
[test.validator]
url = "https://api.mainnet-beta.solana.com" # 这是账户克隆自的集群的 URL(见 `test.validator.clone`)。
warp_slot = "1337" # 启动验证者后将账本扭曲到 `warp_slot`。
slots_per_epoch = "32" # 覆盖一个纪元中的插槽数量(值必须 >= 32)
rpc_port = 8896 # 在此端口上设置 JSON RPC,以及下一个用于 RPC websocket 的端口。
limit_ledger_size = "1337" # 在根插槽中保留此数量的碎片。
ledger = "test-ledger" # 设置账本位置。
gossip_port = 8994 # 验证者的 gossip 端口号。
gossip_host = "127.0.0.1" # 验证者在 gossip 中公布的 DNS 名称或 IP 地址。
faucet_sol = "1337" # 在创世时给水龙头地址分配这很多 SOL。
faucet_port = 8995 # 在此端口上启用水龙头。
dynamic_port_range = "1337-13337" # 用于动态分配端口的范围。
bind_address = "0.0.0.0"
[!TIP] 最好将
rpc_port
、gossip_port
和faucet_port
设置为不同的端口,以防止测试验证器启动问题。[!TIP] 如果由于某种原因
solana-test-validator
未能启动(你收到错误提示Test validator does not look started...
),请记得查看test-ledger-log.txt
文件,通常位于.anchor/test-ledger/test-ledger-log.txt
。该文件包含有关你的 [test.validator] 配置中可能无效值的信息。
覆盖工作区中的工具链数据,类似于 rust-toolchain.toml
。
[toolchain]
anchor_version = "0.30.1" # 要使用的 `anchor-cli` 版本(需为 `avm`)
solana_version = "1.18.17" # 要使用的 Solana 版本(适用于所有 Solana 工具)
使得像 anchor test
的命令可以在已加载特定程序的情况下启动 solana-test-validator
。
[!IMPORTANT] 这是如何使用 Cross Program Invocation 的一种方式,针对例如从主网转储的 SBF 程序使用。
[!TIP] 要从所需集群转储程序,请使用:
# "-u m" 表示主网 # solana program dump -u m <PROGRAM_ID> <PROGRAM_NAME>.so
[[test.genesis]]
address = "srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX"
program = "dex.so" # SBF 的路径
[[test.genesis]]
address = "22Y43yTVxuUkoRKdm9thyRhQ3SdgQS7c7kB6UNCiaczD"
program = "swap.so" # SBF 的路径
upgradeable = true
使用此命令将账户从 test.validator.url
集群克隆到你的本地集群。
[!IMPORTANT] 这是使用 Cross Program Invocation 的另一种方式,针对例如从主网转储的 SBF 程序使用。
[test.validator]
url = "https://api.mainnet-beta.solana.com"
[[test.validator.clone]]
address = "7NL2qWArf2BbEBBH1vTRZCsoNqFATTddH6h8GkVvrLpG"
[[test.validator.clone]]
address = "2RaN5auQwMdg5efgCaVqpETBV8sacWGR8tkK4m9kjo5r"
[[test.validator.clone]]
address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
使用此命令从 .json
文件上传账户。
[!TIP] 要从所需集群转储账户,请使用:
# "-u m" 表示主网 # solana account -u m <ACCOUNT_ADDRESS> --output json
[[test.validator.account]]
address = "Ev8WSPQsGb4wfjybqff5eZNcS3n6HaMsBkMk9suAiuM"
filename = "some_account.json"
[[test.validator.account]]
address = "Ev8WSPQsGb4wfjybqff5eZNcS3n6HaMsBkMk9suAiuM"
filename = "some_other_account.json"
[!TIP] ProgramTest 提供多种方法,可以更新 Clock Sysvar 值。
使用 set_sysvar
方法向前推进时间(也在 rust-example 中使用)。
// 向前推进程序测试上下文时间指定的秒数的函数。
pub async fn forward_time(program_test_context: &mut ProgramTestContext, seconds: i64) {
// 从程序测试上下文获取当前时钟状态。
let mut clock = program_test_context
.banks_client
.get_sysvar::<Clock>()
.await
.unwrap();
// 计算推进时间后的新时间戳。
let new_timestamp = clock.unix_timestamp + seconds;
// 使用新时间戳更新 Clock 实例。
clock.unix_timestamp = new_timestamp;
// 使用新的 Clock 状态在程序测试上下文中更新 sysvar。
program_test_context.set_sysvar(&clock);
}
不要推进时间。相反,更新截止阈值至较低的数字(即几秒)并使用 sleep()
方法所需的时间。这并不是最佳方法,但仍然有用。
使用 Bankrun 向前推进时间。
下面的代码展示了示例(也在 bankrun-example 中实现)。
// 获取 Clock Sysvar
let clock = await test_env.context.banksClient.getClock()
// 获取当前时间戳
const now = clock.unixTimestamp;
// 计算所需的未来 Unix 时间戳
const in_future_7_days = now + BigInt(7 * 24 * 60 * 60);
// 初始化新的 Clock 实例
let new_clock = new Clock(clock.slot, clock.epochStartTimestamp, clock.epoch, clock.leaderScheduleEpoch, in_future_7_days);
// 设置新的 Clock Sysvar
test_env.context.setClock(new_clock);
[!IMPORTANT] 即使 Bankrun 可以与 Anchor 框架一起使用,仍需注意它与 Anchor 框架的环境不同。Anchor 启动
solana-test-validator
,而 Bankrun 在底层使用 ProgramTest,这意味着事务在两个不同的环境中处理。然而,它可能在一些独立的测试路径中有益使用 Bankrun。
- 原文链接: github.com/Ackee-Blockch...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!