🧪 测试 Solana 程序 - 让Bug无处藏身!
🎯 欢迎来到测试的世界!
嘿,Solana开发者们!👋 还记得那些让你半夜惊醒的Bug吗?今天我们要学习如何在Bug出现之前就消灭它们!
测试就像是你代码的健身教练💪 —— 它让你的程序更强壮、更可靠!让我们一起构建一个铜墙铁壁的测试体系!
🎯 今日任务: 掌握单元测试和集成测试,成为Bug终结者!
🏗️ 测试架构总览
🎨 测试金字塔
🏔️ 测试金字塔
═══════════════
╱ ╲
╱ E2E ╲ ← 🌐 端到端测试(最少但最重要)
╱ 测试 ╲
╱───────────╲
╱ 集成测试 ╲ ← 🔗 集成测试(中等数量)
╱ ╲
╱─────────────────╲
╱ 单元测试 ╲ ← 🧩 单元测试(最多最快)
╱___________________╲
快速 ←─────────────→ 慢速
便宜 ←─────────────→ 昂贵
隔离 ←─────────────→ 真实
📊 两种测试类型对比
┌──────────────┬─────────────────┬──────────────────┐
│ 特性 │ 单元测试 🧩 │ 集成测试 🔗 │
├──────────────┼─────────────────┼──────────────────┤
│ 测试范围 │ 单个函数 │ 多个模块 │
│ 速度 │ 极快 ⚡ │ 较慢 🐢 │
│ 隔离性 │ 完全隔离 │ 部分隔离 │
│ 复杂度 │ 简单 📗 │ 复杂 📕 │
│ 反馈速度 │ 即时 ⏱️ │ 延迟 ⏳ │
└──────────────┴─────────────────┴──────────────────┘
🧩 单元测试 - 代码的体检
🎯 什么是单元测试?
单元测试就像给你的每个函数做体检🏥!它们:
- 🔍 检查单个功能 - 一次只测试一个函数
- ⚡ 运行超快 - 毫秒级响应
- 🎯 精准定位 - 立即找到问题所在
- 🔒 测试私有接口 - 能访问内部实现
📝 创建你的第一个单元测试
// 🏠 在 processor.rs 文件底部添加测试模块
#[cfg(test)] // 🎯 只在测试时编译这段代码
mod tests {
use super::*; // 导入父模块的所有内容
// 🧪 测试属性 - 标记这是一个测试函数
#[test]
fn test_basic_math() {
// 📝 准备测试数据
let a = 2;
let b = 2;
// 🎯 执行被测试的操作
let result = a + b;
// ✅ 断言结果
assert_eq!(result, 4, "2 + 2 应该等于 4!");
// 🎉 如果到这里没报错,测试通过!
}
// 🔧 辅助函数(不是测试)
fn helper_setup() -> TestAccount {
// 准备测试环境的辅助函数
TestAccount::new()
}
// 😱 测试预期的panic
#[test]
#[should_panic(expected = "除零错误")]
fn test_division_by_zero() {
let result = divide(10, 0); // 这应该panic!
}
}
💡 Pro Tip:
#[cfg(test)]
确保测试代码不会被包含在最终的程序中,节省空间!
🚀 编写Solana程序单元测试
🛠️ 设置测试环境
// 🧪 完整的Solana程序单元测试示例
#[cfg(test)]
mod tests {
use {
super::*,
assert_matches::*, // 🎯 强大的断言匹配库
solana_program::instruction::{AccountMeta, Instruction},
solana_program_test::*, // 🏗️ Solana测试框架
solana_sdk::{
signature::Signer,
transaction::Transaction,
signer::keypair::Keypair
},
};
// 🌟 使用tokio进行异步测试
#[tokio::test]
async fn test_initialize_movie_review() {
// 🎭 Step 1: 设置测试环境
println!("🚀 开始测试初始化电影评论...");
// 创建唯一的程序ID
let program_id = Pubkey::new_unique();
// 🏗️ Step 2: 初始化测试环境
// ProgramTest创建一个模拟的Solana运行时
let (mut banks_client, payer, recent_blockhash) = ProgramTest::new(
"movie_review_program", // 程序名称
program_id, // 程序ID
processor!(process_instruction), // 处理器函数
)
.start() // 启动测试环境
.await;
println!("✅ 测试环境准备完成!");
// 👤 Step 3: 创建测试账户
let reviewer = Keypair::new();
let pda_account = Keypair::new();
// 📝 Step 4: 准备指令数据
let title = "Inception";
let rating = 5u8;
let description = "Mind-bending masterpiece!";
// 序列化指令数据
let mut instruction_data = vec![0]; // 0 = AddMovieReview指令
instruction_data.extend_from_slice(title.as_bytes());
instruction_data.push(rating);
instruction_data.extend_from_slice(description.as_bytes());
// 🔨 Step 5: 创建交易
let mut transaction = Transaction::new_with_payer(
&[Instruction {
program_id,
accounts: vec![
AccountMeta::new(payer.pubkey(), true), // 付款人
AccountMeta::new(pda_account.pubkey(), false), // PDA账户
AccountMeta::new_readonly(system_program::id(), false), // 系统程序
],
data: instruction_data,
}],
Some(&payer.pubkey()), // 设置付款人
);
// ✍️ Step 6: 签名交易
transaction.sign(&[&payer], recent_blockhash);
// 🚀 Step 7: 发送并验证交易
let result = banks_client.process_transaction(transaction).await;
// ✅ Step 8: 断言结果
assert_matches!(result, Ok(()), "交易应该成功!");
println!("🎉 测试通过!电影评论创建成功!");
}
// 🧪 测试错误场景
#[tokio::test]
async fn test_invalid_rating() {
let program_id = Pubkey::new_unique();
// ... 设置代码 ...
// 🚫 使用无效评分(大于5)
let invalid_rating = 10u8;
// ... 创建交易 ...
// 期望交易失败
let result = banks_client.process_transaction(transaction).await;
assert!(result.is_err(), "无效评分应该导致错误!");
println!("✅ 正确拒绝了无效评分!");
}
}
🎯 运行单元测试
# 🧪 运行所有测试
cargo test-sbf
# 🎯 运行特定测试
cargo test-sbf test_initialize_movie_review
# 📝 显示打印输出
cargo test-sbf -- --nocapture
# ⚡ 并行运行测试
cargo test-sbf -- --test-threads=4
🔗 集成测试 - 系统的全面体检
🏗️ 设置集成测试
集成测试确保你的程序各个部分能够协同工作!
📁 创建测试目录结构
📦 your-project/
├── 📂 src/
│ └── 📜 lib.rs
├── 📂 tests/ # 🆕 集成测试目录
│ ├── 📜 integration_test.rs
│ └── 📜 e2e_test.rs
└── 📜 Cargo.toml
📝 编写集成测试
// 🧪 tests/integration_test.rs
use movie_review_program; // 导入你的程序库
use solana_program_test::*;
use solana_sdk::{
pubkey::Pubkey,
signature::{Keypair, Signer},
transaction::Transaction,
};
// 🌟 集成测试 - 测试完整的工作流程
#[tokio::test]
async fn test_complete_review_workflow() {
println!("🎬 开始完整工作流程测试...");
// 🏗️ Step 1: 设置测试环境
let program_id = Pubkey::new_unique();
let mut program_test = ProgramTest::new(
"movie_review_program",
program_id,
processor!(movie_review_program::process_instruction),
);
// 💰 添加一些测试账户
program_test.add_account(
test_account_pubkey,
Account {
lamports: 1_000_000_000, // 1 SOL
..Account::default()
},
);
let (mut banks_client, payer, recent_blockhash) = program_test.start().await;
// 📝 Step 2: 创建电影评论
println!("📝 创建电影评论...");
let review_result = create_movie_review(
&mut banks_client,
&payer,
"The Matrix",
5,
"Revolutionary sci-fi!",
).await;
assert!(review_result.is_ok(), "创建评论失败!");
// 💬 Step 3: 添加评论
println!("💬 添加评论...");
let comment_result = add_comment(
&mut banks_client,
&payer,
review_pda,
"Totally agree! Best movie ever!",
).await;
assert!(comment_result.is_ok(), "添加评论失败!");
// ✏️ Step 4: 更新评论
println!("✏️ 更新评论...");
let update_result = update_movie_review(
&mut banks_client,
&payer,
"The Matrix",
5,
"Revolutionary sci-fi masterpiece!",
).await;
assert!(update_result.is_ok(), "更新评论失败!");
println!("🎉 完整工作流程测试通过!");
}
// 🔧 辅助函数
async fn create_movie_review(
banks_client: &mut BanksClient,
payer: &Keypair,
title: &str,
rating: u8,
description: &str,
) -> Result<(), Box<dyn std::error::Error>> {
// 实现创建评论的逻辑
Ok(())
}
🧪 TypeScript集成测试
🎯 设置TypeScript测试环境
# 📦 安装测试依赖
npm install --save-dev mocha chai @types/mocha @types/chai ts-node
# 🎨 安装Solana相关包
npm install @solana/web3.js @solana/spl-token
📝 配置package.json
{
"name": "movie-review-tests",
"scripts": {
"test": "mocha -r ts-node/register './test/**/*.test.ts' --timeout 10000",
"test:watch": "npm run test -- --watch"
}
}
🚀 编写TypeScript测试
// 🧪 test/movie-review.test.ts
import { expect } from 'chai';
import * as anchor from '@project-serum/anchor';
import { Program } from '@project-serum/anchor';
import {
Connection,
Keypair,
PublicKey,
Transaction,
SystemProgram
} from '@solana/web3.js';
describe('🎬 Movie Review Program Tests', () => {
// 🌐 设置连接和程序
const connection = new Connection('http://localhost:8899', 'confirmed');
let program: Program;
let payer: Keypair;
// 🏗️ 测试前准备
before(async () => {
console.log('🚀 初始化测试环境...');
// 创建付款账户
payer = Keypair.generate();
// 空投一些SOL
const airdropSig = await connection.requestAirdrop(
payer.publicKey,
2 * anchor.web3.LAMPORTS_PER_SOL
);
await connection.confirmTransaction(airdropSig);
console.log('✅ 测试环境准备完成!');
});
// 🧪 测试:创建电影评论
it('应该成功创建电影评论', async () => {
console.log('📝 测试创建电影评论...');
// 准备测试数据
const title = "Inception";
const rating = 5;
const description = "Mind-bending masterpiece!";
// 创建PDA
const [reviewPda, bump] = await PublicKey.findProgramAddress(
[
Buffer.from("review"),
payer.publicKey.toBuffer(),
Buffer.from(title)
],
program.programId
);
// 执行交易
const tx = await program.methods
.addMovieReview(title, rating, description)
.accounts({
reviewer: payer.publicKey,
reviewAccount: reviewPda,
systemProgram: SystemProgram.programId,
})
.signers([payer])
.rpc();
console.log('📋 交易签名:', tx);
// 获取并验证账户数据
const reviewAccount = await program.account.movieReview.fetch(reviewPda);
// 断言
expect(reviewAccount.title).to.equal(title);
expect(reviewAccount.rating).to.equal(rating);
expect(reviewAccount.description).to.equal(description);
console.log('✅ 电影评论创建成功!');
});
// 🧪 测试:添加评论
it('应该成功添加评论', async () => {
console.log('💬 测试添加评论...');
const comment = "I totally agree! Amazing movie!";
// ... 实现添加评论的测试逻辑 ...
expect(commentAccount.comment).to.equal(comment);
console.log('✅ 评论添加成功!');
});
// 🧪 测试:错误处理
it('应该拒绝无效的评分', async () => {
console.log('🚫 测试无效评分处理...');
const invalidRating = 10; // 超过5的评分
try {
await program.methods
.addMovieReview("Test Movie", invalidRating, "Test")
.accounts({/* ... */})
.rpc();
// 如果没有抛出错误,测试失败
expect.fail('应该抛出错误!');
} catch (error) {
// 验证错误消息
expect(error.message).to.include('InvalidRating');
console.log('✅ 正确拒绝了无效评分!');
}
});
// 🧹 测试后清理
after(async () => {
console.log('🧹 清理测试环境...');
// 清理代码
});
});
🚀 运行TypeScript测试
# 🧪 运行所有测试
npm run test
# 👀 监视模式(自动重新运行)
npm run test:watch
# 🎯 运行特定测试文件
npm test -- test/movie-review.test.ts
# 📊 生成覆盖率报告
npm run test:coverage
🚨 错误处理与调试
🔍 理解错误代码
// 🎯 自定义错误枚举
#[derive(Debug, Error)]
pub enum MovieReviewError {
#[error("0x01 - 资金不足")] // 错误代码 0x01
InsufficientFunds,
#[error("0x02 - 评分无效")] // 错误代码 0x02
InvalidRating,
#[error("0x03 - 标题太长")] // 错误代码 0x03
TitleTooLong,
}
// 🔧 错误代码转换辅助函数
pub fn decode_error(error_code: u32) -> String {
match error_code {
0x01 => "资金不足 💸".to_string(),
0x02 => "评分无效 ⭐".to_string(),
0x03 => "标题太长 📏".to_string(),
_ => format!("未知错误: 0x{:02X}", error_code),
}
}
📜 程序日志最佳实践
use solana_program::msg;
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// 🎬 开始日志
msg!("🚀 程序开始执行");
msg!("📋 程序ID: {}", program_id);
msg!("📊 账户数量: {}", accounts.len());
msg!("📦 数据长度: {} 字节", instruction_data.len());
// 🔍 详细日志
#[cfg(feature = "debug")]
{
msg!("🔍 调试模式 - 详细信息:");
for (i, account) in accounts.iter().enumerate() {
msg!(" 账户[{}]: {}", i, account.key);
msg!(" 可写: {}", account.is_writable);
msg!(" 签名: {}", account.is_signer);
}
}
// ⚠️ 错误日志
if instruction_data.is_empty() {
msg!("❌ 错误:指令数据为空!");
return Err(ProgramError::InvalidInstructionData);
}
// ✅ 成功日志
msg!("✅ 处理完成!");
Ok(())
}
💡 Pro Tip: 在测试中使用
println!()
而不是msg!()
!
💻 计算预算管理
📊 理解计算限制
┌──────────────────────────────────────┐
│ 💻 Solana计算预算 │
├──────────────────────────────────────┤
│ 默认预算: 200K CU × 指令数 │
│ 最大预算: 1.4M CU │
│ 基础费用: 5,000 Lamports │
│ │
│ 优先费用计算: │
│ 费用 = CU预算 × 微Lamports/CU │
└──────────────────────────────────────┘
🎯 优化计算预算
// 💰 设置自定义计算预算
import { ComputeBudgetProgram } from '@solana/web3.js';
// 🚀 创建优化的交易
async function createOptimizedTransaction() {
// 📊 Step 1: 设置计算单元限制
const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
units: 300_000 // 设置为300K CU
});
// 💸 Step 2: 添加优先费用(可选)
const addPriorityFee = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: 1000 // 每CU 0.001 Lamports
});
// 🔨 Step 3: 构建交易
const transaction = new Transaction()
.add(modifyComputeUnits) // 必须在前3条指令内!
.add(addPriorityFee) // 优先费用
.add(yourInstruction); // 你的实际指令
// 💡 计算总费用
const baseFee = 5000; // 基础费用
const priorityFee = 300_000 * 0.001; // 300 Lamports
const totalFee = baseFee + priorityFee;
console.log(`💰 预计总费用: ${totalFee} Lamports`);
return transaction;
}
🔍 监控计算使用
use solana_program::log::sol_log_compute_units;
pub fn process_complex_calculation() -> ProgramResult {
msg!("🚀 开始复杂计算...");
sol_log_compute_units(); // 记录当前CU
// 执行一些计算
perform_heavy_computation()?;
msg!("📊 计算后剩余CU:");
sol_log_compute_units(); // 查看使用了多少
Ok(())
}
📦 栈内存管理
🎯 避免栈溢出
// ❌ 错误:可能导致栈溢出
fn process_large_data() -> ProgramResult {
let huge_array = [0u8; 5000]; // 5KB > 4KB栈限制!
// 处理...
}
// ✅ 正确:使用堆分配
fn process_large_data_safely() -> ProgramResult {
// 使用Box将数据放在堆上
let huge_array = Box::new([0u8; 5000]); // 现在在堆上!
msg!("📦 数据大小: {} 字节", huge_array.len());
// 使用解引用操作符访问数据
let first_byte = (*huge_array)[0];
Ok(())
}
// 🎯 更优雅的解决方案:使用Vec
fn process_dynamic_data() -> ProgramResult {
// Vec自动在堆上分配
let mut data = Vec::with_capacity(5000);
data.resize(5000, 0);
msg!("📊 动态数据准备完成");
Ok(())
}
💡 内存优化技巧
// 🏗️ 智能内存管理示例
pub struct OptimizedStorage {
// 小数据:直接存储
pub id: u32, // 4字节,栈上
pub status: u8, // 1字节,栈上
// 大数据:使用Box
pub large_data: Box<[u8; 1024]>, // 1KB,堆上
// 动态数据:使用Vec
pub comments: Vec<String>, // 动态大小,堆上
}
impl OptimizedStorage {
pub fn new() -> Self {
Self {
id: 0,
status: 0,
large_data: Box::new([0; 1024]),
comments: Vec::new(),
}
}
// 🔍 检查内存使用
pub fn memory_usage(&self) -> usize {
let stack_usage = std::mem::size_of::<u32>() +
std::mem::size_of::<u8>();
let heap_usage = 1024 +
self.comments.capacity() * std::mem::size_of::<String>();
msg!("📊 栈使用: {} 字节", stack_usage);
msg!("📦 堆使用: {} 字节", heap_usage);
stack_usage + heap_usage
}
}
🎯 测试最佳实践
📋 测试清单
// 🏆 完美的测试结构
#[cfg(test)]
mod tests {
use super::*;
// 🏗️ 设置辅助函数
fn setup() -> TestEnvironment {
TestEnvironment::new()
}
// ✅ 测试正常路径
#[test]
fn test_happy_path() {
// Given (准备)
let env = setup();
// When (执行)
let result = perform_action(&env);
// Then (验证)
assert!(result.is_ok());
}
// 🚫 测试错误情况
#[test]
fn test_error_cases() {
// 测试各种错误场景
}
// 🔍 测试边界条件
#[test]
fn test_edge_cases() {
// 测试极限情况
}
// 🎭 测试状态转换
#[test]
fn test_state_transitions() {
// 测试状态变化
}
}
🚀 测试优化技巧
-
并行测试 🚄
cargo test-sbf -- --test-threads=8
-
只运行失败的测试 🎯
cargo test-sbf -- --failed
-
测试覆盖率 📊
cargo tarpaulin --out Html
-
性能测试 ⚡
#[bench]
fn bench_process_instruction(b: &mut Bencher) {
b.iter(|| process_instruction(/*...*/));
}
🎓 总结
📚 你学到的技能
┌────────────────────────────────────┐
│ 🏆 测试大师技能 │
├────────────────────────────────────┤
│ ✅ 编写单元测试 │
│ ✅ 创建集成测试 │
│ ✅ TypeScript测试 │
│ ✅ 错误处理和调试 │
│ ✅ 计算预算优化 │
│ ✅ 内存管理 │
└────────────────────────────────────┘
💡 记住这些要点
- 🧪 测试是投资 - 短期成本,长期收益
- 🎯 早测试,常测试 - 越早发现Bug越便宜
- 📊 覆盖率不是一切 - 质量比数量重要
- 🚀 自动化一切 - CI/CD是你的朋友
🚀 下一步
恭喜你成为了测试大师!🎉 现在你可以:
- 构建可靠的Solana程序
- 快速定位和修复Bug
- 自信地部署到主网
💬 终极智慧: "未经测试的代码就是Bug!" - 每个经验丰富的开发者
让我们一起构建更可靠的Web3世界! 🚀🧪✨