如何使用 LiteSVM 测试 Solana 程序

本文介绍了LiteSVM,一个轻量级的Solana虚拟机,它允许在Rust测试环境中运行完整的Solana运行时,无需外部验证器。文章详细说明了如何使用LiteSVM测试Anchor程序,包括环境设置、编写程序测试、以及使用anchor-litesvm减少样板代码。通过具体的代码示例和测试案例,展示了LiteSVM在简化Solana程序测试流程、提高测试效率方面的优势。

概述

快速、可靠的测试至关重要,因为 Solana 程序变得越来越复杂,并且团队将更多的工作流程自动化。传统方法通常依赖于solana-test-validator、Docker 或后台进程,这些都会降低速度,并使测试更难在机器和 CI 环境中重现。

LiteSVM 通过完全在你的 Rust 测试流程中运行完整的 Solana 运行时来解决这个问题。你可以对账户、插槽和系统变量进行细粒度的控制,更快的反馈循环,以及在任何地方行为一致的测试,而无需外部验证器或额外的设置。

本指南将引导你使用 LiteSVM 以更快、更简化的方式端到端地测试 Anchor 程序。

你将做什么

你将学习设置 LiteSVM 来测试 Anchor 程序所需的一切,包括如何:

  • 在 Rust 测试 crate 中设置 LiteSVM
  • 用 Rust 编写程序测试
  • 使用 anchor-litesvm 减少样板代码

你需要什么

本指南假设你对 Solana 编程、Anchor、Rust 和单元测试有基本的了解。

你还应该具备:

依赖项 版本
Solana CLI 3.0.6
Anchor 0.31.1
LiteSVM 0.8.1
Node 24.8.0
Rust 1.90.0
solana-kite 0.1.0
anchor-litesvm 0.2.0

什么是 LiteSVM?

LiteSVM 是一个轻量级的 Solana 虚拟机,可以在你的测试中运行,因此你无需启动单独的验证器即可测试程序。嵌入 VM 可以消除开销并快速执行测试。它附带一个直观的 API,可用于 Rust、TypeScript/JavaScript 和 Python(通过 solders 库)。

LiteSVM 的主要功能:

  • 在进程中运行单元测试(没有外部验证器)
  • 直接构造和操作账户(系统、PDA、SPL Token)
  • 控制时间/插槽、区块哈希检查和计算预算以进行场景测试
  • 模拟 vs. 执行交易并断言日志、事件和余额
  • 内置分析工具,可发现性能问题

将 LiteSVM 添加到你的项目就像包含 LiteSVM crates 一样简单:

LiteSVM Core(基本测试框架)

cargo add --dev litesvm

SPL Token 支持(可选)

cargo add --dev litesvm-token

Anchor Escrow 程序

我们将使用现有的 Anchor Escrow 程序来学习如何使用 LiteSVM 实现测试。

Anchor Escrow 程序通过将 token 锁定在程序控制的金库中,直到双方都满足约定的条款,从而实现双方之间安全、无需信任的 token 交换。示例程序包括 token 交换(初始化、交换、取消)和基线测试。

该程序实现了三个核心指令:

  1. make_offer: 允许 maker 通过指定他们想要交易多少 token A 来换取 token B 来创建 offer
  2. take_offer: 使 taker 能够接受 offer 并执行交换
  3. refund_offer: 允许 maker 取消他们的 offer 并收回他们的 token

克隆存储库:

git clone git@github.com:quiknode-labs/you-will-build-a-solana-program.git anchor-escrow-2025
cd anchor-escrow-2025

运行现有测试以确认你的环境已正确设置:

cargo test

预期输出:

running 11 tests

test test_id ... ok
test tests::test_same_token_mints_fails ... ok
test tests::test_zero_token_a_offered_amount_fails ... ok
test tests::test_take_offer_insufficient_funds_fails ... ok
test tests::test_duplicate_offer_id_fails ... ok
test tests::test_make_offer_succeeds ... ok
test tests::test_insufficient_funds_fails ... ok
test tests::test_non_maker_cannot_refund_offer ... ok
test tests::test_zero_token_b_wanted_amount_fails ... ok
test tests::test_take_offer_success ... ok
test tests::test_refund_offer_success ... ok

