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.
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:
- Manual testing via
cargo run - Unit tests using Rust's standard
#[test]framework - Integration tests for business logic
- Visual regression testing (custom implementation)
- 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)
- No automated UI testing framework like Selenium or Playwright
- No visual regression testing built-in
- No accessibility testing tools
- No performance profiling integrated into test framework
Workarounds
- Manual testing with documented test cases
- Custom screenshot comparison tools
- Platform-specific testing (XCTest for iOS, Espresso for Android)
- Browser automation for web builds (Puppeteer, Playwright)
Resources
- Makepad GitHub Repository
- Makepad Examples
- Makepad Documentation
- Rust Testing Guide
- Robius Blog - Building with Makepad
Conclusion
While Makepad doesn't have a dedicated makepad-test crate, you can build robust testing strategies using:
- Rust's standard test framework for unit/integration tests
- Example applications for manual UI testing
- Platform-specific testing tools
- Custom visual regression frameworks
- 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.