nigig-org/MAKEPAD_TESTING_BEST_PRACTICES.md
andodeki 3bf992e700 docs: add comprehensive Makepad testing best practices guide
This guide explains that Makepad does not have a dedicated makepad-test
crate or automated UI testing framework. Instead, it provides practical
testing strategies:

- Application structure for testability
- Separating business logic from UI code
- Unit testing with Rust's standard #[test] framework
- Integration testing patterns
- Manual UI testing via cargo run
- Platform-specific testing (iOS, Android, Web, Desktop)
- Visual regression testing (custom implementation)
- Example applications for manual testing
- Best practices and limitations

Includes code examples showing how to:
- Structure Makepad apps for testability
- Write unit tests for business logic
- Create example test applications
- Document manual test cases
- Set up CI/CD pipelines

This addresses the gap in Makepad's testing ecosystem by providing
a comprehensive guide for developers.
2026-07-27 17:11:07 +00:00

14 KiB

Makepad Testing Best Practices Guide

Overview

Makepad is a creative software development platform for Rust that compiles to multiple platforms (Web/WASM, macOS/Metal, Windows/DX11, Linux/OpenGL). Unlike some UI frameworks, Makepad does not have a dedicated automated UI testing framework like makepad-test.

However, Makepad applications can be thoroughly tested using:

  1. Manual testing via cargo run
  2. Unit tests using Rust's standard #[test] framework
  3. Integration tests for business logic
  4. Visual regression testing (custom implementation)
  5. Platform-specific testing (iOS, Android, Web, Desktop)

Testing Architecture

1. Application Structure for Testability

Makepad applications follow a specific structure that separates concerns:

// src/main.rs - Entry point
pub use makepad_widgets;
use makepad_widgets::*;

app_main!(App);

// src/app.rs - Application logic
live_design! {
    import makepad_widgets::base::*;
    import makepad_widgets::theme_desktop_dark::*;
    
    App = {{App}} {
        ui: <Window> {
            window: {inner_size: vec2(800, 600)},
            pass: {clear_color: #000}
            
            body = <View> {
                flow: Down,
                spacing: 10,
                padding: 20,
                
                title = <Label> {
                    text: "My App",
                    draw_text: {
                        color: #fff,
                        text_style: <TITLE_TEXT>{font_size: 24.0}
                    }
                }
                
                counter_label = <Label> {
                    text: "Count: 0",
                    draw_text: {
                        color: #fff,
                        text_style: <REGULAR_TEXT>{font_size: 18.0}
                    }
                }
                
                increment_button = <Button> {
                    text: "Increment",
                    draw_bg: {
                        color: #4a90e2
                    }
                }
            }
        }
    }
}

#[derive(Live, LiveHook)]
pub struct App {
    #[live] ui: WidgetRef,
    #[rust] counter: i32,
}

impl LiveRegister for App {
    fn live_register(cx: &mut Cx) {
        makepad_widgets::live_design(cx);
    }
}

impl MatchEvent for App {
    fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
        // Handle button clicks
        if self.ui.button(id!(increment_button)).clicked(&actions) {
            self.counter += 1;
            self.update_counter_label(cx);
        }
    }
}

impl AppMain for App {
    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
        self.match_event(cx, event);
        self.ui.handle_event(cx, event, &mut Scope::empty());
    }
}

impl App {
    fn update_counter_label(&mut self, cx: &mut Cx) {
        let label = self.ui.label(id!(counter_label));
        label.set_text(&format!("Count: {}", self.counter));
        label.redraw(cx);
    }
}

2. Separating Business Logic for Testing

Key Principle: Separate UI code from business logic to enable unit testing.

// src/logic.rs - Testable business logic
pub struct CounterLogic {
    count: i32,
}

impl CounterLogic {
    pub fn new() -> Self {
        Self { count: 0 }
    }
    
    pub fn increment(&mut self) -> i32 {
        self.count += 1;
        self.count
    }
    
    pub fn decrement(&mut self) -> i32 {
        self.count -= 1;
        self.count
    }
    
    pub fn reset(&mut self) -> i32 {
        self.count = 0;
        self.count
    }
    