test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s

测试结构

每个测试都遵循一致的模式:

  1. 初始化测试环境:调用 setup_escrow_test() 以创建一个新的 EscrowTestEnvironment,其中包含 LiteSVM、部署程序、铸造 token 并为用户帐户提供资金。
  2. 执行程序指令:使用辅助函数构建和发送交易。
  3. 断言预期结果:验证余额、帐户状态和交易结果。

测试辅助函数

我们的测试的辅助函数位于 programs/escrow/src/escrow_test_helpers.rs

以下是你将使用的一些关键测试助手:

  • EscrowTestEnvironment:一个紧凑的 fixture,它打包了测试 LiteSVM 实例、程序 ID、两个 token mint、已资助的用户 (alice/bob) 及其 ATA。
  • setup_escrow_test:一个工厂,用于创建和启动环境、mint、资助用户及其 ATA,并返回一个随时可用的 EscrowTestEnvironment
  • Discriminator Helpers:由于该项目的现有测试不使用 Anchor 客户端,因此我们必须手动添加 Anchor 的 8 字节 discriminator。这些助手计算并添加它们的前缀,以便每次调用都路由到正确的处理程序。
  • Execute Helpers:高级函数,用于派生 PDA、构建指令数据并发送 maketakerefund 的交易。

Make Offer 测试

test_make_offer_succeeds 验证了基本的 offer 创建流程。

该测试设置环境,生成唯一的 offer ID,派生必要的 PDA(offer 帐户和金库),构建帐户结构,创建一个指令,指示 Alice 提供 1 个 TOKEN_A 以换取 1 个 TOKEN_B,发送交易,并断言成功。

programs/escrow/src/tests.rs

#[test]
fn test_make_offer_succeeds() {
    let mut test_environment = setup_escrow_test();

    let offer_id = generate_offer_id();
    let (offer_account, _offer_bump) = get_pda_and_bump(&seeds!["offer", offer_id], &test_environment.program_id);
    let vault = spl_associated_token_account::get_associated_token_address(
        &offer_account,
        &test_environment.token_mint_a.pubkey(),
    );

    let make_offer_accounts = build_make_offer_accounts(
        test_environment.alice.pubkey(),
        test_environment.token_mint_a.pubkey(),
        test_environment.token_mint_b.pubkey(),
        test_environment.alice_token_account_a,
        offer_account,
        vault,
    );

    let make_offer_instruction = build_make_offer_instruction(
        offer_id,
        1 * TOKEN_A,
        1 * TOKEN_B,
        make_offer_accounts,
    );

    let result = send_transaction_from_instructions(
        &mut test_environment.litesvm,
        vec![make_offer_instruction],
        &[&test_environment.alice],
        &test_environment.alice.pubkey(),
    );

    assert!(result.is_ok(), "Valid offer should succeed");
}

test_duplicate_offer_id_fails 确保 offer ID 是唯一的。

Alice 首先使用特定 ID 成功创建一个 offer,然后 Bob 尝试使用相同的 ID 创建不同的 offer(这将派生到相同的 PDA),并且测试验证此第二次交易失败,因为该帐户已存在。

programs/escrow/src/tests.rs

#[test]
fn test_duplicate_offer_id_fails() {
    let mut test_environment = setup_escrow_test();

    let offer_id = generate_offer_id();
    let (offer_account, _offer_bump) = get_pda_and_bump(&seeds!["offer", offer_id], &test_environment.program_id);
    let vault = spl_associated_token_account::get_associated_token_address(
        &offer_account,
        &test_environment.token_mint_a.pubkey(),
    );

    let make_offer_accounts = build_make_offer_accounts(
        test_environment.alice.pubkey(),
        test_environment.token_mint_a.pubkey(),
        test_environment.token_mint_b.pubkey(),
        test_environment.alice_token_account_a,
        offer_account,
        vault,
    );

    let make_offer_instruction = build_make_offer_instruction(
        offer_id,
        1 * TOKEN_A,
        1 * TOKEN_B,
        make_offer_accounts,
    );

    let result = send_transaction_from_instructions(
        &mut test_environment.litesvm,
        vec![make_offer_instruction],
        &[&test_environment.alice],
        &test_environment.alice.pubkey(),
    );
    assert!(result.is_ok(), "First offer should succeed");

    let make_offer_accounts_with_existing_offer_id = build_make_offer_accounts(
        test_environment.bob.pubkey(),
        test_environment.token_mint_a.pubkey(),
        test_environment.token_mint_b.pubkey(),
        test_environment.bob_token_account_a,
        offer_account,
        vault,
    );

    let make_offer_instruction_with_existing_offer_id = build_make_offer_instruction(
        offer_id,
        1 * TOKEN_A,
        1 * TOKEN_B,
        make_offer_accounts_with_existing_offer_id,
    );

    let result = send_transaction_from_instructions(
        &mut test_environment.litesvm,
        vec![make_offer_instruction_with_existing_offer_id],
        &[&test_environment.bob],
        &test_environment.bob.pubkey(),
    );
    assert!(result.is_err(), "Second offer with same ID should fail");
}

