From book
Generates tests for Dojo models and systems using spawn_test_world, cheat codes, and assertions. For testing game logic, verifying state changes, ensuring system correctness.
How this skill is triggered — by the user, by Claude, or both
Slash command
/book:dojo-testThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Write comprehensive tests for your Dojo models and systems using Cairo's test framework and Dojo's test utilities.
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) | Delete 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-deploynpx claudepluginhub dojoengine/bookGenerates Cairo Dojo system contracts for implementing game logic, modifying model state, handling player actions, and emitting events in Starknet games.
Guides writing and running Cairo smart-contract tests with snforge: unit, integration, fuzz, fork, and regression tests. Covers cheatcode usage, coverage, and test strategy.
Generates systematic test suites for any component by analyzing state transitions, failure modes, and integration points. Catches consistency bugs through lifecycle, cross-component, and logic tests.