Yew Testing Strategies and Practices
Testing Yew applications requires considering the WebAssembly environment and Rust testing frameworks. This article introduces comprehensive testing strategies.
Unit Testing
1. Component Unit Testing
rustuse yew::prelude::*; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); #[function_component(SimpleComponent)] fn simple_component() -> Html { let count = use_state(|| 0); let increment = { let count = count.clone(); Callback::from(move |_| count.set(*count + 1)) }; html! { <div> <button onclick={increment}>{ "Increment" }</button> <span>{ *count }</span> </div> } } #[wasm_bindgen_test] async fn test_simple_component() { let result = yew::ServerRenderer::<SimpleComponent>::render().await; assert!(result.contains("Increment")); assert!(result.contains("0")); }
2. Hook Unit Testing
rustuse yew::prelude::*; use yew_hooks::prelude::*; #[wasm_bindgen_test] async fn test_use_counter() { let result = yew::ServerRenderer::<CounterComponent>::render().await; assert!(result.contains("Counter: 0")); } #[function_component(CounterComponent)] fn counter_component() -> Html { let (count, set_count) = use_counter(0); html! { <div> <p>{ format!("Counter: {}", count) }</p> <button onclick={Callback::from(move |_| set_count(*count + 1))}> { "Increment" } </button> </div> } }
Integration Testing
1. Component Interaction Testing
rustuse yew::prelude::*; use wasm_bindgen_test::*; #[function_component(FormComponent)] fn form_component() -> Html { let username = use_state(|| String::new()); let submitted = use_state(|| false); let onsubmit = { let username = username.clone(); let submitted = submitted.clone(); Callback::from(move |e: SubmitEvent| { e.prevent_default(); if !username.is_empty() { submitted.set(true); } }) }; let oninput = { let username = username.clone(); Callback::from(move |e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); username.set(input.value()); }) }; html! { <form onsubmit={onsubmit}> <input type="text" value={(*username).clone()} oninput={oninput} placeholder="Username" /> <button type="submit">{ "Submit" }</button> { if *submitted { html! { <p>{ "Form submitted!" }</p> } } else { html! {} }} </form> } } #[wasm_bindgen_test] async fn test_form_submission() { let result = yew::ServerRenderer::<FormComponent>::render().await; assert!(result.contains("Username")); assert!(result.contains("Submit")); }
2. Async Component Testing
rustuse yew::prelude::*; use wasm_bindgen_futures::spawn_local; #[function_component(AsyncComponent)] fn async_component() -> Html { let data = use_state(|| String::new()); let loading = use_state(|| false); let fetch_data = { let data = data.clone(); let loading = loading.clone(); Callback::from(move |_| { loading.set(true); spawn_local(async move { gloo_timers::future::sleep(std::time::Duration::from_millis(100)).await; data.set("Async data loaded!".to_string()); loading.set(false); }); }) }; html! { <div> <button onclick={fetch_data} disabled={*loading}> { if *loading { "Loading..." } else { "Fetch Data" } } </button> <p>{ &*data }</p> </div> } } #[wasm_bindgen_test] async fn test_async_component() { let result = yew::ServerRenderer::<AsyncComponent>::render().await; assert!(result.contains("Fetch Data")); assert!(result.contains("Loading...")); }
End-to-End Testing
1. Using yew-app for E2E Testing
rustuse yew::prelude::*; use wasm_bindgen_test::*; #[function_component(App)] fn app() -> Html { let count = use_state(|| 0); let increment = { let count = count.clone(); Callback::from(move |_| count.set(*count + 1)) }; html! { <div id="app"> <h1>{ "Counter App" }</h1> <button id="increment-btn" onclick={increment}> { "Increment" } </button> <p id="count-display">{ *count }</p> </div> } } #[wasm_bindgen_test] async fn test_counter_increment() { // Render application yew::Renderer::<App>::with_root( gloo::utils::document().get_element_by_id("output").unwrap(), ).render(); // Simulate click let button = gloo::utils::document() .get_element_by_id("increment-btn") .unwrap(); let event = web_sys::MouseEvent::new("click").unwrap(); button.dispatch_event(&event).unwrap(); // Verify result let display = gloo::utils::document() .get_element_by_id("count-display") .unwrap(); assert_eq!(display.text_content().unwrap(), "1"); }
Mock and Stub
1. Mock HTTP Requests
rustuse yew::prelude::*; use gloo_net::http::Request; #[function_component(ApiComponent)] fn api_component() -> Html { let user = use_state(|| None::<String>); let loading = use_state(|| false); let fetch_user = { let user = user.clone(); let loading = loading.clone(); Callback::from(move |_| { loading.set(true); spawn_local(async move { match Request::get("/api/user").send().await { Ok(response) => { if response.ok() { if let Ok(data) = response.text().await { user.set(Some(data)); } } } Err(_) => {} } loading.set(false); }); }) }; html! { <div> <button onclick={fetch_user} disabled={*loading}> { if *loading { "Loading..." } else { "Fetch User" } } </button> { if let Some(ref u) = *user { html! { <p>{ u }</p> } } else { html! {} }} </div> } } #[cfg(test)] mod tests { use super::*; use wasm_bindgen_test::*; #[wasm_bindgen_test] async fn test_api_component_with_mock() { // In tests, you can use mock servers // Simplified here to test component rendering let result = yew::ServerRenderer::<ApiComponent>::render().await; assert!(result.contains("Fetch User")); } }
2. Mock WebSocket
rustuse yew::prelude::*; use gloo_net::websocket::Message; #[function_component(WebSocketComponent)] fn websocket_component() -> Html { let messages = use_state(|| Vec::<String>::new()); let connected = use_state(|| false); let connect = { let messages = messages.clone(); let connected = connected.clone(); Callback::from(move |_| { let ws = gloo_net::websocket::futures::WebSocket::open("ws://localhost:8080"); if let Ok(ws) = ws { let onmessage = { let messages = messages.clone(); Callback::from(move |msg: Message| { if let Message::Text(text) = msg { let mut msgs = (*messages).clone(); msgs.push(text); messages.set(msgs); } }) }; ws.set_onmessage(onmessage); connected.set(true); } }) }; html! { <div> <button onclick={connect} disabled={*connected}> { if *connected { "Connected" } else { "Connect" } } </button> <ul> { messages.iter().map(|msg| { html! { <li>{ msg }</li> } }).collect::<Html>() } </ul> </div> } }
Test Coverage
1. Using tarpaulin for Test Coverage
bash# Install tarpaulin cargo install cargo-tarpaulin # Run coverage tests cargo tarpaulin --out Html --output-dir ./coverage # For Wasm tests wasm-pack test --firefox --headless
2. Configure Test Coverage
toml# Cargo.toml [package] name = "yew-app" version = "0.1.0" edition = "2021" [dev-dependencies] wasm-bindgen-test = "0.3" gloo = "0.8" web-sys = { version = "0.3", features = ["console"] }
Performance Testing
1. Component Rendering Performance Testing
rustuse yew::prelude::*; use web_sys::Performance; #[function_component(PerformanceTestComponent)] fn performance_test_component() -> Html { let render_time = use_state(|| 0.0); let measure_render = { let render_time = render_time.clone(); Callback::from(move |_| { if let Ok(performance) = Performance::new() { let start = performance.now(); // Simulate render operation let _ = std::hint::black_box(42); let end = performance.now(); render_time.set(end - start); } }) }; html! { <div> <button onclick={measure_render}>{ "Measure Render" }</button> <p>{ "Render time: " }{ *render_time }{ " ms" }</p> </div> } } #[wasm_bindgen_test] async fn test_render_performance() { let result = yew::ServerRenderer::<PerformanceTestComponent>::render().await; assert!(result.contains("Measure Render")); assert!(result.contains("Render time:")); }
Testing Best Practices
1. Test Organization
rust// tests/common/mod.rs pub mod common { use yew::prelude::*; pub fn setup_test_environment() { // Setup test environment } pub fn cleanup_test_environment() { // Cleanup test environment } } // tests/components/button_test.rs mod button_test { use super::common::*; use wasm_bindgen_test::*; #[wasm_bindgen_test] async fn test_button_render() { setup_test_environment(); let result = yew::ServerRenderer::<ButtonComponent>::render().await; assert!(result.contains("Click me")); cleanup_test_environment(); } }
2. Test Data Management
rust// tests/fixtures/mod.rs pub mod fixtures { pub fn get_test_user() -> User { User { id: 1, name: "Test User".to_string(), email: "test@example.com".to_string(), } } pub fn get_test_posts() -> Vec<Post> { vec![ Post { id: 1, title: "Test Post 1".to_string(), body: "Test content".to_string(), }, Post { id: 2, title: "Test Post 2".to_string(), body: "More test content".to_string(), }, ] } }
Continuous Integration
1. GitHub Actions Configuration
yaml# .github/workflows/test.yml name: Test on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable override: true - name: Install wasm-pack run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: Run tests run: | cargo test --all wasm-pack test --firefox --headless - name: Run coverage run: cargo tarpaulin --out Xml
2. GitLab CI Configuration
yaml# .gitlab-ci.yml stages: - test test: stage: test image: rust:latest before_script: - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh script: - cargo test --all - wasm-pack test --firefox --headless - cargo tarpaulin --out Xml coverage: '/^\d+.\d+% coverage/' artifacts: reports: coverage_report: coverage_format: cobertura path: cobertura.xml
Debugging Tests
1. Using Browser Developer Tools
rustuse web_sys::console; #[wasm_bindgen_test] async fn test_with_debugging() { console::log_1(&"Starting test".into()); let result = yew::ServerRenderer::<TestComponent>::render().await; console::log_2( &"Render result:".into(), &result.into() ); assert!(result.contains("Expected content")); }
2. Using console_error_panic_hook
rustuse console_error_panic_hook; #[wasm_bindgen_test] async fn test_with_panic_hook() { console_error_panic_hook::set_once(); // Test code let result = yew::ServerRenderer::<TestComponent>::render().await; assert!(result.contains("Expected content")); }
Recommended Testing Tools
- wasm-bindgen-test: Wasm environment testing framework
- yew-app: Yew application testing tool
- gloo: Web API bindings and utilities
- cargo-tarpaulin: Code coverage tool
- web-sys: Web API bindings
Summary
Yew testing requires combining Rust testing frameworks with WebAssembly characteristics. Through reasonable testing strategies, mock techniques, and continuous integration configuration, you can build high-quality Yew applications.