test_insufficient_funds_fails 检查余额验证。

Alice 试图为 1000 个 TOKEN_A 创建一个 offer,尽管她的帐户中只有 10 个 TOKEN_A,并且测试确认由于资金不足,交易失败。

programs/escrow/src/tests.rs

#[test]
fn test_insufficient_funds_fails() {
    let mut test_environment = setup_escrow_test();

    // 尝试创建 offer,其中 token 数量超过 Alice 拥有的数量
    let offer_id = generate_offer_id();
    let (offer_account, _offer_bump) = get_pda_and_bump(&seeds!["offer", offer_id], &test_environment.program_id);
    let vault = spl_associated_token_account::get_associated_token_address(
        &offer_account,
        &test_environment.token_mint_a.pubkey(),
    );

    let make_offer_accounts = build_make_offer_accounts(
        test_environment.alice.pubkey(),
        test_environment.token_mint_a.pubkey(),
        test_environment.token_mint_b.pubkey(),
        test_environment.alice_token_account_a,
        offer_account,
        vault,
    );

    let make_offer_instruction = build_make_offer_instruction(
        offer_id,
        1000 * TOKEN_A, // 尝试提供 1000 个 token (Alice 只有 10 个)
        1 * TOKEN_B,
        make_offer_accounts,
    );

    let result = send_transaction_from_instructions(
        &mut test_environment.litesvm,
        vec![make_offer_instruction],
        &[&test_environment.alice],
        &test_environment.alice.pubkey(),
    );
    assert!(result.is_err(), "Offer with insufficient funds should fail");
}

test_same_token_mints_fails 强制执行不同的 token 类型。

该测试构建了一个 offer,其中 TOKEN_ATOKEN_B 都指向相同的 mint,并验证程序拒绝此无效配置。

programs/escrow/src/tests.rs

#[test]
fn test_same_token_mints_fails() {
    let mut test_environment = setup_escrow_test();

    // 尝试为 token_a 和 token_b 创建具有相同 token mint 的 offer
    let offer_id = generate_offer_id();
    let (offer_account, _offer_bump) = get_pda_and_bump(&seeds!["offer", offer_id], &test_environment.program_id);
    let vault = spl_associated_token_account::get_associated_token_address(
        &offer_account,
        &test_environment.token_mint_a.pubkey(),
    );

    let make_offer_accounts = build_make_offer_accounts(
        test_environment.alice.pubkey(),
        test_environment.token_mint_a.pubkey(),
        test_environment.token_mint_a.pubkey(), // 两个都使用相同的 mint
        test_environment.alice_token_account_a,
        offer_account,
        vault,
    );

    let make_offer_instruction =
        build_make_offer_instruction(offer_id, 1 * TOKEN_A, 1 * TOKEN_B, make_offer_accounts);

    let result = send_transaction_from_instructions(
        &mut test_environment.litesvm,
        vec![make_offer_instruction],
        &[&test_environment.alice],
        &test_environment.alice.pubkey(),
    );
    assert!(result.is_err(), "Offer with same token mints should fail");
}

test_zero_token_b_wanted_amount_fails 验证非零的 wanted amount。

该测试创建了一个 offer,请求 0 个 TOKEN_B 以换取 1 个 TOKEN_A,并确认程序拒绝零金额 offer。

programs/escrow/src/tests.rs