    pub fn get_count(&self) -> i32 {
        self.count
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_increment() {
        let mut logic = CounterLogic::new();
        assert_eq!(logic.get_count(), 0);
        
        logic.increment();
        assert_eq!(logic.get_count(), 1);
        
        logic.increment();
        assert_eq!(logic.get_count(), 2);
    }
    
    #[test]
    fn test_decrement() {
        let mut logic = CounterLogic::new();
        logic.increment();
        logic.increment();
        
        logic.decrement();
        assert_eq!(logic.get_count(), 1);
    }
    
    #[test]
    fn test_reset() {
        let mut logic = CounterLogic::new();
        logic.increment();
        logic.increment();
        logic.increment();
        
        logic.reset();
        assert_eq!(logic.get_count(), 0);
    }
}

3. Running Makepad Applications

Desktop (macOS, Windows, Linux)

# Run the application
cargo run

# Run in release mode
cargo run --release

# Run with specific features
cargo run --features map_style

Web (WASM)

# Install wasm-pack
cargo install wasm-pack

# Build for web
wasm-pack build --target web

# Serve locally
python3 -m http.server 8080
# Open http://localhost:8080

iOS

# Install on simulator
cargo makepad apple ios --org=my.test --app=my-app run-sim -p my-app --release

# Install on device
cargo makepad apple ios \
  --org=my.test \
  --profile=ABC \
  --cert=DEF \
  --device=MyiPhone \
  --app=my-app \
  run-device -p my-app --release

Android

# Run on device/emulator
cargo makepad android run -p my-app --release

Testing Strategies

1. Unit Testing (Rust Standard)

Test business logic separately from UI:

# Run all tests
cargo test

# Run specific test
cargo test test_increment

# Run with output
cargo test -- --nocapture

# Run with coverage (requires cargo-tarpaulin)
cargo tarpaulin --out Html

2. Integration Testing

Test interactions between modules:

// tests/integration_test.rs
use my_app::logic::CounterLogic;

#[test]
fn test_full_workflow() {
    let mut logic = CounterLogic::new();
    
    // Simulate user workflow
    logic.increment();
    logic.increment();
    logic.increment();
    assert_eq!(logic.get_count(), 3);
    
    logic.decrement();
    assert_eq!(logic.get_count(), 2);
    
    logic.reset();
    assert_eq!(logic.get_count(), 0);
}

3. Manual UI Testing

Create test applications that demonstrate features:

// examples/test_button.rs
use makepad_widgets::*;

live_design! {
    import makepad_widgets::base::*;
    import makepad_widgets::theme_desktop_dark::*;
    
    TestButtonApp = {{TestButtonApp}} {
        ui: <Window> {
            window: {inner_size: vec2(400, 300)},
            
            body = <View> {
                flow: Down,
                spacing: 20,
                padding: 40,
                align: {x: 0.5, y: 0.5},
                
                <Label> {
                    text: "Button Test",
                    draw_text: {
                        color: #fff,
                        text_style: <TITLE_TEXT>{font_size: 24.0}
                    }
                }
                
                test_button = <Button> {
                    text: "Click Me",
                    draw_bg: {
                        color: #4a90e2,
                        color_hover: #5ba0f2,
                        color_down: #3a80d2
                    }
                }
                
                result_label = <Label> {
                    text: "Clicks: 0",
                    draw_text: {
                        color: #fff,
                        text_style: <REGULAR_TEXT>{font_size: 18.0}
                    }
                }
            }
        }
    }
}

app_main!(TestButtonApp);

#[derive(Live, LiveHook)]
pub struct TestButtonApp {
    #[live] ui: WidgetRef,
    #[rust] click_count: i32,
}

impl LiveRegister for TestButtonApp {
    fn live_register(cx: &mut Cx) {
        makepad_widgets::live_design(cx);
    }
}

impl MatchEvent for TestButtonApp {
    fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
        if self.ui.button(id!(test_button)).clicked(&actions) {
            self.click_count += 1;
            let label = self.ui.label(id!(result_label));
            label.set_text(&format!("Clicks: {}", self.click_count));
            label.redraw(cx);
        }
    }
}

impl AppMain for TestButtonApp {
    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
        self.match_event(cx, event);
        self.ui.handle_event(cx, event, &mut Scope::empty());
    }
}

Run the test:

cargo run --example test_button

4. Visual Regression Testing

Create a custom visual testing framework:

// tests/visual_test.rs
use makepad_widgets::*;
use image::{RgbaImage, save_buffer};

struct VisualTest {
    name: String,
    golden_path: String,
    diff_path: String,
}

