“文字地牢”小游戏通关 Rust 入门-测试
·
测试
上文我们大致了解了"文字地牢"小游戏的模块化与可见性,那么我们本章的目标是掌握单元测试与集成测试;了解断言宏与常见属性;构建可测试的设计。
基本概念
单元测试
单元测试是针对程序中最小可测试单元(通常是函数或方法)的测试,用于验证其行为是否符合预期。
单元测试的特点:
- 快速执行:通常在毫秒级完成
- 隔离性:测试单个函数或方法,不依赖外部系统
- 自动化:可以自动运行并报告结果
- 可重复性:每次运行都应该产生相同的结果
集成测试
集成测试是针对多个模块或组件协同工作的测试,验证它们之间的交互是否正确。
集成测试的特点:
- 端到端验证:测试整个功能流程
- 外部依赖:可能涉及文件系统、网络等外部系统
- 真实场景:更接近实际使用情况
- 较慢执行:通常比单元测试慢
断言宏
Rust提供了多种断言宏,如assert!、assert_eq!和assert_ne!,用于在测试中验证条件是否满足。
常用的断言宏:
- assert!:验证布尔条件
- assert_eq!:验证两个值是否相等
- assert_ne!:验证两个值是否不相等
- matches!:验证值是否匹配模式
- panic!:在测试中故意触发panic
测试属性
Rust提供了多种测试相关的属性来控制测试行为:
- #[test]:标记测试函数
- #[cfg(test)]:条件编译测试代码
- #[should_panic]:期望测试函数panic
- #[ignore]:标记忽略的测试
如何使用
编写单元测试
// 基本单元测试结构
#[cfg(test)]
mod tests {
use super::*; // 导入被测试模块的所有项
#[test]
fn test_addition() {
assert_eq!(2 + 2, 4);
}
#[test]
fn test_string_processing() {
let input = "hello world";
let expected = "HELLO WORLD";
assert_eq!(input.to_uppercase(), expected);
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn test_panic() {
let vec = vec![1, 2, 3];
let _ = vec[10]; // 这会panic
}
#[test]
#[ignore]
fn expensive_test() {
// 这个测试会被忽略,除非使用 --ignored 标志运行
std::thread::sleep(std::time::Duration::from_secs(10));
}
}
使用测试辅助工具
// 自定义断言消息
#[test]
fn test_with_custom_message() {
let left = 5;
let right = 10;
assert_eq!(left, right, "Left value {} should equal right value {}", left, right);
}
// 使用matches!宏进行模式匹配
#[test]
fn test_enum_variant() {
let result: Result<i32, &str> = Ok(42);
assert!(matches!(result, Ok(42)));
}
// 测试错误情况
#[test]
fn test_error_handling() {
let result = divide(10.0, 0.0);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Division by zero");
}
集成测试
// tests/integration_test.rs
use dungeon_explorer::game::{Game, GameState, Player, Position};
use dungeon_explorer::map::Map;
#[test]
fn test_full_game_flow() {
// 创建游戏状态
let map = Map::new(10, 10);
let player = Player {
position: Position { x: 1, y: 1 },
hp: 100,
};
let state = GameState { map, player };
let mut game = Game::new(state);
// 执行游戏操作
game.handle_move('d'); // 向右移动
// 验证结果
assert_eq!(game.state.player.position.x, 2);
assert_eq!(game.state.player.position.y, 1);
}
单元测试
- 在模块文件末尾添加:
#[cfg(test)]
mod tests { /* 用 super::* 访问当前模块项 */ }
- 使用断言:
assert_eq!,assert!,matches!;可用#[should_panic]断言崩溃。
集成测试
tests/目录下创建测试文件,针对库 API。- 二进制项目建议将逻辑提取到库模块以便集成测试。
注意事项
- 测试代码应该覆盖各种情况,包括正常情况、边界情况和错误情况。
- 避免在测试中引入外部依赖,如网络或文件系统,除非必要。
- 使用
#[should_panic]时要指定预期的panic信息,以确保测试的准确性。 - 保持测试的独立性,避免测试之间相互依赖。
- 定期运行测试,确保代码更改不会破坏现有功能。
- 使用描述性的测试函数名称,清楚表达测试意图。
- 避免在测试中使用随机数,除非使用固定种子。
- 对于耗时较长的测试,考虑使用
#[ignore]标记。
本项目中的使用
在我们的项目中,我们在各个模块文件的末尾添加了单元测试,例如在map/mod.rs中:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn map_border_is_wall() {
let m = Map::new(5, 5);
for x in 0..5 {
assert!(matches!(m.get_tile(x, 0), Tile::Wall));
assert!(matches!(m.get_tile(x, 4), Tile::Wall));
}
for y in 0..5 {
assert!(matches!(m.get_tile(0, y), Tile::Wall));
assert!(matches!(m.get_tile(4, y), Tile::Wall));
}
}
}
在game/mod.rs中也有类似的测试:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn move_blocked_by_wall() {
let map = Map::new(5, 5);
let player = Player { position: Position { x: 1, y: 1 }, hp: 10 };
let state = GameState { map, player };
let mut game = Game::new(state);
// 上方是墙(y=0)
game.handle_move('w');
assert_eq!(game.state.player.position, Position { x: 1, y: 1 });
}
}
这些测试确保了我们的核心功能按预期工作。
测试改进建议
在地牢探险游戏中,我们可以添加更多类型的测试:
- 边界条件测试:
#[test]
fn test_player_movement_boundaries() {
let map = Map::new(5, 5);
let player = Player { position: Position { x: 0, y: 0 }, hp: 10 };
let state = GameState { map, player };
let mut game = Game::new(state);
// 测试左边界
game.handle_move('a'); // 向左移动
assert_eq!(game.state.player.position, Position { x: 0, y: 0 });
// 测试上边界
game.handle_move('w'); // 向上移动
assert_eq!(game.state.player.position, Position { x: 0, y: 0 });
}
- 参数化测试:
#[test]
fn test_all_directions_blocked_by_walls() {
let test_cases = vec![
('w', Position { x: 1, y: 1 }), // 上
('s', Position { x: 1, y: 3 }), // 下
('a', Position { x: 1, y: 1 }), // 左
('d', Position { x: 3, y: 1 }), // 右
];
for (direction, expected_position) in test_cases {
let map = Map::new(5, 5);
let player = Player { position: expected_position, hp: 10 };
let state = GameState { map, player };
let mut game = Game::new(state);
game.handle_move(direction);
assert_eq!(game.state.player.position, expected_position,
"Failed for direction: {}", direction);
}
}
- Mock测试:
#[cfg(test)]
mod mock_tests {
use super::*;
// Mock随机数生成器用于可重现测试
struct MockRng {
values: std::collections::VecDeque<bool>,
}
impl MockRng {
fn new(values: Vec<bool>) -> Self {
Self {
values: values.into(),
}
}
}
impl rand::RngCore for MockRng {
fn next_u32(&mut self) -> u32 {
if let Some(val) = self.values.pop_front() {
if val { 1 } else { 0 }
} else {
0
}
}
fn next_u64(&mut self) -> u64 { self.next_u32() as u64 }
fn fill_bytes(&mut self, _dest: &mut [u8]) {}
fn try_fill_bytes(&mut self, _dest: &mut [u8]) -> Result<(), rand::Error> {
Ok(())
}
}
}
高级测试技术
测试覆盖率
# 安装tarpaulin
cargo install cargo-tarpaulin
# 运行覆盖率测试
cargo tarpaulin --ignore-tests
基准测试
// benches/game_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn bench_player_movement(c: &mut Criterion) {
let map = Map::new(10, 10);
let player = Player { position: Position { x: 1, y: 1 }, hp: 10 };
let state = GameState { map, player };
let mut game = Game::new(state);
c.bench_function("player_movement", |b| {
b.iter(|| {
game.handle_move(black_box('d'));
})
});
}
criterion_group!(benches, bench_player_movement);
criterion_main!(benches);
属性测试
// 使用proptest进行属性测试
use proptest::prelude::*;
proptest! {
#[test]
fn test_player_stays_in_bounds(x in 0..100usize, y in 0..100usize) {
let map = Map::new(10, 10);
let player = Player { position: Position { x, y }, hp: 10 };
let state = GameState { map, player };
let mut game = Game::new(state);
// 执行多次移动
for _ in 0..10 {
let directions = ['w', 'a', 's', 'd'];
let dir = *directions.choose(&mut rand::thread_rng()).unwrap();
game.handle_move(dir);
}
// 验证玩家仍在地图内
assert!(game.state.player.position.x < 10);
assert!(game.state.player.position.y < 10);
}
}
练习:
- 扩展移动逻辑的测试覆盖多方向与边界。
- 把渲染拆分为纯函数,断言字符串输出。
概念补充
- 测试金字塔:单元测试快而多,集成测试覆盖边界,端到端可选;优先构建可测试的纯逻辑。
- 隔离副作用:I/O、随机、时间通过注入接口或参数(种子/时钟)以替换为可预测实现。
- 固件与夹具:使用
#[ctor]/once_cell/lazy_static构造共享测试数据要谨慎,避免跨测试耦合。 - 断言与诊断:
pretty_assertions改善 diff 可读性;失败信息尽量包含关键上下文。 - 覆盖范围:
cargo tarpaulin(Linux)或基于 llvm-cov 的方案评估覆盖率,关注关键路径与错误分支。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)