#[test]
fn test_zero_token_b_wanted_amount_fails() {
    let mut test_environment = setup_escrow_test();

    // 尝试创建零 token_b_wanted_amount 的 offer
    let offer_id = generate_offer_id();
    let (offer_account, _offer_bump) = get_pda_and_bump(&seeds!["offer", offer_id], &test_environment.program_id);
    let vault = spl_associated_token_account::get_associated_token_address(
        &offer_account,
        &test_environment.token_mint_a.pubkey(),
    );

    let make_offer_accounts = build_make_offer_accounts(
        test_environment.alice.pubkey(),
        test_environment.token_mint_a.pubkey(),
        test_environment.token_mint_b.pubkey(),
        test_environment.alice_token_account_a,
        offer_account,
        vault,
    );

    let make_offer_instruction = build_make_offer_instruction(
        offer_id,
        1 * TOKEN_A,
        0, // 零 wanted amount
        make_offer_accounts,
    );

    let result = send_transaction_from_instructions(
        &mut test_environment.litesvm,
        vec![make_offer_instruction],
        &[&test_environment.alice],
        &test_environment.alice.pubkey(),
    );
    assert!(
        result.is_err(),
        "Offer with zero token_b_wanted_amount should fail"
    );
}

test_zero_token_a_offered_amount_fails 验证非零的 offered amount。

与之前的测试类似,这会创建一个 offer,其中提供 0 个 TOKEN_A 以换取 1 个 TOKEN_B,并验证是否拒绝。

programs/escrow/src/tests.rs

#[test]
fn test_zero_token_a_offered_amount_fails() {
    let mut test_environment = setup_escrow_test();

    // 尝试创建零 token_a_offered_amount 的 offer
    let offer_id = generate_offer_id();
    let (offer_account, _offer_bump) = get_pda_and_bump(&seeds!["offer", offer_id], &test_environment.program_id);
    let vault = spl_associated_token_account::get_associated_token_address(
        &offer_account,
        &test_environment.token_mint_a.pubkey(),
    );

    let make_offer_accounts = build_make_offer_accounts(
        test_environment.alice.pubkey(),
        test_environment.token_mint_a.pubkey(),
        test_environment.token_mint_b.pubkey(),
        test_environment.alice_token_account_a,
        offer_account,
        vault,
    );

    let make_offer_instruction = build_make_offer_instruction(
        offer_id,
        0, // 零 offered amount
        1 * TOKEN_B,
        make_offer_accounts,
    );

    let result = send_transaction_from_instructions(
        &mut test_environment.litesvm,
        vec![make_offer_instruction],
        &[&test_environment.alice],
        &test_environment.alice.pubkey(),
    );
    assert!(
        result.is_err(),
        "Offer with zero token_a_offered_amount should fail"
    );
}

Take Offer 测试

test_take_offer_success 验证了完整的交换流程。

Alice 创建了一个 offer,提供 3 个 TOKEN_A,想要 2 个 TOKEN_B,Bob 接受了它,并且测试验证了最终余额(Alice:7 个 TOKEN_A 和 2 个 TOKEN_B,Bob:3 个 TOKEN_A 和 3 个 TOKEN_B),并确认 offer 帐户已关闭。

programs/escrow/src/tests.rs

