dojo-test

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Dojo Test Generation

Dojo 测试生成

Write comprehensive tests for your Dojo models and systems using Cairo's test framework and Dojo's test utilities.
使用Cairo的测试框架和Dojo的测试工具为你的Dojo模型与系统编写全面测试。

When to Use This Skill

适用场景

  • "Write tests for the move system"
  • "Test the Position model"
  • "Add unit tests for combat logic"
  • "Create integration tests"
  • "为移动系统编写测试"
  • "测试Position模型"
  • "为战斗逻辑添加单元测试"
  • "创建集成测试"

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
生成包含以下内容的测试文件:
  • spawn_test_world()
    初始化设置
  • 模型与系统注册
  • 带断言的测试函数
  • 用于操纵执行上下文的作弊码使用
  • 状态验证

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"
交互式模式:
"Write tests for the spawn system"
我会询问以下信息:
  • 测试对象(模型、系统或两者)
  • 测试场景(正常流程、边缘情况)
  • 需要的状态断言
直接模式:
"Test that the move system correctly updates Position"

Running Tests

运行测试

bash
undefined
bash
undefined

Run all tests

Run all tests

sozo test
sozo test

Run specific test

Run specific test

sozo test test_spawn
undefined
sozo test test_spawn
undefined

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");
    }
}
将单元测试放在与模型相同的文件中:
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");
    }
}
创建
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");
}
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:
使用Starknet内置的测试作弊码来操纵执行上下文:

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

核心测试工具

FunctionPurpose
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
函数用途
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)
删除模型

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
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
编写测试后:
  1. 运行
    sozo test
    执行测试
  2. 使用
    dojo-review
    技能验证测试覆盖率
  3. 在部署前运行测试,配合
    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
  • dojo-model: 创建待测试的模型
  • dojo-system: 创建待测试的系统
  • dojo-review: 检查测试覆盖率
  • dojo-deploy: 测试通过后部署