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.
610 lines
14 KiB
Markdown
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.
|