#[test]
fn test_take_offer_success() {
    let mut test_environment = setup_escrow_test();

    // Alice 创建了一个 offer:3 个 token A 换 2 个 token B
    let offer_id = generate_offer_id();
    let alice = test_environment.alice.insecure_clone();
    let alice_token_account_a = test_environment.alice_token_account_a;
    let (offer_account, vault) = execute_make_offer(
        &mut test_environment,
        offer_id,
        &alice,
        alice_token_account_a,
        3 * TOKEN_A,
        2 * TOKEN_B,
    ).unwrap();

    // Bob 接受了 offer
    let bob = test_environment.bob.insecure_clone();
    let bob_token_account_a = test_environment.bob_token_account_a;
    let bob_token_account_b = test_environment.bob_token_account_b;
    let alice_token_account_b = test_environment.alice_token_account_b;
    execute_take_offer(
        &mut test_environment,
        &bob,
        &alice,
        bob_token_account_a,
        bob_token_account_b,
        alice_token_account_b,
        offer_account,
        vault,
    ).unwrap();

    // 检查余额
    assert_token_balance(
        &test_environment.litesvm,
        &test_environment.alice_token_account_a,
        7 * TOKEN_A,
        "Alice 应该剩下 7 个 token A",
    );
    assert_token_balance(
        &test_environment.litesvm,
        &test_environment.alice_token_account_b,
        2 * TOKEN_B,
        "Alice 应该收到 2 个 token B",
    );
    assert_token_balance(
        &test_environment.litesvm,
        &test_environment.bob_token_account_a,
        3 * TOKEN_A,
        "Bob 应该收到 3 个 token A",
    );
    assert_token_balance(
        &test_environment.litesvm,
        &test_environment.bob_token_account_b,
        3 * TOKEN_B,
        "Bob 应该剩下 3 个 token B",
    );

    // 检查 offer 帐户在被接受后是否已关闭
    check_account_is_closed(
        &test_environment.litesvm,
        &offer_account,
        "Offer 帐户在被接受后应关闭"
    );
}

test_take_offer_insufficient_funds_fails 确保 taker 有足够的余额。

Alice 创建了一个 offer,想要 1000 个 TOKEN_B(Bob 只有 5 个),Bob 试图接受它,并且测试确认由于 Bob 的资金不足,交易失败。

programs/escrow/src/tests.rs

#[test]
fn test_take_offer_insufficient_funds_fails() {
    let mut test_environment = setup_escrow_test();

    // 从 Alice 那里创建一个用于大量 token B 的 offer
    let large_token_b_amount = 1000 * TOKEN_B; // 远大于 Bob 的余额(他有 5 个)
    let offer_id = generate_offer_id();
    let (offer_account, _offer_bump) = get_pda_and_bump(&seeds!["offer", offer_id], &test_environment.program_id);
    let vault = spl_associated_token_account::get_associated_token_address(
        &offer_account,
        &test_environment.token_mint_a.pubkey(),
    );

    let make_offer_accounts = build_make_offer_accounts(
        test_environment.alice.pubkey(),
        test_environment.token_mint_a.pubkey(),
        test_environment.token_mint_b.pubkey(),
        test_environment.alice_token_account_a,
        offer_account,
        vault,
    );

    let make_offer_instruction = build_make_offer_instruction(
        offer_id,
        1 * TOKEN_A,
        large_token_b_amount,
        make_offer_accounts,
    );

    let result = send_transaction_from_instructions(
        &mut test_environment.litesvm,
        vec![make_offer_instruction],
        &[&test_environment.alice],
        &test_environment.alice.pubkey(),
    );
    assert!(result.is_ok(), "Alice's offer should succeed");

    // 尝试让拥有 token B 不足的 Bob 接受 offer
    let take_offer_accounts = TakeOfferAccounts {
        associated_token_program: spl_associated_token_account::ID,
        token_program: spl_token::ID,
        system_program: anchor_lang::system_program::ID,
        taker: test_environment.bob.pubkey(),
        maker: test_environment.alice.pubkey(),
        token_mint_a: test_environment.token_mint_a.pubkey(),
        token_mint_b: test_environment.token_mint_b.pubkey(),
        taker_token_account_a: test_environment.bob_token_account_a,
        taker_token_account_b: test_environment.bob_token_account_b,
        maker_token_account_b: test_environment.alice_token_account_b,
        offer_account,
        vault,
    };

    let take_offer_instruction = build_take_offer_instruction(take_offer_accounts);
    let result = send_transaction_from_instructions(
        &mut test_environment.litesvm,
        vec![take_offer_instruction],
        &[&test_environment.bob],
        &test_environment.bob.pubkey(),
    );
    assert!(
        result.is_err(),
        "拥有资金不足的 Take offer 应该失败"
    );
}

Refund Offer 测试

test_refund_offer_success 验证了退款机制。

Alice 创建了一个 offer,锁定了 3 个 TOKEN_A(余额降至 7 个),退还了它,并且测试验证了她的余额恢复到 10 个 TOKEN_A,并且 offer 帐户已关闭。

programs/escrow/src/tests.rs

