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

610 lines
14 KiB
Markdown

# 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:
```rust
// 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.
```rust
// 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)
```bash
# Run the application
cargo run
# Run in release mode
cargo run --release
# Run with specific features
cargo run --features map_style
```
#### Web (WASM)
```bash
# 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
```bash
# 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
```bash
# 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:
```bash
# 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:
```rust
// 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:
```rust
// 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:
```bash
cargo run --example test_button
```
### 4. Visual Regression Testing
Create a custom visual testing framework:
```rust
// 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
```bash
# 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
```bash
# 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
```bash
# 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
```rust
#[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
```markdown
# 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
```toml
[features]
default = []
test_mode = [] # Enable test-specific behavior
debug_ui = [] # Show debug overlays
```
```rust
#[cfg(feature = "test_mode")]
fn test_specific_logic() {
// Test-only code
}
```
### 6. Continuous Integration
```yaml
# .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
- [Makepad GitHub Repository](https://github.com/makepad/makepad)
- [Makepad Examples](https://github.com/makepad/makepad/tree/dev/examples)
- [Makepad Documentation](https://makepad.dev)
- [Rust Testing Guide](https://doc.rust-lang.org/book/ch11-00-testing.html)
- [Robius Blog - Building with Makepad](https://robius.rs/blog/building-chatgpt-client-using-makepad/)
## 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.