Skip to main content

πŸ§ͺ Comprehensive Testing Guide

Build confidence in your Dubhe applications with robust testing strategies

Prerequisites: Basic understanding of Move development and Dubhe ECS concepts

🎯 Testing Philosophy

Testing blockchain applications requires a multi-layered approach due to their distributed, immutable nature. Dubhe’s ECS architecture makes testing more straightforward by separating concerns and enabling focused unit tests.

Unit Testing

Test individual components and systems in isolation

Integration Testing

Test interactions between systems and components

End-to-End Testing

Test complete user workflows across frontend and blockchain

πŸ“‹ Testing Pyramid

🌐 E2E Tests

Few, expensive, high confidence

  • Complete user workflows
  • Frontend to blockchain integration
  • Real network interactions

πŸ”— Integration Tests

More tests, moderate cost, good confidence

  • System interactions
  • Component combinations
  • Event emission and handling

🧩 Unit Tests

Many tests, cheap, fast feedback

  • Individual functions
  • Component creation and validation
  • Pure logic testing

🧩 Unit Testing Move Contracts

Component Testing

#[test_only]
module game::test_components {
    use game::components::{HealthComponent, PositionComponent, AttackComponent};
    
    #[test]
    fun test_health_component_creation() {
        let health = HealthComponent {
            current: 100,
            maximum: 100,
        };
        
        assert!(health.current == 100, 0);
        assert!(health.maximum == 100, 1);
    }
    
    #[test]
    fun test_position_component_validation() {
        let position = PositionComponent {
            x: 50,
            y: 75,
        };
        
        assert!(position.x == 50, 0);
        assert!(position.y == 75, 1);
    }
    
    #[test]
    fun test_attack_component_with_cooldown() {
        let attack = AttackComponent {
            damage: 25,
            range: 3,
            cooldown: 1000, // 1 second in milliseconds
        };
        
        assert!(attack.damage == 25, 0);
        assert!(attack.range == 3, 1);
        assert!(attack.cooldown == 1000, 2);
    }
}

System Testing

#[test_only]
module game::test_combat_system {
    use sui::test_scenario::{Self, Scenario};
    use game::world::{Self, World};
    use game::combat_system;
    use game::components::{HealthComponent, AttackComponent, DefenseComponent};
    
    #[test]
    fun test_basic_damage_calculation() {
        let mut scenario = test_scenario::begin(@0x1);
        let ctx = test_scenario::ctx(&mut scenario);
        
        // Create test world
        let world = world::create_for_testing(ctx);
        
        // Create attacker
        let attacker = world::spawn_entity(&mut world);
        world::add_component(&mut world, attacker, AttackComponent {
            damage: 30,
            range: 1,
            cooldown: 0,
        });
        
        // Create target
        let target = world::spawn_entity(&mut world);
        world::add_component(&mut world, target, HealthComponent {
            current: 100,
            maximum: 100,
        });
        
        // Execute attack
        combat_system::deal_damage(&mut world, target, 30);
        
        // Verify damage
        let health = world::get_component<HealthComponent>(&world, target);
        assert!(health.current == 70, 0); // 100 - 30 = 70
        
        test_scenario::return_shared(world);
        test_scenario::end(scenario);
    }
    
    #[test]
    fun test_damage_with_defense() {
        let mut scenario = test_scenario::begin(@0x1);
        let ctx = test_scenario::ctx(&mut scenario);
        
        let world = world::create_for_testing(ctx);
        
        let attacker = world::spawn_entity(&mut world);
        world::add_component(&mut world, attacker, AttackComponent {
            damage: 50,
            range: 1,
            cooldown: 0,
        });
        
        let target = world::spawn_entity(&mut world);
        world::add_component(&mut world, target, HealthComponent {
            current: 100,
            maximum: 100,
        });
        world::add_component(&mut world, target, DefenseComponent {
            armor: 10,        // Reduces damage by 10
            resistance: 20,   // Reduces remaining damage by 20%
        });
        
        // Execute attack with defense calculation
        combat_system::attack(&mut world, attacker, target, ctx);
        
        // Expected: (50 - 10) * 0.8 = 40 * 0.8 = 32 damage
        // Health should be: 100 - 32 = 68
        let health = world::get_component<HealthComponent>(&world, target);
        assert!(health.current == 68, 0);
        
        test_scenario::return_shared(world);
        test_scenario::end(scenario);
    }
    
