Write tests for Dojo models and systems using spawn_test_world, cheat codes, and assertions. Use when testing game logic, verifying state changes, or ensuring system correctness.
83
Does it follow best practices?
If you maintain this skill, you can automatically optimize it using the tessl CLI to improve its score:
npx tessl skill review --optimize ./path/to/skillEvaluation — 99%
↑ 2.02xAgent success when using this skill
Validation for skill structure
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_spawnPlace 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 testsAfter writing tests:
sozo test to executedojo-review skill to verify test coveragedojo-deploy44466c6
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.