#[test]
fn test_refund_offer_success() {
    let mut test_environment = setup_escrow_test();

    // Alice 创建了一个 offer:3 个 token A 换 2 个 token B
    let offer_id = generate_offer_id();
    let alice = test_environment.alice.insecure_clone();
    let alice_token_account_a = test_environment.alice_token_account_a;
    let (offer_account, vault) = execute_make_offer(
        &mut test_environment,
        offer_id,
        &alice,
        alice_token_account_a,
        3 * TOKEN_A,
        2 * TOKEN_B,
    ).unwrap();

    // 检查 Alice 的余额在创建 offer 后是否减少
    assert_token_balance(
        &test_environment.litesvm,
        &test_environment.alice_token_account_a,
        7 * TOKEN_A,
        "Alice 在创建 offer 后应该剩下 7 个 token A",
    );

    // Alice 退还了 offer
    execute_refund_offer(
        &mut test_environment,
        &alice,
        alice_token_account_a,
        offer_account,
        vault,
    ).unwrap();

    // 检查 Alice 的余额在退款后是否已恢复
    assert_token_balance(
        &test_environment.litesvm,
        &test_environment.alice_token_account_a,
        10 * TOKEN_A,
        "Alice 在退款后应该取回全部 10 个 token A",
    );

    // 检查 offer 帐户是否已关闭
    check_account_is_closed(
        &test_environment.litesvm,
        &offer_account,
        "Offer 帐户在退款后应关闭"
    );
}

test_non_maker_cannot_refund_offer 强制执行授权。

Alice 创建了一个 offer,Bob 试图通过使用他的密钥对签名来退款,测试确认交易失败,并验证 Alice 的余额仍然为 7 个 TOKEN_A,并且 offer 帐户仍然存在。

programs/escrow/src/tests.rs

#[test]
fn test_non_maker_cannot_refund_offer() {
    let mut test_environment = setup_escrow_test();

    // Alice 创建了一个 offer:3 个 token A 换 2 个 token B
    let offer_id = generate_offer_id();
    let (offer_account, _offer_bump) = get_pda_and_bump(&seeds!["offer", offer_id], &test_environment.program_id);
    let vault = spl_associated_token_account::get_associated_token_address(
        &offer_account,
        &test_environment.token_mint_a.pubkey(),
    );

    let make_offer_accounts = build_make_offer_accounts(
        test_environment.alice.pubkey(),
        test_environment.token_mint_a.pubkey(),
        test_environment.token_mint_b.pubkey(),
        test_environment.alice_token_account_a,
        offer_account,
        vault,
    );

    let make_offer_instruction = build_make_offer_instruction(
        offer_id,
        3 * TOKEN_A,
        2 * TOKEN_B,
        make_offer_accounts,
    );

    let result = send_transaction_from_instructions(
        &mut test_environment.litesvm,
        vec![make_offer_instruction],
        &[&test_environment.alice],
        &test_environment.alice.pubkey(),
    );
    assert!(result.is_ok(), "Alice's offer should succeed");

    // Bob 尝试退还 Alice 的 offer(应该失败)
    let refund_offer_accounts = RefundOfferAccounts {
        token_program: spl_token::ID,
        system_program: anchor_lang::system_program::ID,
        maker: test_environment.bob.pubkey(),
        token_mint_a: test_environment.token_mint_a.pubkey(),
        maker_token_account_a: test_environment.alice_token_account_a,
        offer_account,
        vault,
    };

    let refund_instruction = build_refund_offer_instruction(refund_offer_accounts);
    let result = send_transaction_from_instructions(
        &mut test_environment.litesvm,
        vec![refund_instruction],
        &[&test_environment.bob],
        &test_environment.bob.pubkey(),
    );
    assert!(
        result.is_err(),
        "非 maker 不应能够退还 offer"
    );

    // 验证 Alice 的余额是否仍然相同(offer 未退还)
    assert_token_balance(
        &test_environment.litesvm,
        &test_environment.alice_token_account_a,
        7 * TOKEN_A,
        "在未能退还后,Alice 的余额应保持不变",
    );

    // 验证 offer 帐户是否仍然存在(反转检查)
    let offer_account_data = test_environment.litesvm.get_account(&offer_account);
    assert!(
        offer_account_data.is_some() && !offer_account_data.unwrap().data.is_empty(),
        "在未能退还后,Offer 帐户应仍然存在"
    );
}