    #[test]
    fun test_lethal_damage() {
        let mut scenario = test_scenario::begin(@0x1);
        let ctx = test_scenario::ctx(&mut scenario);
        
        let world = world::create_for_testing(ctx);
        
        let attacker = world::spawn_entity(&mut world);
        world::add_component(&mut world, attacker, AttackComponent {
            damage: 150, // More than target health
            range: 1,
            cooldown: 0,
        });
        
        let target = world::spawn_entity(&mut world);
        world::add_component(&mut world, target, HealthComponent {
            current: 100,
            maximum: 100,
        });
        
        combat_system::attack(&mut world, attacker, target, ctx);
        
        // Target should be dead
        let health = world::get_component<HealthComponent>(&world, target);
        assert!(health.current == 0, 0);
        
        // Should have death component
        assert!(world::has_component<DeadTag>(&world, target), 1);
        
        test_scenario::return_shared(world);
        test_scenario::end(scenario);
    }
    
    #[test]
    #[expected_failure(abort_code = game::combat_system::ETargetNotFound)]
    fun test_attack_nonexistent_target() {
        let mut scenario = test_scenario::begin(@0x1);
        let ctx = test_scenario::ctx(&mut scenario);
        
        let world = world::create_for_testing(ctx);
        
        let attacker = world::spawn_entity(&mut world);
        world::add_component(&mut world, attacker, AttackComponent {
            damage: 30,
            range: 1,
            cooldown: 0,
        });
        
        // Try to attack entity that doesn't exist
        combat_system::attack(&mut world, attacker, 999, ctx);
        
        test_scenario::return_shared(world);
        test_scenario::end(scenario);
    }
}

Test Utilities

#[test_only]
module game::test_utils {
    use sui::test_scenario::{Self, Scenario};
    use game::world::{Self, World};
    use game::components::*;
    
    // Helper to create a basic player entity
    public fun create_test_player(world: &mut World, health: u64): u64 {
        let entity = world::spawn_entity(world);
        
        world::add_component(world, entity, HealthComponent {
            current: health,
            maximum: health,
        });
        
        world::add_component(world, entity, PositionComponent {
            x: 50,
            y: 50,
        });
        
        world::add_component(world, entity, PlayerComponent {
            name: b"TestPlayer",
            owner: @0x1,
        });
        
        entity
    }
    
    // Helper to create a basic enemy entity
    public fun create_test_enemy(world: &mut World, damage: u64): u64 {
        let entity = world::spawn_entity(world);
        
        world::add_component(world, entity, HealthComponent {
            current: 50,
            maximum: 50,
        });
        
        world::add_component(world, entity, AttackComponent {
            damage,
            range: 1,
            cooldown: 1000,
        });
        
        world::add_component(world, entity, EnemyTag {});
        
        entity
    }
    
    // Helper to verify entity state
    public fun assert_entity_health(world: &World, entity: u64, expected: u64) {
        let health = world::get_component<HealthComponent>(world, entity);
        assert!(health.current == expected, 0);
    }
    
    // Helper to verify entity position
    public fun assert_entity_position(world: &World, entity: u64, x: u64, y: u64) {
        let pos = world::get_component<PositionComponent>(world, entity);
        assert!(pos.x == x, 0);
        assert!(pos.y == y, 1);
    }
}

πŸ”— Integration Testing

Multi-System Interactions

#[test_only]
module game::test_combat_inventory_integration {
    use sui::test_scenario::{Self, Scenario};
    use game::world::{Self, World};
    use game::combat_system;
    use game::inventory_system;
    use game::test_utils;
    
    #[test]
    fun test_weapon_affects_damage() {
        let mut scenario = test_scenario::begin(@0x1);
        let ctx = test_scenario::ctx(&mut scenario);
        
        let world = world::create_for_testing(ctx);
        
        // Create player with weapon
        let player = test_utils::create_test_player(&mut world, 100);
        
        // Create and equip weapon
        let weapon = world::spawn_entity(&mut world);
        world::add_component(&mut world, weapon, ItemComponent {
            name: b"Iron Sword",
            item_type: 0, // weapon
            stack_size: 1,
            rarity: 1,
        });
        world::add_component(&mut world, weapon, WeaponComponent {
            damage_bonus: 15,
            critical_chance: 10,
        });
        
        // Add weapon to inventory and equip
        world::add_component(&mut world, player, InventoryComponent {
            items: vector::singleton(weapon),
            capacity: 10,
        });
        
        inventory_system::equip_item(&mut world, player, weapon, 0, ctx);
        
        // Create target
        let target = test_utils::create_test_enemy(&mut world, 10);
        
        // Attack should now include weapon bonus
        combat_system::attack(&mut world, player, target, ctx);
        
        // Verify enhanced damage (base + weapon bonus)
        let health = world::get_component<HealthComponent>(&world, target);
        // Assuming base damage is 10, weapon adds 15, total 25
        assert!(health.current == 25, 0); // 50 - 25 = 25
        
        test_scenario::return_shared(world);
        test_scenario::end(scenario);
    }
    
