home / skills / dojoengine / book / dojo-test

dojo-test skill

/skills/dojo-test

This skill helps you generate comprehensive Dojo tests for models and systems using spawn_test_world and cheat codes to verify state and behavior.

npx playbooks add skill dojoengine/book --skill dojo-test

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
8.2 KB
---
name: dojo-test
description: 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.
allowed-tools: Read, Write, Edit, Glob, Grep
---

# Dojo Test Generation

Write comprehensive tests for your Dojo models and systems using Cairo's test framework and Dojo's test utilities.

## When to Use This Skill

- "Write tests for the move system"
- "Test the Position model"
- "Add unit tests for combat logic"
- "Create integration tests"

## What This Skill Does

Generates test files with:
- `spawn_test_world()` setup
- Model and system registration
- Test functions with assertions
- Cheat code usage for manipulating execution context
- State verification

## Quick Start

**Interactive mode:**
```
"Write tests for the spawn system"
```

I'll ask about:
- What to test (models, systems, or both)
- Test scenarios (happy path, edge cases)
- State assertions needed

**Direct mode:**
```
"Test that the move system correctly updates Position"
```

## Running Tests

```bash
# Run all tests
sozo test

# Run specific test
sozo test test_spawn
```

## Test Structure

### Unit Tests (in model files)

Place unit tests in the same file as the model:

```cairo
// 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");
    }
}
```

### Integration Tests

Create a `tests` directory for system integration tests:

```cairo
// 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");
    }
}
```

### Testing Model Read/Write

```cairo
#[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");
}
```

## Cheat Codes

Use starknet's built-in testing cheat codes to manipulate execution context:

### Set Caller Address
```cairo
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
}
```

### Set Contract Address
```cairo
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
}
```

### Set Block Timestamp
```cairo
use starknet::testing;

#[test]
fn test_with_timestamp() {
    testing::set_block_timestamp(123456);
    // Now get_block_timestamp() returns 123456
}
```

### Set Block Number
```cairo
use starknet::testing;

#[test]
fn test_with_block_number() {
    testing::set_block_number(1234567);
    // Now get_block_number() returns 1234567
}
```

## Test Patterns

### Test Expected Panic
```cairo
#[test]
#[should_panic(expected: ('No moves remaining',))]
fn test_no_moves_remaining() {
    // Setup with zero moves
    // ...
    actions_system.move(Direction::Right(())); // Should panic
}
```

### Test Multiple Players
```cairo
#[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 State Transitions
```cairo
#[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");
}
```

## Key Test Utilities

| 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 |

## Test Organization

```
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
```

## Next Steps

After writing tests:
1. Run `sozo test` to execute
2. Use `dojo-review` skill to verify test coverage
3. Run tests before deploying with `dojo-deploy`

## Related Skills

- **dojo-model**: Create models to test
- **dojo-system**: Create systems to test
- **dojo-review**: Review test coverage
- **dojo-deploy**: Deploy after tests pass

Overview

This skill generates comprehensive tests for Dojo models and systems using spawn_test_world, cheat codes, and assertions. It focuses on producing unit and integration tests that verify game logic, state transitions, and system behaviors in TypeScript/Cairo test environments. Use it to speed up test creation and ensure consistent test patterns.

How this skill works

The skill scaffolds test files and test functions that set up a test world with spawn_test_world, register models and systems, and sync permissions. It inserts assertions, model read/write checks, and cheat-code calls (set_caller_address, set_contract_address, set_block_timestamp, etc.) to manipulate execution context and verify state changes. It can create unit tests in model files and integration tests in a tests directory following Dojo conventions.

When to use it

  • When you need unit tests for models (inline #[cfg(test)] modules).
  • When writing integration tests for systems using spawn_test_world().
  • To validate state transitions after system actions (moves, spawns, combat).
  • When testing multi-player scenarios or permissioned interactions.
  • To reproduce edge cases using cheat codes (timestamps, caller/contract addresses).

Best practices

  • Keep unit tests next to model code for fast feedback and clearer context.
  • Use spawn_test_world with a NamespaceDef to register only needed resources and contracts.
  • Prefer world.read_model and world.write_model_test for deterministic state checks and bypassing permissions when needed.
  • Use cheat codes to simulate callers, contracts, and time-based logic instead of complex setup flows.
  • Assert both positive (happy path) and negative (should_panic) scenarios; test state before and after actions.

Example use cases

  • Write a test verifying move system updates Position and decrements Moves remaining.
  • Add unit tests for a Position model: zero-vector checks and equality helpers.
  • Create integration tests that spawn players, perform actions, and assert independent player state.
  • Use testing::set_block_timestamp to validate time-gated abilities or events.
  • Write a should_panic test to ensure actions fail when preconditions (no moves left) are violated.

FAQ

How do I set up the test world resources?

Define a NamespaceDef with the model and event class hashes and pass [ndef].span() to spawn_test_world() to create the test world.

When should I use write_model_test vs read_model?

Use read_model for normal state inspection; use write_model_test to bypass permissions during setup or to inject specific states for edge-case tests.