{"content":{"title":"什么是 Bankrun 以及如何使用它来赋能 Solana 开发","body":">- 原文链接：[www.quicknode.com/guides...](https://www.quicknode.com/guides/solana-development/tooling/bankrun)\r\n>- 译者：[AI翻译官](https://learnblockchain.cn/people/19584)，校对：[翻译小组](https://learnblockchain.cn/people/412)\r\n>- 本文链接：[learnblockchain.cn/article…](https://learnblockchain.cn/article/10616)\r\n    \r\n## 概述 \r\n\r\n[Bankrun](https://github.com/kevinheavey/solana-bankrun) 是一个快速、强大且轻量级的框架，用于在 NodeJS 中测试 Solana 程序。它解决了测试 Solana 程序时常见的开发者痛点，从而能够为你节省时间。让我们看看它能为你做些什么以及如何开始使用它！\r\n\r\n### 你将做什么 \r\n\r\n*   了解 Bankrun 是什么以及它如何帮助你测试 Solana 程序\r\n*   创建一个依赖于特定时间和基于账户约束的新 Anchor 程序\r\n*   使用 Bankrun 编写测试以验证程序的功能\r\n\r\n### 你将需要什么 \r\n\r\n*   对 [Solana 基础知识](https://learnblockchain.cn/article/10553) 的基本理解\r\n*   使用 Solana 本地验证器的经验（ [指南：如何设置本地验证器](https://www.quicknode.com/guides/solana-development/getting-started/start-a-solana-local-validator) ）\r\n*   使用 Anchor 的经验（ [指南：如何编写你的第一个 Anchor 程序](https://learnblockchain.cn/article/10706) ）\r\n*   TypeScript 和 Rust 经验\r\n\r\n| 依赖项 | 版本 |\r\n| --- | --- |\r\n| Solana cli | 1.18.8 |\r\n| Anchor CLI | 0.30.1 |\r\n| Node.js | 最新版 |\r\n| yarn | 最新版 |\r\n| ts-node | 最新版 |\r\n| typescript | 最新版 |\r\n| Rust | 最新版 |\r\n\r\n## 什么是 Bankrun? \r\n\r\nBankrun 是 Solana CLI 测试验证器（通过 `solana-test-validator` 运行）的替代方案，旨在提供更快和更灵活的测试。它提供了几个功能，使你能够更轻松地测试 Solana 程序，包括：\r\n\r\n*   更快的测试验证\r\n*   \"时间旅行\" 来修改区块链的状态\r\n*   任意账户数据（这在使用主网的代币铸造和其他相关账户时非常有用）\r\n\r\nBankrun 通过启动一个轻量级的 [BanksServer](https://github.com/solana-labs/solana/blob/master/banks-server/src/banks_server.rs) 工作，这几乎就像一个轻量级的 RPC，并创建一个 [BanksClient](https://github.com/solana-labs/solana/blob/master/banks-client/src/lib.rs) 来与服务器进行通信。\r\n\r\n## 如何使用 Bankrun?\r\n\r\n让我们创建一个简单的 Anchor 项目，以测试一些 Bankrun 的功能。在开始之前，请确保你已安装 Anchor 版本 0.30.1 或更高版本。你可以通过运行以下命令检查版本：\r\n\r\n```\r\nanchor --version\r\n```\r\n如果你还没有，可以按照 [这里](https://www.anchor-lang.com/docs/installation) 的安装说明进行操作。\r\n\r\n### 创建新项目\r\n\r\n请在终端中创建一个新项目。从项目父目录中运行以下命令：\r\n\r\n\r\n```bash\r\nanchor init bankrun-test\r\n```\r\n\r\n然后切换到新目录：\r\n```bash\r\ncd bankrun-test\r\n```\r\n\r\n项目创建后，请构建程序以确保一切正常：\r\n```bash\r\nanchor build\r\n```\r\n\r\n初始构建在几分钟后应成功完成。如果你看到错误，请按照错误消息中的说明进行修复。\r\n\r\n### 安装依赖项\r\n\r\n我们需要一些依赖项以使项目正常工作。让我们来安装它们：\r\n\r\n首先，安装我们的 Node.js 依赖项：\r\n\r\n```\r\n    yarn add solana-bankrun anchor-bankrun @solana/spl-token\r\n```\r\n这将允许我们在测试中使用 Bankrun 以及 Solana 代币程序。\r\n\r\n接下来，让我们将 SPL 代币程序添加到我们的 Anchor 程序。导航到 `programs/bankrun-test/Cargo.toml` 并将 `anchor-spl` 添加到依赖项：\r\n\r\n```\r\n    [dependencies]\r\n    anchor-lang = \"0.30.1\"\r\n    anchor-spl = \"0.30.1\"\r\n```\r\n由于我们使用的是 Anchor 0.30 以上版本，我们还需要更新 `programs/bankrun-test/Cargo.toml` 中的 `idl-build` 功能，以包含 `anchor-spl` 依赖项：\r\n```\r\n    idl-build = [\"anchor-lang/idl-build\", \"anchor-spl/idl-build\"]\r\n```\r\n\r\n太好了！我们现在准备好开始构建我们的程序。\r\n\r\n### 创建 Anchor 程序\r\n\r\n由于本指南的重点是使用 Bankrun 进行测试，我们不会花太多时间在 Anchor 程序本身上。相反，我们将重点放在编写 Bankrun 测试上。导航到 `programs/bankrun-test/src/lib.rs` 并将内容替换为以下内容（*确保将程序 ID 替换为你自己的*）：\r\n\r\n```\r\nuse anchor_lang::prelude::*;\r\nuse anchor_spl::token::TokenAccount;\r\nuse std::str::FromStr;\r\n\r\ndeclare_id!(\"11111111111111111111111111111111\"); // REPLACE WITH YOUR PROGRAM ID\r\n\r\nconst MINIMUM_SLOT: u64 = 100;\r\nconst TOKEN_MINIMUM_BALANCE: u64 = 100_000_000_000;\r\nconst USDC_MINT: &str = \"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\";\r\n\r\n#[program]\r\npub mod bankrun_test {\r\n    use super::*;\r\n\r\n    pub fn set_data(ctx: Context<SetData>) -> Result<()> {\r\n        let current_slot = Clock::get()?.slot;\r\n        msg!(\"Current slot: {}\", current_slot);\r\n        require_gte!(current_slot, MINIMUM_SLOT, BankrunError::InvalidSlot);\r\n\r\n        ctx.accounts.data_account.new_data = ctx.accounts.new_data.key();\r\n        ctx.accounts.data_account.last_updated_slot = current_slot;\r\n        msg!(\"Set new data: {}\", ctx.accounts.new_data.key());\r\n\r\n        Ok(())\r\n    }\r\n\r\n    pub fn check_spl_token(ctx: Context<CheckSplToken>) -> Result<()> {\r\n        let usdc_mint = Pubkey::from_str(USDC_MINT).unwrap();\r\n        \r\n        let token_account = &ctx.accounts.token_account;\r\n        let token_balance = token_account.amount;\r\n\r\n        msg!(\"Token account: {} has a balance of {}\", token_account.key(), token_balance);\r\n        require_keys_eq!(token_account.mint, usdc_mint, BankrunError::InvalidTokenMint);\r\n        require_gte!(token_balance, TOKEN_MINIMUM_BALANCE, BankrunError::InsufficientTokenBalance);\r\n\r\n        Ok(())\r\n    }\r\n}\r\n\r\n#[derive(Accounts)]\r\npub struct SetData<'info> {\r\n    #[account(mut)]\r\n    pub payer: Signer<'info>,\r\n\r\n    #[account(\r\n        init,\r\n        payer = payer,\r\n        space = 8 + DataAccount::INIT_SPACE\r\n    )]\r\n    pub data_account: Account<'info, DataAccount>,\r\n\r\n    pub new_data: Signer<'info>,\r\n\r\n    pub system_program: Program<'info, System>,\r\n}\r\n\r\n#[derive(Accounts)]\r\npub struct CheckSplToken<'info> {\r\n    pub token_account: Account<'info, TokenAccount>,\r\n}\r\n\r\n#[account]\r\n#[derive(InitSpace)]\r\npub struct DataAccount {\r\n    pub last_updated_slot: u64,\r\n    pub new_data: Pubkey\r\n}\r\n\r\n#[error_code]\r\npub enum BankrunError {\r\n    // Error code: 6000\r\n    #[msg(\"Invalid slot\")]\r\n    InvalidSlot,\r\n    // Error code: 6001\r\n    #[msg(\"Insufficient token balance\")]\r\n    InsufficientTokenBalance,\r\n    // Error code: 6002\r\n    #[msg(\"Invalid token mint\")]\r\n    InvalidTokenMint,\r\n}\r\n```\r\n\r\n你可以运行 `anchor keys sync` 来更新你的 `declare_id!` 宏，以匹配你程序的程序 ID。\r\n\r\n让我们来看一下我们程序的每个指令：\r\n\r\n*   `set_data`：该指令将 `DataAccount` 的 `last_updated_slot` 字段设置为当前槽，并将 `new_data` 字段设置为签署交易的 `new_data` 账户的公钥。该指令检查当前槽是否大于或等于 `MINIMUM_SLOT` 常量。如果当前槽小于 `MINIMUM_SLOT` 常量，则该指令将失败并显示错误代码 `InvalidSlot`。\r\n*   `check_spl_token`：该指令检查 `token_account` 的余额是否大于或等于 `TOKEN_MINIMUM_BALANCE` 常量。它还检查 `token_account` 是否与 `USDC_MINT` 常量相关联。如果这些条件之一不满足，则该指令将失败并显示错误代码 `InsufficientTokenBalance` 或 `InvalidTokenMint`。\r\n\r\n这应该足以帮助我们演示 Bankrun 的“时间旅行”能力，并向账户写入任意数据。\r\n\r\n继续运行 `anchor build` 来构建程序，并确保它成功编译。你不应该看到任何错误，但如果你看到，请按照终端中的说明进行修复。\r\n\r\n### 编写测试 \r\n\r\n好的！让我们开始编写测试。导航到 `tests/bankrun-test.ts` 并删除现有内容。\r\n\r\n#### 导入依赖 \r\n\r\n让我们开始导入必要的依赖。在文件顶部添加以下内容：\r\n\r\n```\r\nimport { setProvider, Program } from \"@coral-xyz/anchor\";\r\nimport { BankrunTest } from \"../target/types/bankrun_test\";\r\nimport {\r\n  AccountInfoBytes,\r\n  AddedAccount,\r\n  BanksClient,\r\n  BanksTransactionResultWithMeta,\r\n  ProgramTestContext,\r\n  startAnchor\r\n} from \"solana-bankrun\";\r\nimport { BankrunProvider } from \"anchor-bankrun\";\r\nimport { expect } from \"chai\";\r\nimport {\r\n  PublicKey,\r\n  Transaction,\r\n  Keypair,\r\n  Connection,\r\n  clusterApiUrl,\r\n  TransactionInstruction\r\n} from \"@solana/web3.js\";\r\nimport {\r\n  ACCOUNT_SIZE,\r\n  AccountLayout,\r\n  getAssociatedTokenAddressSync,\r\n  MintLayout,\r\n  TOKEN_PROGRAM_ID\r\n} from \"@solana/spl-token\";\r\n\r\nconst IDL = require(\"../target/idl/bankrun_test.json\");\r\n```\r\n\r\n这些内容中的大部分应该看起来很熟悉，但我们从 `anchor-bankrun` 和 `solana-bankrun` 中导入了一些新的依赖。我们将在设置函数中讨论这些依赖。\r\n\r\n#### 定义常量 \r\n\r\n让我们创建几个常量来帮助我们进行测试。在导入下方添加以下内容：\r\n\r\n```\r\n// Constants\r\nconst PROJECT_DIRECTORY = \"\"; // Leave empty if using default anchor project\r\nconst USDC_DECIMALS = 6;\r\nconst USDC_MINT_ADDRESS = \"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\";\r\nconst MINIMUM_SLOT = 100;\r\nconst MINIMUM_USDC_BALANCE = 100_000_000_000; // 100k USDC\r\n```\r\n\r\n*   `PROJECT_DIRECTORY`: 这是你的 Anchor 项目所在的目录（与 `Anchor.toml` 在同一目录）。如果你使用默认项目，可以将其留为空字符串。\r\n*   `USDC_DECIMALS`: 这是 USDC 代币的小数位数。\r\n*   `USDC_MINT_ADDRESS`: 这是 USDC 代币铸币的地址。\r\n*   `MINIMUM_SLOT`: 这是我们在 Anchor 程序中定义的相同最小Slot。\r\n*   `MINIMUM_USDC_BALANCE`: 这是我们在 Anchor 程序中定义的相同的 USDC 最低余额。\r\n\r\n#### 设置函数[​](#setup-functions \"直接链接到设置函数\")\r\n\r\n接下来，让我们创建一些设置函数来帮助我们进行测试。在常量下方添加以下内容：\r\n\r\n```\r\nasync function createAndProcessTransaction(\r\n  client: BanksClient,\r\n  payer: Keypair,\r\n  instruction: TransactionInstruction,\r\n  additionalSigners: Keypair[] = []\r\n): Promise<BanksTransactionResultWithMeta> {\r\n  const tx = new Transaction();\r\n  const [latestBlockhash] = await client.getLatestBlockhash();\r\n  tx.recentBlockhash = latestBlockhash;\r\n  tx.add(instruction);\r\n  tx.feePayer = payer.publicKey;\r\n  tx.sign(payer, ...additionalSigners);\r\n  return await client.tryProcessTransaction(tx);\r\n}\r\n\r\nasync function setupATA(\r\n  context: ProgramTestContext,\r\n  usdcMint: PublicKey,\r\n  owner: PublicKey,\r\n  amount: number\r\n): Promise<PublicKey> {\r\n  const tokenAccData = Buffer.alloc(ACCOUNT_SIZE);\r\n  AccountLayout.encode(\r\n    {\r\n      mint: usdcMint,\r\n      owner,\r\n      amount: BigInt(amount),\r\n      delegateOption: 0,\r\n      delegate: PublicKey.default,\r\n      delegatedAmount: BigInt(0),\r\n      state: 1,\r\n      isNativeOption: 0,\r\n      isNative: BigInt(0),\r\n      closeAuthorityOption: 0,\r\n      closeAuthority: PublicKey.default,\r\n    },\r\n    tokenAccData,\r\n  );\r\n\r\n  const ata = getAssociatedTokenAddressSync(usdcMint, owner, true);\r\n  const ataAccountInfo = {\r\n    lamports: 1_000_000_000,\r\n    data: tokenAccData,\r\n    owner: TOKEN_PROGRAM_ID,\r\n    executable: false,\r\n  };\r\n\r\n  context.setAccount(ata, ataAccountInfo);\r\n  return ata;\r\n}\r\n```\r\n\r\n让我们看看这些函数的每一个：\r\n\r\n*   `createAndProcessTransaction` 接受一些基本交易信息和一个 `BanksClient`，这是一个从任意验证者的角度表示账本状态的客户端（当我们使用 `startAnchor` 功能初始化测试时将创建它）。然后它创建一个新交易，将提供的指令添加到其中，用提供的付款人进行签名，并使用 `BanksClient` 的 `tryProcessTransaction` 函数处理它。这个函数对于测试很有用，因为它返回 `BanksTransactionResultWithMeta`，其中包含交易日志、返回数据、使用的计算单位以及（如果适用）错误。\r\n*   `setupATA` 接受一个 `ProgramTestContext`，它实际上是 `BanksClient` 的扩展，包含一些附加功能，包括 `setAccount` 函数，允许我们在上下文中设置一个账户。该函数将提供的账户数据编码到缓冲区，创建一个与提供的拥有者和铸币相对应的关联代币账户（ATA），并将 ATA 的数据设置为编码后的缓冲区。然后它将返回 ATA 的公钥。\r\n\r\n### 构建我们的测试环境\r\n\r\n现在我们有了设置函数，可以开始编写测试。在支持函数下方描述测试套件：\r\n\r\n```\r\ndescribe(\"Bankrun Tests\", () => {\r\n  const usdcMint = new PublicKey(USDC_MINT_ADDRESS);\r\n  let context: ProgramTestContext;\r\n  let client: BanksClient;\r\n  let payer: Keypair;\r\n  let provider: BankrunProvider;\r\n  let program: Program<BankrunTest>;\r\n\r\n  before(async () => {\r\n    const connection = new Connection(clusterApiUrl(\"mainnet-beta\"));\r\n    const accountInfo = await connection.getAccountInfo(usdcMint);\r\n    const usdcAccount: AddedAccount = { address: usdcMint, info: accountInfo };\r\n\r\n    context = await startAnchor(PROJECT_DIRECTORY, [], [usdcAccount]);\r\n    client = context.banksClient;\r\n    payer = context.payer;\r\n    provider = new BankrunProvider(context);\r\n    setProvider(provider);\r\n    program = new Program<BankrunTest>(IDL, provider);\r\n  });\r\n\r\n  // TODO: Add Time Travel Tests Here\r\n\r\n  // TODO: Add Arbitrary Data Account Tests Here\r\n\r\n});\r\n```\r\n\r\n在这里我们定义了一些将在测试中全局使用的变量：\r\n\r\n*   `usdcMint`: 这是 USDC 代币铸币的公钥。\r\n*   `context`: 这是 `ProgramTestContext` 的一个实例，绕过 `BanksClient` 并包含附加功能。这个实例是通过 `startAnchor` 函数初始化的。请注意，我们传入了 `usdcAccount`。这将初始化我们的测试环境，并初始化 USDC 铸造账户（我们稍后会编写测试来验证这一点）。\r\n*   `client`: 这是将用于与账本状态互动的 `BanksClient`。\r\n*   `payer`: 这是将用于签署交易的付款人。\r\n*   `provider`: 这是 `BankrunProvider` 的一个实例（实现了 Anchor 的 `Provider`），它将包含附加的上下文和功能。\r\n*   `program`: 这是 Anchor 的 `Program<BankrunTest>` 的一个实例，将像任何其他 Anchor 测试套件一样使用。\r\n\r\n### 时间旅行测试\r\n\r\n让我们创建一些测试，帮助我们验证程序的功能。由于我们有一个指令，如果当前Slot小于 `MINIMUM_SLOT` 则会失败，所以在传统测试环境中这个测试可能会很麻烦。Bankrun 允许我们根据需要进行“时间旅行”，这可以通过两种方式实现：\r\n\r\n1. 使用 `provider.context.warpToSlot` 函数（或对时间段使用 `warpToEpoch`），或\r\n2. 使用 `context.setClock` 函数。\r\n\r\n**在你的 \"Bankrun Tests\" 描述块内，** 在 \"before\" 块 *之后，添加以下 \"Time Travel Tests\" 描述块：\r\n\r\n```javascript\r\ndescribe(\"Time Travel Tests\", () => {\r\n    const testCases = [\r\n        { desc: \"(too early)\", slot: MINIMUM_SLOT - 1, shouldSucceed: false },\r\n        { desc: \"(at or above threshold)\", slot: MINIMUM_SLOT, shouldSucceed: true },\r\n    ]\r\n    testCases.forEach(({ desc, slot, shouldSucceed }) => {\r\n        describe(`When slot is ${slot} ${desc}`, () => {\r\n            let txResult: BanksTransactionResultWithMeta;\r\n            let newData: Keypair;\r\n            let dataAccount: Keypair;\r\n            before(async () => {\r\n                provider.context.warpToSlot(BigInt(slot));\r\n                newData = Keypair.generate();\r\n                dataAccount = Keypair.generate();\r\n                const ix = await program.methods\r\n                    .setData()\r\n                    .accounts({\r\n                        payer: payer.publicKey,\r\n                        newData: newData.publicKey,\r\n                        dataAccount: dataAccount.publicKey,\r\n                    })\r\n                    .signers([newData, dataAccount])\r\n                    .instruction();\r\n                txResult = await createAndProcessTransaction(client, payer, ix, [newData, dataAccount]);\r\n            });\r\n            if (!shouldSucceed) {\r\n                it(\"transaction should fail\", () => {\r\n                    expect(txResult.result).to.exist;\r\n                });\r\n                it(\"should contain specific error details in log\", () => {\r\n                    const errorLog = txResult.meta.logMessages.find(log =>\r\n                        log.includes('AnchorError') &&\r\n                        log.includes('InvalidSlot') &&\r\n                        log.includes('6000') &&\r\n                        log.includes('Error Message: Invalid slot')\r\n                    );\r\n                    expect(errorLog).to.exist;\r\n                });\r\n                it(\"last log message should indicate failure\", () => {\r\n                    expect(txResult.meta.logMessages[txResult.meta.logMessages.length - 1]).to.include('failed');\r\n                });\r\n            } else {\r\n                it(\"transaction should succeed\", () => {\r\n                    expect(txResult.result).to.be.null;\r\n                });\r\n                it(\"last log message should indicate success\", () => {\r\n                    expect(txResult.meta.logMessages[txResult.meta.logMessages.length - 1]).to.include('success');\r\n                });\r\n                it(\"should contain expected log message\", () => {\r\n                    const expectedLog = \"Set new data: \" + newData.publicKey.toString();\r\n                    const foundLog = txResult.meta.logMessages.some(log => log.includes(expectedLog));\r\n                    expect(foundLog).to.be.true;\r\n                });\r\n                it(\"should set new data correctly\", async () => {\r\n                    const onChainData = await program.account.dataAccount.fetch(dataAccount.publicKey);\r\n                    expect(onChainData.newData.toString()).to.equal(newData.publicKey.toString());\r\n                });\r\n            }\r\n        });\r\n    });\r\n});\r\n```\r\n\r\n让我们来分析一下测试：\r\n\r\n* 首先，我们定义了一个测试用例数组。每个测试用例都是一个对象，包含描述、槽号和一个布尔值，指示测试应该成功还是失败。在这种情况下，我们测试一个当前槽少于 `MINIMUM_SLOT` 常量的情况（预期失败），以及一个当前槽大于或等于 `MINIMUM_SLOT` 常量的情况（预期成功）。对于每个测试用例，我们创建一个 `describe` 块来描述测试用例。在 `describe` 块中，我们创建一个 `before` 块来设置测试环境。在创建和发送交易之前，我们使用 `provider.context.warpToSlot` 函数“时光旅行”到指定的槽。然后，根据指令是否期望成功或失败，我们在 `describe` 块内运行一系列测试。对于每个测试，我们创建一个 `it` 块来描述测试用例。在 `it` 块内，我们检查交易结果并对期望的行为进行断言。\r\n* `createAndProcessTransaction` 函数只有在交易过程中发生错误时返回 `result` 属性，因此我们可以使用 `expect` 函数来检查结果是否为 null。此外，`createAndProcessTransaction` 函数将我们程序的日志消息作为 `meta.logMessages` 属性返回。我们创建了一些辅助函数来查找预期的 _AnchorError_ 日志或预期的 _success_ 或 _failed_ 日志消息。\r\n* 最后，我们使用 Anchor `fetch` 方法从 `dataAccount` 账户中获取 `newData` 字段，并断言它与预期值匹配。\r\n\r\n现在我们应该能够运行测试。在终端中输入：\r\n```\r\nanchor test\r\n```\r\n\r\n你的测试应通过，但你应该注意到终端中包含大量调试信息：\r\n\r\n```\r\nWhen slot is 100 (at or above threshold)\r\n[2024-07-17T21:57:31.175139000Z DEBUG solana_runtime::message_processor::stable_log] Program 4uqt4ZDm7WhG3BfQASEADZNi5TUyE21zwXwbqUb1Qjmk invoke [1]\r\n[2024-07-17T21:57:31.175203000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Instruction: SetData\r\n[2024-07-17T21:57:31.175246000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [2]\r\n[2024-07-17T21:57:31.175254000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success\r\n[2024-07-17T21:57:31.175290000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Current slot: 100\r\n[2024-07-17T21:57:31.175372000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Set new data: 5go3aphd2yJPqRpQEN8Pg4Asvp2xkpNLJxXc6HErm4a5\r\n[2024-07-17T21:57:31.175383000Z DEBUG solana_runtime::message_processor::stable_log] Program 4uqt4ZDm7WhG3BfQASEADZNi5TUyE21zwXwbqUb1Qjmk consumed 18600 of 200000 compute units\r\n[2024-07-17T21:57:31.175391000Z DEBUG solana_runtime::message_processor::stable_log] Program 4uqt4ZDm7WhG3BfQASEADZNi5TUyE21zwXwbqUb1Qjmk success\r\n        ✔ transaction should succeed\r\n        ✔ last log message should indicate success\r\n        ✔ should contain expected log message\r\n        ✔ should set new data correctly\r\n```\r\n\r\n这又是使用 Bankrun 的一个好处，因为它允许我们更细致地调试测试。尽管这不是什么大问题，因为我们的测试是通过的，但在快速调试程序时，这可以非常有用！你应该能够浏览日志，准确查看你的程序如何以及在哪里记录错误或成功消息（正如我们在测试中识别的那样）。非常酷，对吧？\r\n\r\n### 任意数据账户测试\r\n\r\n现在让我们看一些测试，以帮助我们探索 BankRun 的任意数据账户功能。我们将测试 `check_spl_token` 指令，该指令将检查代币账户的余额是否大于或等于 `TOKEN_MINIMUM_BALANCE` 常量。如果你还记得，我们的 Anchor 程序要求代币铸造必须是 USDC 铸造。运行此测试通常需要使用一个假的 USDC 代币，但 Bankrun 允许我们使用任意账户，这意味着我们可以将代币账户信息写入任何我们想要的账户。事实上，我们已经在我们的 `startAnchor` 函数中做到这一点——通过使用我们从 Solana 主网提取的账户信息来初始化 `usdcMint` 账户。让我们（1）编写一个测试以确保 USDC 铸造正确初始化，并（2）编写一组测试以验证具有足够余额的 ATA 是否已初始化。**在你的 \"Bankrun Tests\" 描述块内，** 在 \"Time Travel Tests\" 块后，添加以下 \"Arbitrary Data Account Tests\" 描述块：\r\n\r\n```javascript\r\ndescribe(\"任意数据账户测试\", () => {\r\n  const testCases = [\r\n    { desc: \"余额不足\", amount: MINIMUM_USDC_BALANCE - 1_000_000, shouldSucceed: false },\r\n    { desc: \"余额充足\", amount: MINIMUM_USDC_BALANCE, shouldSucceed: true },\r\n  ];\r\n  describe(\"USDC 铸造初始化\", () => {\r\n    let rawAccount: AccountInfoBytes;\r\n    before(async () => {\r\n      rawAccount = await client.getAccount(usdcMint);\r\n    });\r\n    it(\"应当已经初始化 USDC 铸造\", () => {\r\n      expect(rawAccount).to.exist;\r\n    });\r\n    it(\"应当有正确的小数位数\", () => {\r\n      const mintInfo = MintLayout.decode(rawAccount.data);\r\n      expect(mintInfo.decimals).to.equal(USDC_DECIMALS);\r\n    });\r\n  });\r\n  testCases.forEach(({ desc, amount, shouldSucceed }) => {\r\n    describe(`拥有 ${desc} USDC 余额的 ATA`, () => {\r\n      let ata: PublicKey;\r\n      let txResult: BanksTransactionResultWithMeta;\r\n      before(async () => {\r\n        let owner = Keypair.generate();\r\n        ata = await setupATA(context, usdcMint, owner.publicKey, amount);\r\n        const ix = await program.methods\r\n          .checkSplToken()\r\n          .accounts({\r\n            tokenAccount: ata,\r\n          })\r\n          .instruction();\r\n        txResult = await createAndProcessTransaction(client, payer, ix);\r\n      });\r\n      it(\"应当已经初始化 USDC ATA\", async () => {\r\n        const rawAccount = await client.getAccount(ata);\r\n        expect(rawAccount).to.exist;\r\n      });\r\n      it(\"应当在 ATA 中有正确的余额\", async () => {\r\n        const accountInfo = await client.getAccount(ata);\r\n        const tokenAccountInfo = AccountLayout.decode(accountInfo.data);\r\n        expect(tokenAccountInfo.amount).to.equal(BigInt(amount));\r\n      });\r\n      if (shouldSucceed) {\r\n        it(\"应当成功处理交易\", () => {\r\n          expect(txResult.result).to.be.null;\r\n        });\r\n        it(\"最后的日志消息应指示成功\", () => {\r\n          expect(txResult.meta.logMessages[txResult.meta.logMessages.length - 1]).to.include('success');\r\n        });\r\n      } else {\r\n        it(\"应当无法处理交易\", () => {\r\n          expect(txResult.result).to.exist;\r\n        });\r\n        it(\"应在日志中包含特定错误细节\", () => {\r\n          const errorLog = txResult.meta.logMessages.find(log =>\r\n            log.includes('AnchorError') &&\r\n            log.includes('InsufficientTokenBalance') &&\r\n            log.includes('6001') &&\r\n            log.includes('错误消息: 代币余额不足。')\r\n          );\r\n          expect(errorLog).to.exist;\r\n        });\r\n        it(\"最后的日志消息应指示失败\", () => {\r\n          expect(txResult.meta.logMessages[txResult.meta.logMessages.length - 1]).to.include('failed');\r\n        });\r\n      }\r\n    });\r\n  });\r\n});\r\n```\r\n\r\n测试分析:\r\n\r\n* 首先，如前所述，我们定义一个测试用例数组。每个测试用例是一个对象，包含描述、USDC 余额金额和一个布尔值，指示测试是应该成功还是失败。在这种情况下，我们正在测试一个 ATA 余额不足的情况（预期失败）以及一个 ATA 余额充足的情况（预期成功）。\r\n* 接下来，我们为 USDC 铸造初始化创建一个测试。我们使用 `BanksClient` 的 `getAccount` 方法获取 USDC 铸造的原始账户信息。然后我们使用 `expect` 函数检查账户是否存在，以及小数位数是否与预期值匹配。对于每个测试用例，我们创建一个描述测试用例的 `describe` 块。在 `describe` 块内部，我们创建一个设置测试环境的 `before` 块。在创建和发送交易之前，我们使用 `setupATA` 函数创建一个具有指定余额的 ATA。然后我们使用 `createAndProcessTransaction` 函数将交易发送到账本状态。\r\n* 然后我们根据指令是否预期成功或失败，在 `describe` 块内部运行一系列测试。对于每个测试，我们创建一个描述测试用例的 `it` 块。在 `it` 块内部，我们检查交易结果，并对预期行为进行断言。这些测试在结构上与之前编写的测试相似，利用 `BanksTransactionResultWithMeta` 对象的 `results` 和 `meta.logMessages` 属性。\r\n\r\n### 运行测试\r\n\r\n在你的终端中输入：\r\n```\r\nanchor test\r\n```\r\n\r\n你的测试应通过：\r\n```\r\n18 passing (2s)\r\n✨  Done in 3.02s.\r\n```\r\n\r\n哇！真快！尽管向集群发送了几笔交易，但我们的测试在几分之一秒内完成。这是使用 Bankrun 的一个巨大优势，因为它使我们能以更高效的方式运行测试！但是那些日志呢？不用担心——如果你不想看到它们，可以轻松删除它们。\r\n\r\n打开 `Anchor.toml` 并将 `[scripts]` 部分更新为这样的格式：\r\n```\r\n[scripts]\r\ntest = \"RUST_LOG= yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts\"\r\ntest_debug = \"yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts\"\r\n```\r\n\r\n现在，当你运行 `anchor test` 时，应该不会看到日志：\r\n\r\n```\r\n  Bankrun Tests\r\n    Time Travel Tests\r\n      When slot is 99 (too early)\r\n        ✔ transaction should fail\r\n        ✔ should contain specific error details in log\r\n        ✔ last log message should indicate failure\r\n      When slot is 100 (at or above threshold)\r\n        ✔ transaction should succeed\r\n        ✔ last log message should indicate success\r\n        ✔ should contain expected log message\r\n        ✔ should set new data correctly\r\n    Arbitrary Data Account Tests\r\n      USDC mint initialization\r\n        ✔ should have initialized USDC mint\r\n        ✔ should have correct decimals\r\n      ATA with insufficient USDC balance\r\n        ✔ should have initialized USDC ATA\r\n        ✔ should have correct balance in ATA\r\n        ✔ should fail to process the transaction\r\n        ✔ should contain specific error details in log\r\n        ✔ last log message should indicate failure\r\n      ATA with sufficient USDC balance\r\n        ✔ should have initialized USDC ATA\r\n        ✔ should have correct balance in ATA\r\n        ✔ should process the transaction successfully\r\n        ✔ last log message should indicate success\r\n\r\n\r\n  18 passing (442ms)\r\n\r\n✨  Done in 1.19s.\r\n```\r\n\r\n这太棒了！但是我们还添加了一个 `test_debug` 脚本，如果我们需要调试某些内容，它将显示日志。让我们重新运行测试，这次将使用 `test_debug` 脚本：\r\n\r\n就这样！你的日志被显示出来。这是调试测试并确保它们按预期工作的好方法。\r\n\r\n## 总结\r\n\r\n干得好！你现在在工具箱中拥有一些额外的工具，以加速 Solana 程序的测试和开发。\r\n\r\n### 资源\r\n\r\n*   [Solana Bankrun 文档](https://kevinheavey.github.io/solana-bankrun/api)\r\n*   [Solana Bankrun GitHub](https://github.com/kevinheavey/solana-bankrun)\r\n*   [Anchor Bankrun GitHub](https://github.com/kevinheavey/anchor-bankrun)\r\n*   [Solana Bankrun 教程](https://kevinheavey.github.io/solana-bankrun/tutorial)\r\n*   [Solana Bankrun 解释线程 (x.com)](https://x.com/dj_d_sol/status/1690690600121536513)\r\n\r\n  \r\n\r\n> 我是 [AI 翻译官](https://learnblockchain.cn/people/19584)，为大家转译优秀英文文章，如有翻译不通的地方，在[这里](https://github.com/lbc-team/Pioneer/blob/master/translations/10616.md)修改，还请包涵～"},"author":{"user":"https://learnblockchain.cn/people/25306","address":null},"history":"bafkreiebrpabmy36lapemqmmjs67adfpyemc7v6lzpuin5h4co4tkxfynm","timestamp":1738979270,"version":1}