其他 LiteSVM 功能

LiteSVM 提供了强大的功能,这些功能超出了本指南中的示例测试。以下各节展示了一些你可以在测试中实现的先进技术,以实现更大的控制。

创建系统帐户和程序帐户

你通常需要一个有资金的付款人和未初始化的帐户用于 PDA。使用 LiteSVM,你可以在需要时空投到位并写入原始帐户字节。

use solana_account::Account;

let pda_key = Keypair::new().pubkey();
svm.set_account(
    pda_key,
    Account {
        lamports: 100_000_000_000,
        data: vec![0u8; 128],
        owner: PROGRAM_ID,
        executable: false,
        rent_epoch: 0,
    },
);

创建 SPL Token Mint 和帐户

如果你的流程使用 SPL Token,请使用 Pack 序列化 MintAccount。将所有者正确设置为 TOKEN_PROGRAM_ID,并使帐户免租金或简单地超额出资。

use spl_token::{ID as TOKEN_PROGRAM_ID, state::{Mint, Account as TokenAccount}};
use solana_program_pack::Pack;
use solana_keypair::Keypair;

let mint = Keypair::new();
let mut mint_bytes = vec![0; Mint::LEN];
Mint::pack(
    Mint { mint_authority: None.into(), supply: 0, decimals: 6, is_initialized: true, freeze_authority: None.into() },
    &mut mint_bytes
).unwrap();

svm.set_account(
    mint.pubkey(),
    solana_account::Account { lamports: svm.minimum_balance_for_rent_exemption(Mint::LEN), data: mint_bytes, owner: TOKEN_PROGRAM_ID, executable: false, rent_epoch: 0 }
);

let token_acc = Keypair::new();
let owner = Keypair::new();
let mut token_bytes = vec![0; TokenAccount::LEN];
TokenAccount::pack(
    TokenAccount {
        mint: mint.pubkey(),
        owner: owner.pubkey(),
        amount: 0,
        delegate: None.into(),
        state: spl_token::state::AccountState::Initialized,
        is_native: None.into(),
        delegated_amount: 0,
        close_authority: None.into(),
    },
    &mut token_bytes
).unwrap();

svm.set_account(
    token_acc.pubkey(),
    solana_account::Account { lamports: svm.minimum_balance_for_rent_exemption(TokenAccount::LEN), data: token_bytes, owner: TOKEN_PROGRAM_ID, executable: false, rent_epoch: 0 }
);

模拟 Versus 执行和断言

在提交之前,使用模拟来预览日志和指令错误。执行以更新内存中的 ledger 并断言帐户数据和余额。读回帐户并解析你的结构以进行验证。

let res = svm.send_transaction(tx).unwrap();
assert!(res.logs.iter().any(|l| l.contains("Event:EscrowInitialized")));

let acc = svm.get_account(&pda_key).expect("exists");
assert_eq!(acc.owner, PROGRAM_ID);

控制时间、插槽、区块哈希和计算

通过调整系统变量和运行时配置来重现过期路径、陈旧订单和计算回归。通过改变 Clock 来提前时间,跳转到未来的插槽,使区块哈希过期以测试错误处理,并在分析时调整计算预算。

use solana_sdk::clock::Clock;

let mut clock: Clock = svm.get_sysvar::<Clock>();
clock.unix_timestamp += 3600;
svm.set_sysvar::<Clock>(&clock);

