重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
npx skills add https://github.com/dojoengine/book --skill dojo-test使用 Cairo 测试框架和 Dojo 测试工具,为你的 Dojo 模型和系统编写全面的测试。
生成包含以下内容的测试文件:
spawn_test_world() 设置交互模式:
"为生成系统编写测试"
我会询问:
直接模式:
"测试移动系统是否正确更新 Position"
# 运行所有测试
sozo test
# 运行特定测试
sozo test test_spawn
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
将单元测试放在与模型相同的文件中:
// models.cairo
#[derive(Copy, Drop, Serde)]
#[dojo::model]
struct Position {
#[key]
player: ContractAddress,
vec: Vec2,
}
#[cfg(test)]
mod tests {
use super::{Position, Vec2, Vec2Trait};
#[test]
fn test_vec_is_zero() {
assert!(Vec2Trait::is_zero(Vec2 { x: 0, y: 0 }), "not zero");
}
#[test]
fn test_vec_is_equal() {
let position = Vec2 { x: 420, y: 0 };
assert!(position.is_equal(Vec2 { x: 420, y: 0 }), "not equal");
}
}
为系统集成测试创建一个 tests 目录:
// tests/test_move.cairo
#[cfg(test)]
mod tests {
use dojo::model::{ModelStorage, ModelValueStorage, ModelStorageTest};
use dojo::world::WorldStorageTrait;
use dojo_cairo_test::{spawn_test_world, NamespaceDef, TestResource, ContractDefTrait};
use dojo_starter::systems::actions::{actions, IActionsDispatcher, IActionsDispatcherTrait};
use dojo_starter::models::{Position, m_Position, Moves, m_Moves, Direction};
fn namespace_def() -> NamespaceDef {
NamespaceDef {
namespace: "dojo_starter",
resources: [
TestResource::Model(m_Position::TEST_CLASS_HASH),
TestResource::Model(m_Moves::TEST_CLASS_HASH),
TestResource::Event(actions::e_Moved::TEST_CLASS_HASH),
TestResource::Contract(actions::TEST_CLASS_HASH)
].span()
}
}
fn contract_defs() -> Span<ContractDef> {
[
ContractDefTrait::new(@"dojo_starter", @"actions")
.with_writer_of([dojo::utils::bytearray_hash(@"dojo_starter")].span())
].span()
}
#[test]
fn test_move() {
let caller = starknet::contract_address_const::<0x0>();
let ndef = namespace_def();
let mut world = spawn_test_world([ndef].span());
// 同步权限和初始化
world.sync_perms_and_inits(contract_defs());
// 从 DNS 获取合约地址
let (contract_address, _) = world.dns(@"actions").unwrap();
let actions_system = IActionsDispatcher { contract_address };
// 生成玩家
actions_system.spawn();
// 读取初始状态
let initial_moves: Moves = world.read_model(caller);
let initial_position: Position = world.read_model(caller);
assert(
initial_position.vec.x == 10 && initial_position.vec.y == 10,
"wrong initial position"
);
// 向右移动
actions_system.move(Direction::Right(()));
// 验证状态变化
let moves: Moves = world.read_model(caller);
assert(moves.remaining == initial_moves.remaining - 1, "moves is wrong");
let new_position: Position = world.read_model(caller);
assert(new_position.vec.x == initial_position.vec.x + 1, "position x is wrong");
assert(new_position.vec.y == initial_position.vec.y, "position y is wrong");
}
}
#[test]
fn test_world_test_set() {
let caller = starknet::contract_address_const::<0x0>();
let ndef = namespace_def();
let mut world = spawn_test_world([ndef].span());
// 测试初始位置(默认为零)
let mut position: Position = world.read_model(caller);
assert(position.vec.x == 0 && position.vec.y == 0, "initial position wrong");
// 测试 write_model_test(绕过权限)
position.vec.x = 122;
position.vec.y = 88;
world.write_model_test(@position);
let mut position: Position = world.read_model(caller);
assert(position.vec.y == 88, "write_model_test failed");
// 测试模型删除
world.erase_model(@position);
let position: Position = world.read_model(caller);
assert(position.vec.x == 0 && position.vec.y == 0, "erase_model failed");
}
使用 starknet 内置的测试作弊代码来操作执行上下文:
use starknet::{testing, contract_address_const};
#[test]
fn test_as_different_caller() {
let player1 = contract_address_const::<'player1'>();
testing::set_caller_address(player1);
// 现在 get_caller_address() 返回 player1
}
use starknet::{testing, contract_address_const};
#[test]
fn test_with_contract_address() {
let contract = contract_address_const::<'contract'>();
testing::set_contract_address(contract);
// 现在 get_contract_address() 返回 contract
}
use starknet::testing;
#[test]
fn test_with_timestamp() {
testing::set_block_timestamp(123456);
// 现在 get_block_timestamp() 返回 123456
}
use starknet::testing;
#[test]
fn test_with_block_number() {
testing::set_block_number(1234567);
// 现在 get_block_number() 返回 1234567
}
#[test]
#[should_panic(expected: ('No moves remaining',))]
fn test_no_moves_remaining() {
// 设置零移动次数
// ...
actions_system.move(Direction::Right(())); // 应该恐慌
}
#[test]
fn test_two_players() {
let player1 = contract_address_const::<0x111>();
let player2 = contract_address_const::<0x222>();
// 玩家 1 操作
testing::set_contract_address(player1);
actions_system.spawn();
// 玩家 2 操作
testing::set_contract_address(player2);
actions_system.spawn();
// 验证两者具有独立状态
let pos1: Position = world.read_model(player1);
let pos2: Position = world.read_model(player2);
}
#[test]
fn test_spawn_then_move() {
// 初始状态
actions_system.spawn();
let initial: Position = world.read_model(caller);
// 转换
actions_system.move(Direction::Right(()));
// 验证
let after: Position = world.read_model(caller);
assert(after.vec.x == initial.vec.x + 1, "did not move right");
}
| 函数 | 用途 |
|---|---|
spawn_test_world([ndef].span()) | 创建包含模型的测试世界 |
world.sync_perms_and_inits(contract_defs()) | 同步权限 |
world.dns(@"contract_name") | 按名称获取合约地址 |
world.read_model(keys) | 读取模型状态 |
world.write_model_test(@model) | 写入模型(绕过权限) |
world.erase_model(@model) | 删除模型 |
src/
├── models.cairo # 在 #[cfg(test)] mod 中包含单元测试
├── systems/
│ └── actions.cairo # 在 #[cfg(test)] mod 中包含单元测试
└── tests/
└── test_world.cairo # 集成测试
编写测试后:
sozo test 来执行dojo-review 技能验证测试覆盖率dojo-deploy 运行测试每周安装次数
63
代码仓库
GitHub 星标数
53
首次出现
2026年1月30日
安全审计
安装于
codex59
opencode59
kimi-cli57
gemini-cli57
cursor57
github-copilot56
Write comprehensive tests for your Dojo models and systems using Cairo's test framework and Dojo's test utilities.
Generates test files with:
spawn_test_world() setupInteractive mode:
"Write tests for the spawn system"
I'll ask about:
Direct mode:
"Test that the move system correctly updates Position"
# Run all tests
sozo test
# Run specific test
sozo test test_spawn
Place unit tests in the same file as the model:
// models.cairo
#[derive(Copy, Drop, Serde)]
#[dojo::model]
struct Position {
#[key]
player: ContractAddress,
vec: Vec2,
}
#[cfg(test)]
mod tests {
use super::{Position, Vec2, Vec2Trait};
#[test]
fn test_vec_is_zero() {
assert!(Vec2Trait::is_zero(Vec2 { x: 0, y: 0 }), "not zero");
}
#[test]
fn test_vec_is_equal() {
let position = Vec2 { x: 420, y: 0 };
assert!(position.is_equal(Vec2 { x: 420, y: 0 }), "not equal");
}
}
Create a tests directory for system integration tests:
// tests/test_move.cairo
#[cfg(test)]
mod tests {
use dojo::model::{ModelStorage, ModelValueStorage, ModelStorageTest};
use dojo::world::WorldStorageTrait;
use dojo_cairo_test::{spawn_test_world, NamespaceDef, TestResource, ContractDefTrait};
use dojo_starter::systems::actions::{actions, IActionsDispatcher, IActionsDispatcherTrait};
use dojo_starter::models::{Position, m_Position, Moves, m_Moves, Direction};
fn namespace_def() -> NamespaceDef {
NamespaceDef {
namespace: "dojo_starter",
resources: [
TestResource::Model(m_Position::TEST_CLASS_HASH),
TestResource::Model(m_Moves::TEST_CLASS_HASH),
TestResource::Event(actions::e_Moved::TEST_CLASS_HASH),
TestResource::Contract(actions::TEST_CLASS_HASH)
].span()
}
}
fn contract_defs() -> Span<ContractDef> {
[
ContractDefTrait::new(@"dojo_starter", @"actions")
.with_writer_of([dojo::utils::bytearray_hash(@"dojo_starter")].span())
].span()
}
#[test]
fn test_move() {
let caller = starknet::contract_address_const::<0x0>();
let ndef = namespace_def();
let mut world = spawn_test_world([ndef].span());
// Sync permissions and initializations
world.sync_perms_and_inits(contract_defs());
// Get contract address from DNS
let (contract_address, _) = world.dns(@"actions").unwrap();
let actions_system = IActionsDispatcher { contract_address };
// Spawn player
actions_system.spawn();
// Read initial state
let initial_moves: Moves = world.read_model(caller);
let initial_position: Position = world.read_model(caller);
assert(
initial_position.vec.x == 10 && initial_position.vec.y == 10,
"wrong initial position"
);
// Move right
actions_system.move(Direction::Right(()));
// Verify state changes
let moves: Moves = world.read_model(caller);
assert(moves.remaining == initial_moves.remaining - 1, "moves is wrong");
let new_position: Position = world.read_model(caller);
assert(new_position.vec.x == initial_position.vec.x + 1, "position x is wrong");
assert(new_position.vec.y == initial_position.vec.y, "position y is wrong");
}
}
#[test]
fn test_world_test_set() {
let caller = starknet::contract_address_const::<0x0>();
let ndef = namespace_def();
let mut world = spawn_test_world([ndef].span());
// Test initial position (default zero)
let mut position: Position = world.read_model(caller);
assert(position.vec.x == 0 && position.vec.y == 0, "initial position wrong");
// Test write_model_test (bypasses permissions)
position.vec.x = 122;
position.vec.y = 88;
world.write_model_test(@position);
let mut position: Position = world.read_model(caller);
assert(position.vec.y == 88, "write_model_test failed");
// Test model deletion
world.erase_model(@position);
let position: Position = world.read_model(caller);
assert(position.vec.x == 0 && position.vec.y == 0, "erase_model failed");
}
Use starknet's built-in testing cheat codes to manipulate execution context:
use starknet::{testing, contract_address_const};
#[test]
fn test_as_different_caller() {
let player1 = contract_address_const::<'player1'>();
testing::set_caller_address(player1);
// Now get_caller_address() returns player1
}
use starknet::{testing, contract_address_const};
#[test]
fn test_with_contract_address() {
let contract = contract_address_const::<'contract'>();
testing::set_contract_address(contract);
// Now get_contract_address() returns contract
}
use starknet::testing;
#[test]
fn test_with_timestamp() {
testing::set_block_timestamp(123456);
// Now get_block_timestamp() returns 123456
}
use starknet::testing;
#[test]
fn test_with_block_number() {
testing::set_block_number(1234567);
// Now get_block_number() returns 1234567
}
#[test]
#[should_panic(expected: ('No moves remaining',))]
fn test_no_moves_remaining() {
// Setup with zero moves
// ...
actions_system.move(Direction::Right(())); // Should panic
}
#[test]
fn test_two_players() {
let player1 = contract_address_const::<0x111>();
let player2 = contract_address_const::<0x222>();
// Player 1 actions
testing::set_contract_address(player1);
actions_system.spawn();
// Player 2 actions
testing::set_contract_address(player2);
actions_system.spawn();
// Verify both have independent state
let pos1: Position = world.read_model(player1);
let pos2: Position = world.read_model(player2);
}
#[test]
fn test_spawn_then_move() {
// Initial state
actions_system.spawn();
let initial: Position = world.read_model(caller);
// Transition
actions_system.move(Direction::Right(()));
// Verify
let after: Position = world.read_model(caller);
assert(after.vec.x == initial.vec.x + 1, "did not move right");
}
| Function | Purpose |
|---|---|
spawn_test_world([ndef].span()) | Create test world with models |
world.sync_perms_and_inits(contract_defs()) | Sync permissions |
world.dns(@"contract_name") | Get contract address by name |
world.read_model(keys) | Read model state |
world.write_model_test(@model) | Write model (bypass permissions) |
world.erase_model(@model) |
src/
├── models.cairo # Include unit tests in #[cfg(test)] mod
├── systems/
│ └── actions.cairo # Include unit tests in #[cfg(test)] mod
└── tests/
└── test_world.cairo # Integration tests
After writing tests:
sozo test to executedojo-review skill to verify test coveragedojo-deployWeekly Installs
63
Repository
GitHub Stars
53
First Seen
Jan 30, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex59
opencode59
kimi-cli57
gemini-cli57
cursor57
github-copilot56
Grimoire Polymarket 技能:Polymarket 市场发现、CLOB 数据与订单管理 CLI 工具
11,400 周安装
| Delete model |