集成测试与单元测试

本文详细介绍了单元测试与集成测试的定义与区别,同时提供了使用 Rust 和 TypeScript 进行集成测试的工作流程与代码示例。文章还涵盖了如何使用 Anchor Manifest 来配置测试环境,以及如何在不同的时间段进行时间推进测试的方法,提供了实用的代码示例和技巧。

单元测试和集成测试

目录

单元测试和集成测试

上下文 单元测试 集成测试
目的 验证单个函数或小组件在隔离状态下的正确性。 测试多个组件之间或整个程序在 Solana 环境下的互动。
范围 狭窄,专注于特定函数或模块。 广泛,涵盖多个组件之间的交互。
复杂性 相对简单,一次测试一段代码。 更复杂,因为它测试程序的不同部分如何协同工作。
依赖性 较少,通常被模拟或与程序的其余部分隔离。 高,需要一个真实或接近真实的 Solana 环境及实际依赖项。
执行时间 快速,因为只测试小单元的代码。 较慢,因为涉及更多组件和可能的网络交互。
测试数据 简化,通常是硬编码或模拟以适应特定测试的函数。 真实,使用与程序在生产中可能遇到的数据相似的数据。
维护性 更容易维护,因为它们专注于特定代码段。 维护更复杂,因为程序的一个部分的变更可能会影响多个测试。
使用案例 确保特定逻辑正确工作(例如,计算余额、执行数学逻辑)。 确保整体程序行为正确(例如,提交多个事务,与其他程序交互)。
示例 测试特定指令处理程序是否正确处理输入。 测试一系列事务或账户之间的交互是否按预期工作。

[!NOTE] 剩余的材料将重点关注集成测试。

[!TIP] 有关单元测试的更多详细信息,请参见 Unit Tests。 你也可以查看 rust-unit-tests 示例

使用 Rust 进行测试

示例

测试工作流程

  1. 初始化 tests 目录
  2. Cargo.toml 中指定所需的 dev-dependencies
  3. 将来自所需集群(例如 Mainnet)的转储 SBF 程序放在 tests/fixtures 目录中。
  4. 编写测试
  5. 执行:
    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

如何编写测试

  1. 初始化新的程序测试实例。
  2. 可选地,包括其他执行所需的程序(用于 CPI 调用)。
  3. 创建(可选)多个钱包。
  4. 空投资金。
  5. 启动上下文。
  6. 创建指令。
  7. 处理事务。
  8. 检查输出。

[!IMPORTANT] 以下示例以及 rust-exampleMetaplex 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());
}

使用 Typescript 进行测试

示例 Anchor 测试

示例与 Bankrun 的 Anchor 测试

测试工作流程

  1. 在项目中初始化 tests 文件夹。
  2. 通过 npmyarn 安装与 Anchor 的 TypeScript 测试相关的必要依赖。
  3. tests 目录中创建或更新 TypeScript 测试文件。
  4. 执行:
    anchor test

    [!TIP] 查看 Anchor 清单,获取关于设置 solana-test-validator 的更多详细信息,或查看 参考

如何编写测试

  1. 生成一个新的密钥对作为测试中的签名者。此密钥对将用于签署事务。
  2. 向签名者的账户空投 SOL,以支付事务费用和测试期间的其他操作。
  3. 使用 Mocha 的 it 函数(或任何其他测试框架)编写独立的测试用例。
  4. 使用 Anchor 方法发送事务,例如调用程序方法,传递必要的账户和签名者,并确保事务得到确认。
  5. 执行事务后,获取相关的链上数据,使用你的测试框架的断言库断言这些数据是否与预期结果匹配。

[!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());
    });

});

Anchor 清单

provider

用于所有命令的钱包和集群。

[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"

scripts

测试脚本由 anchor test 执行。

[!TIP] 其他定义的脚本可以通过 anchor run <script> 运行。但请注意,此命令不会启动 solana-test-validator。

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

registry

在与可验证构建相关的命令中使用的注册表(例如,使用 anchor publish 推送可验证构建时)。

[registry]
url = "https://api.apr.dev"

[!TIP] 在这里阅读有关注册表的更多内容 Publishing Source

features

账户解析是指客户端能够在发送事务时无须手动指定账户的能力。

[features]
resolution = true

对使用 UncheckedAccountAccountInfo 的文档要求。

[features]
skip-lint = false

workspace

types

添加一个目录,当运行 anchor buildanchor idl parse 时,该目录将复制 <idl>.ts 文件。

[!TIP] 当你希望将此文件保存在版本控制中时,这一点很有用,比如在前端使用时,前端可能无法访问由 anchor 生成的目标目录。

[workspace]
types = "app/src/idl/"
members

设置相对于 Anchor.toml 的路径,以定位本地工作区中所有程序,即与每个可以由 anchor CLI 编译的程序相关的 Cargo.toml 清单路径。

[!TIP] 对于使用标准 Anchor 工作流的程序,可以省略此项。对于未用 Anchor 编写但仍希望发布的程序,应添加此项。

[workspace]
members = [
    "programs/*",
    "other_place/my_program"
]

programs

工作区中程序的地址。

[!TIP] 更多程序 = 更多地址。

[programs.localnet]
my_program = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"

test

startup_wait

增加 anchor 等待 solana-test-validator 启动的时间。

[!TIP] 如果你正在进行克隆(请参见 test.validator.clone),并且有许多账户,这将会很有用,因为这会增加验证者的启动时间。

[test]
startup_wait = 10000
upgradeable

使用 --upgradeable-program 将程序部署到 solana-test-validator。这使得测试只能由程序的升级授权者执行特定的指令成为可能。初始升级授权者将设置为 provider.wallet

如果未指定或明确设置为 false,则测试程序将使用 --bpf-program 部署,从而禁用对其的升级。

[test]
upgradeable = true

test.validator

这些选项将传递给 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_portgossip_portfaucet_port 设置为不同的端口,以防止测试验证器启动问题。

[!TIP] 如果由于某种原因 solana-test-validator 未能启动(你收到错误提示 Test validator does not look started...),请记得查看 test-ledger-log.txt 文件,通常位于 .anchor/test-ledger/test-ledger-log.txt。该文件包含有关你的 [test.validator] 配置中可能无效值的信息。

toolchain

覆盖工作区中的工具链数据,类似于 rust-toolchain.toml

[toolchain]
anchor_version = "0.30.1"   # 要使用的 `anchor-cli` 版本(需为 `avm`)
solana_version = "1.18.17"    # 要使用的 Solana 版本(适用于所有 Solana 工具)

从不同集群克隆

genesis

使得像 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.clone

使用此命令将账户从 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"

test.validator.account

使用此命令从 .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"

时间推进

使用 Rust 进行测试

[!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);
}

使用 Typescript 进行测试

选项 1

不要推进时间。相反,更新截止阈值至较低的数字(即几秒)并使用 sleep() 方法所需的时间。这并不是最佳方法,但仍然有用。

选项 2

使用 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Ackee-Blockchain
Ackee-Blockchain
江湖只有他的大名,没有他的介绍。