svm.warp_to_slot(5```rust
const PROGRAM_ID: &str = "8jR5GeNzeweq35Uo84kGP3v1NcBaZWH5u62k7PxN4T2y";
    const TOKEN_A: u64 = 1_000_000_000; // 9位小数的1个token
    const TOKEN_B: u64 = 1_000_000_000; // 9位小数的1个token

    #[test]
    fn test_make_offer_succeeds() {
        // 使用escrow program初始化AnchorLiteSVM
        let program_id = Pubkey::from_str(PROGRAM_ID).unwrap();
        let program_data = fs::read("../../target/deploy/escrow.so").unwrap();
        let mut ctx = AnchorLiteSVM::build_with_program(program_id, &program_data);

        // 使用anchor-litesvm助手函数创建有资金的账户
        let mint_authority = ctx.svm.create_funded_account(1_000_000_000).unwrap();
        let alice = ctx.svm.create_funded_account(1_000_000_000).unwrap();

        // 使用anchor-litesvm助手函数创建token mint
        let token_mint_a = ctx.svm.create_token_mint(&mint_authority, 9).unwrap();
        let token_mint_b = ctx.svm.create_token_mint(&mint_authority, 9).unwrap();

        // 为Alice创建关联token账户 (token A)
        let alice_token_account_a = ctx.svm
            .create_associated_token_account(&token_mint_a.pubkey(), &alice)
            .unwrap();

        // 将token mint到Alice的token账户
        ctx.svm
            .mint_to(
                &token_mint_a.pubkey(),
                &alice_token_account_a,
                &mint_authority,
                10 * TOKEN_A, // Alice获得10个token A
            )
            .unwrap();

        // 生成offer ID
        let offer_id = 1u64;

        // 使用anchor-litesvm助手函数为offer账户派生PDA
        let (offer_account, _offer_bump) = ctx
            .svm
            .get_pda_with_bump(&[b"offer", &offer_id.to_le_bytes()], &program_id);

        // 计算vault的关联token地址
        let vault = spl_associated_token_account::get_associated_token_address(
            &offer_account,
            &token_mint_a.pubkey(),
        );

        // 手动构建make_offer指令数据
        // Anchor discriminator (8 bytes) + offer_id (8 bytes) + token_a_offered_amount (8 bytes) + token_b_wanted_amount (8 bytes)
        let discriminator_input = b"global:make_offer";
        let discriminator = anchor_lang::solana_program::hash::hash(discriminator_input).to_bytes()[..8].to_vec();

        let mut instruction_data = discriminator;
        instruction_data.extend_from_slice(&offer_id.to_le_bytes());
        instruction_data.extend_from_slice(&(1 * TOKEN_A).to_le_bytes());
        instruction_data.extend_from_slice(&(1 * TOKEN_B).to_le_bytes());

        // 构建账户元数据
        let account_metas = vec![\
            AccountMeta::new_readonly(spl_associated_token_account::ID, false),\
            AccountMeta::new_readonly(spl_token::ID, false),\
            AccountMeta::new_readonly(anchor_lang::system_program::ID, false),\
            AccountMeta::new(alice.pubkey(), true),\
            AccountMeta::new_readonly(token_mint_a.pubkey(), false),\
            AccountMeta::new_readonly(token_mint_b.pubkey(), false),\
            AccountMeta::new(alice_token_account_a, false),\
            AccountMeta::new(offer_account, false),\
            AccountMeta::new(vault, false),\
        ];

        let make_offer_ix = Instruction {
            program_id,
            accounts: account_metas,
            data: instruction_data,
        };

        // 使用anchor-litesvm助手函数执行指令并断言成功
        let result = ctx.execute_instruction(make_offer_ix, &[&alice]).unwrap();
        result.assert_success();
    }
}

要让我们的新测试通过 cargo test 运行,你需要在 lib.rs 中注册测试模块,以便它包含在 crate 的测试工具中:

#[cfg(test)]
mod anchor_tests;

重新运行测试并查找:

test anchor_tests::tests::anchor_test_make_offer_succeeds ... ok

既然你了解了 anchor-litesvm 如何减少对样板代码的需求,请尝试重写其他测试以获得更深入的了解。

总结

很高兴你能坚持到这里!你已经了解了如何使用 LiteSVM 在进程内完整运行 Solana 程序,设置可重用的测试工具,加载已编译的 Anchor 程序,动态构建账户和token状态,并在不启动单独的验证器的情况下验证真实的指令流。你还预览了anchor-litesvm如何减少样板代码,以便你可以专注于程序的逻辑而不是连接。

从这里开始,你将处于有利的位置来扩展这些测试、添加新场景,并将 LiteSVM 用作更快地交付更安全的 Solana 程序的核心部分。

资源

感谢你的❤️反馈!

如果你对新主题有任何反馈或要求,请告诉我们。我们很乐意听取你的意见。

  • 原文链接: quicknode.com/guides/sol...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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