    #[test]
    fun test_item_drop_on_death() {
        let mut scenario = test_scenario::begin(@0x1);
        let ctx = test_scenario::ctx(&mut scenario);
        
        let world = world::create_for_testing(ctx);
        
        let enemy = test_utils::create_test_enemy(&mut world, 10);
        
        // Give enemy an item to drop
        let loot_item = world::spawn_entity(&mut world);
        world::add_component(&mut world, loot_item, ItemComponent {
            name: b"Health Potion",
            item_type: 3, // consumable
            stack_size: 5,
            rarity: 0,
        });
        
        world::add_component(&mut world, enemy, LootTableComponent {
            items: vector::singleton(loot_item),
            drop_rates: vector::singleton(100), // 100% drop rate
        });
        
        // Kill the enemy
        combat_system::deal_damage(&mut world, enemy, 1000); // Overkill
        
        // Verify enemy is dead
        assert!(world::has_component<DeadTag>(&world, enemy), 0);
        
        // Verify item was dropped (should now have position component)
        assert!(world::has_component<PositionComponent>(&world, loot_item), 1);
        
        test_scenario::return_shared(world);
        test_scenario::end(scenario);
    }
}

🌐 Frontend Integration Testing

TypeScript/JavaScript Testing

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    setupFiles: ['./src/test-setup.ts'],
    testTimeout: 30000, // Blockchain interactions can be slow
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});

🎭 End-to-End Testing

Playwright E2E Tests

// e2e/setup.ts
import { test as base, expect } from '@playwright/test';
import { TestClient } from '@0xobelisk/dubhe-client';

type TestFixtures = {
  dubheClient: TestClient;
  gameUrl: string;
};

export const test = base.extend<TestFixtures>({
  dubheClient: async ({}, use) => {
    const client = await TestClient.setup({
      network: 'localnet',
      packageId: process.env.E2E_PACKAGE_ID,
    });
    
    await use(client);
    await client.cleanup();
  },
  
  gameUrl: async ({}, use) => {
    const url = process.env.E2E_BASE_URL || 'http://localhost:3000';
    await use(url);
  },
});

export { expect } from '@playwright/test';

🎯 Testing Best Practices

Test Organization

Test Structure

  • Arrange: Set up test data and environment
  • Act: Execute the function being tested
  • Assert: Verify the expected outcomes
  • Cleanup: Dispose of resources properly

Test Naming

  • Use descriptive names that explain the scenario
  • Include expected outcome in test name
  • Group related tests in describe blocks
  • Use consistent naming conventions

Data Management

// Create factory functions for test data
export const createTestPlayer = async (client: TestClient, overrides = {}) => {
  const entity = await client.createEntity();
  
  await client.setComponent(entity, 'PlayerComponent', {
    name: 'TestPlayer',
    owner: client.getAddress(),
    ...overrides,
  });
  
  await client.setComponent(entity, 'HealthComponent', {
    current: 100n,
    maximum: 100n,
    ...overrides,
  });
  
  return entity;
};

// Use builders for complex scenarios
export class GameScenarioBuilder {
  private client: TestClient;
  private entities: Map<string, bigint> = new Map();
  
  constructor(client: TestClient) {
    this.client = client;
  }
  
  async addPlayer(name: string, health = 100) {
    const entity = await createTestPlayer(this.client, { name, current: BigInt(health) });
    this.entities.set(name, entity);
    return this;
  }
  
  async addMonster(name: string, damage = 10) {
    const entity = await this.client.createEntity();
    await this.client.setComponent(entity, 'EnemyTag', {});
    await this.client.setComponent(entity, 'AttackComponent', {
      damage: BigInt(damage),
      range: 1n,
      cooldown: 1000n,
    });
    this.entities.set(name, entity);
    return this;
  }
  
  getEntity(name: string) {
    return this.entities.get(name);
  }
}

Continuous Integration

