# Test harness ## Which kind of test | Claim | Test | | --- | --- | | A rule holds (physics, scoring, generation, netcode) | unit test in the Cx-free crate | | Two systems compose correctly | integration test with both crates as dev-deps | | The app responds to input | `makepad-test` driven test | | It looks right | driven test + `screenshot()` + your inspection | | It is fast enough | probe example in `--release` + `frame_p90_ms()` | **Prefer the lowest rung that can express the claim.** Driven tests are slow and flaky relative to unit tests; use them for what only they can prove. ## Unit tests ```rust #[test] fn checkpoint_counts_once_per_crossing() { let mut kit = RaceKit::new(/* .. */); cross(&mut kit, 0); cross(&mut kit, 0); // same checkpoint again assert_eq!(kit.standings()[0].lap, 0, "re-crossing must not advance the lap"); } #[test] fn generator_is_deterministic() { let a = generate_level(1234); let b = generate_level(1234); assert_eq!(a, b, "same seed must give byte-identical output"); } ``` Determinism tests matter more here than in most codebases: netplay and `(preset, seed, position)` replication both depend on them, and a non-deterministic generator fails silently until a second player joins. ## Integration tests When the claim spans crates, add the other crate as a dev-dependency and test the composition. The house precedent: `makepad-game-blocks` dev-depends on `gen` and `render` because "a walker can enter a REAL generated room" and "the car's body does not float above the road" are claims about real geometry meeting real physics. Split across two crates they become two half-tests that both pass while the car hovers — which is exactly what happened. ```rust #[test] fn car_body_rests_on_the_road() { let road = makepad_game_gen::/* generate real road */; let car = Car::new(CarConfig::default()); let settled = settle(car, &road, 120); let lowest = lowest_vertex_y(&real_glb_body(), settled.transform()); assert!((lowest - road_surface_y).abs() < 0.05, "car floats or sinks: {lowest}"); } ``` ## Driven tests ```rust use makepad_test::{makepad_test, TestApp, KeyCode, Selector}; #[makepad_test] fn pause_and_resume(app: TestApp) { app.wait_for_log_contains("game: ready"); app.press_key(KeyCode::Escape); app.wait_for_log_contains("state: paused"); let paused = app.screenshot(); app.press_key(KeyCode::Escape); app.wait_for_log_contains("state: playing"); println!("paused shot: {}", paused.display()); } ``` Available on `TestApp`: `type_text`, `press_return`, `press_key`, `press_key_with_modifiers`, `touch_down`, `screenshot`, `widget_dump`, `widget_snapshot`, `wait_for_log_contains`, `locator(Selector)`, `forward(Vec)`. Each has a `try_` variant returning `TestResult` for when a failure is expected. `widget_dump()` prints the tree — use it to discover ids rather than guessing selectors. ## Synchronisation **Never sleep.** Emit one log line per state transition and wait on it: ```rust log!("game: ready"); log!("state: paused"); log!("score: {}", score); log!("race: won"); ``` This costs one line per transition and converts the entire game flow into something machine-checkable. It is the highest-value habit in this document. ## Asset-dependent tests They skip without the library, which is correct for CI and dangerous for reporting. ```rust fn require_assets() -> Option { let idx = load_index().ok()?; if idx.is_empty() { eprintln!("SKIP: run ./apps/arcade/download_assets.sh for real-asset coverage"); return None; } Some(idx) } ``` Check test output for skips before reporting a pass. And assert loading, since a bad id degrades silently: ```rust assert!(renderer.model_is_loaded("kenney/racing/vehicle-truck-yellow")); ``` ## Bot playtest ```rust #[makepad_test] fn five_minute_soak(app: TestApp) { app.wait_for_log_contains("game: ready"); for _ in 0..300 { drive_one_second(&app); assert!(!app.widget_dump().contains("panic")); } app.wait_for_log_contains("soak: complete"); } ``` Catches what unit tests structurally cannot: softlocks, memory growth, frame-time drift, and incomplete `reset()` (win twice in a row). ## Flakiness A flaky test is worse than no test — it trains everyone to ignore failures. Causes here: sleeping instead of waiting on a log; asserting exact floats (use an epsilon); depending on iteration order of an unordered container; assuming assets exist; and racing the first frame. Fix the cause; never add a retry.