impl VisualTest {
    fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
            golden_path: format!("tests/golden/{}.png", name),
            diff_path: format!("tests/diff/{}.png", name),
        }
    }
    
    fn capture_screenshot(&self, cx: &Cx) -> Vec<u8> {
        // Capture framebuffer
        // This is platform-specific and requires access to GPU
        todo!("Implement platform-specific screenshot")
    }
    
    fn compare_with_golden(&self, screenshot: &[u8]) -> f64 {
        // Load golden image
        let golden = image::open(&self.golden_path)
            .expect("Failed to load golden image");
        
        // Compare pixel-by-pixel
        let golden_pixels = golden.to_rgba8();
        let screenshot_pixels = RgbaImage::from_raw(
            golden.width(),
            golden.height(),
            screenshot.to_vec()
        ).unwrap();
        
        let mut diff_count = 0;
        let total_pixels = golden_pixels.width() * golden_pixels.height();
        
        for (g, s) in golden_pixels.pixels().zip(screenshot_pixels.pixels()) {
            if g != s {
                diff_count += 1;
            }
        }
        
        diff_count as f64 / total_pixels as f64
    }
    
    fn run(&self, cx: &Cx) {
        let screenshot = self.capture_screenshot(cx);
        let diff_percentage = self.compare_with_golden(&screenshot);
        
        if diff_percentage > 0.01 {
            panic!(
                "Visual regression detected: {} differs by {:.2}%",
                self.name,
                diff_percentage * 100.0
            );
        }
    }
}

5. Platform-Specific Testing

iOS Testing

# Run on simulator
cargo makepad apple ios --org=my.test --app=my-app run-sim -p my-app --release

# Run on device
cargo makepad apple ios \
  --org=my.test \
  --profile=PROFILE_ID \
  --cert=CERT_ID \
  --device=DEVICE_ID \
  --app=my-app \
  run-device -p my-app --release

Android Testing

# Run on emulator
cargo makepad android run -p my-app --release

# Run on device
adb devices  # List connected devices
cargo makepad android run -p my-app --release

Web Testing

# Build for web
wasm-pack build --target web

# Serve and test in browser
python3 -m http.server 8080
# Open http://localhost:8080 in browser
# Use browser DevTools for debugging

Best Practices

1. Separate Concerns

  • UI Layer: Makepad widgets and live_design DSL
  • Business Logic: Pure Rust functions (testable)
  • Data Layer: Models and state management

2. Use Rust's Test Framework

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_something() {
        // Test business logic
    }
}

3. Create Example Applications

examples/
├── test_button.rs
├── test_text_input.rs
├── test_list.rs
├── test_scroll.rs
└── test_map.rs

4. Document Manual Test Cases

# Manual Test Cases

## TC-001: Button Click
1. Run application: `cargo run`
2. Click "Increment" button
3. Expected: Counter increases by 1
4. Actual: Counter shows "Count: 1"
5. Status: PASS ✓

## TC-002: Button Hover
1. Hover mouse over button
2. Expected: Button color changes to lighter shade
3. Actual: Button color changes
4. Status: PASS ✓

5. Use Feature Flags for Testing

[features]
default = []
test_mode = []  # Enable test-specific behavior
debug_ui = []   # Show debug overlays
#[cfg(feature = "test_mode")]
fn test_specific_logic() {
    // Test-only code
}

6. Continuous Integration

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
      - name: Run tests
        run: cargo test --all-features
      - name: Build
        run: cargo build --release

Limitations

What Makepad Doesn't Have (Yet)

  1. No automated UI testing framework like Selenium or Playwright
  2. No visual regression testing built-in
  3. No accessibility testing tools
  4. No performance profiling integrated into test framework

Workarounds

  1. Manual testing with documented test cases
  2. Custom screenshot comparison tools
  3. Platform-specific testing (XCTest for iOS, Espresso for Android)
  4. Browser automation for web builds (Puppeteer, Playwright)

Resources

Conclusion

While Makepad doesn't have a dedicated makepad-test crate, you can build robust testing strategies using:

  1. Rust's standard test framework for unit/integration tests
  2. Example applications for manual UI testing
  3. Platform-specific testing tools
  4. Custom visual regression frameworks
  5. Continuous integration pipelines

The key is to separate business logic from UI code so you can test logic independently of rendering.

Example Test Suite Structure

my-app/
├── src/
│   ├── main.rs           # Entry point
│   ├── app.rs            # UI logic
│   └── logic.rs          # Business logic (testable)
├── tests/
│   ├── unit_tests.rs     # Unit tests for logic
│   ├── integration_tests.rs  # Integration tests
│   └── visual_tests.rs   # Visual regression tests
├── examples/
│   ├── test_button.rs    # Manual test: button
│   ├── test_input.rs     # Manual test: text input
│   └── test_map.rs       # Manual test: map widget
└── docs/
    └── test_cases.md     # Documented manual test cases

This structure gives you comprehensive test coverage while working within Makepad's current capabilities.