name: Test Suite

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Install Sui CLI
        run: |
          cargo install --locked --git https://github.com/MystenLabs/sui.git --branch mainnet sui
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run Move tests
        run: |
          sui move test --coverage
          sui move coverage summary
      
      - name: Run TypeScript tests
        run: npm run test:unit

  integration-tests:
    runs-on: ubuntu-latest
    needs: unit-tests
    steps:
      - uses: actions/checkout@v3
      
      - name: Start local blockchain
        run: |
          sui start --with-faucet &
          sleep 10
      
      - name: Deploy contracts
        run: |
          sui client publish --gas-budget 100000000
      
      - name: Run integration tests
        run: npm run test:integration

  e2e-tests:
    runs-on: ubuntu-latest
    needs: [unit-tests, integration-tests]
    steps:
      - uses: actions/checkout@v3
      
      - name: Install Playwright
        run: npx playwright install
      
      - name: Start services
        run: |
          sui start &
          npm run dev &
          npx wait-on http://localhost:3000
      
      - name: Run E2E tests
        run: npm run test:e2e
      
      - name: Upload test results
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-results
          path: test-results/

πŸ› Debugging Techniques

Move Debugging

#[test_only]
use std::debug;

public entry fun debug_combat_system(
    world: &mut World,
    attacker: u64,
    target: u64
) {
    debug::print(&b"=== Combat Debug Info ===");
    
    let attack_stats = world::get_component<AttackComponent>(world, attacker);
    debug::print(&b"Attack damage:");
    debug::print(&attack_stats.damage);
    
    let target_health_before = world::get_component<HealthComponent>(world, target);
    debug::print(&b"Target health before:");
    debug::print(&target_health_before.current);
    
    // Execute combat logic
    combat_system::attack(world, attacker, target, ctx);
    
    let target_health_after = world::get_component<HealthComponent>(world, target);
    debug::print(&b"Target health after:");
    debug::print(&target_health_after.current);
}

πŸ“Š Performance Testing

Load Testing

# artillery-config.yml
config:
  target: 'http://localhost:3000'
  phases:
    - duration: 60
      arrivalRate: 10
    - duration: 120
      arrivalRate: 50
    - duration: 60
      arrivalRate: 100
  plugins:
    websocket: {}
    
scenarios:
  - name: "Create and update entities"
    weight: 70
    engine: http
    flow:
      - post:
          url: "/api/entities"
          json:
            components:
              - type: "HealthComponent"
                data: { current: 100, maximum: 100 }
      - put:
          url: "/api/entities/{{ $randomString() }}/components/HealthComponent"
          json:
            current: 50
            maximum: 100
  
  - name: "WebSocket updates"
    weight: 30
    engine: ws
    flow:
      - connect:
          url: "ws://localhost:3001"
      - send:
          payload: '{"type": "subscribe", "component": "HealthComponent"}'
      - think: 30

Gas Usage Testing

#[test]
fun test_batch_operations_gas_efficiency() {
    let scenario_vals = test_scenario::begin(ADMIN);
    let scenario = &mut scenario_vals;
    let ctx = test_scenario::ctx(scenario);
    
    // Test individual operations
    let start_gas = tx_context::gas_used(ctx);
    
    let entity1 = world::spawn_entity(world);
    let entity2 = world::spawn_entity(world);
    let entity3 = world::spawn_entity(world);
    
    let individual_gas = tx_context::gas_used(ctx) - start_gas;
    
    // Test batch operations
    let batch_start_gas = tx_context::gas_used(ctx);
    
    world::spawn_entities_batch(world, 3);
    
    let batch_gas = tx_context::gas_used(ctx) - batch_start_gas;
    
    // Batch should be more efficient
    assert!(batch_gas < individual_gas, 0);
    
    test_scenario::end(scenario_vals);
}

🎯 Testing Checklist

Pre-Deployment Checklist

1

Unit Test Coverage

  • All components have creation tests
  • All systems have basic functionality tests
  • Edge cases and error conditions tested
  • Gas usage within acceptable limits
2

Integration Testing

  • Multi-system interactions work correctly
  • Event emission and handling verified
  • Database state consistency maintained
  • Frontend-backend integration functional
3

Performance Validation

  • Load testing completed successfully
  • WebSocket connections stable under load
  • Response times within SLA requirements
  • Memory usage optimized
4

Security Testing

  • Access control mechanisms verified
  • Input validation comprehensive
  • Error handling doesn’t leak information
  • Rate limiting functional

πŸ“š Advanced Testing Topics

Property-Based Testing

Use fuzzing and property-based testing for comprehensive coverage

Chaos Engineering

Test system resilience under failure conditions

Contract Verification

Formal verification techniques for critical contract logic

Multi-Chain Testing

Testing strategies across different blockchain networks

πŸš€ Next Steps

1

Implement Basic Tests

Start with unit tests for your components and systems
2

Add Integration Tests

Test interactions between different parts of your application
3

Set Up E2E Testing

Create comprehensive user workflow tests
4

Establish CI/CD Pipeline

Automate testing and deployment processes