Testing Components in the Rust Frontend Framework Leptos

Testing Components in the Rust Frontend Framework Leptos

Testing Components

Testing user interfaces can be relatively tricky, but it is indeed very important. This article will discuss several principles and methods for testing Leptos applications.

1. Use Regular Rust Tests to Test Business Logic

In many cases, it is reasonable to extract logic from components and test it separately. For some simple components, there may not be specific logic to test, but for many components, it is worthwhile to implement logic in a testable wrapper type within a regular Rust <span>impl</span> block.

For example, instead of embedding logic directly in the component like this:

#[component]pub fn TodoApp() -> impl IntoView {    let (todos, set_todos) = signal(vec![Todo { /* ... */ }]);    //  ⚠️  This is hard to test because it is embedded in the component    let num_remaining = move || todos.read().iter().filter(|todo| !todo.completed).sum();}

You can extract that logic into a separate data structure and test it:

pub struct Todos(Vec<Todo>);impl Todos {    pub fn num_remaining(&self) -> usize {        self.0.iter().filter(|todo| !todo.completed).sum()    }}#[cfg(test)]mod tests {    #[test]    fn test_remaining() {        // ...    }}#[component]pub fn TodoApp() -> impl IntoView {    let (todos, set_todos) = signal(Todos(vec![Todo { /* ... */ }]));    // ✅  This has a relevant test    let num_remaining = move || todos.read().num_remaining();}

Generally, the less logic encapsulated within the component itself, the more idiomatic the code becomes and the easier it is to test.

2. Use End-to-End Tests to Test Components

Our <span>examples</span> directory contains several examples that have undergone extensive end-to-end testing using different testing tools.

The easiest way to learn how to use these tools is to look at the test examples themselves:

Using <span>wasm-bindgen-test</span> in the <span>counter</span> example

This is a fairly simple manual testing setup that uses the <span>wasm-pack test</span> command.

Example Test

#[wasm_bindgen_test]async fn clear() {    let document = document();    let test_wrapper = document.create_element("section").unwrap();    let _ = document.body().unwrap().append_child(&test_wrapper);    // First, render our counter and mount it to the DOM    // Note that we start from an initial value of 10    let _dispose = mount_to(        test_wrapper.clone().unchecked_into(),        || view! { &lt;SimpleCounter initial_value=10 step=1/&gt; },    );    // Now we extract the buttons by iterating through the DOM    // It would be easier if they had IDs    let div = test_wrapper.query_selector("div").unwrap().unwrap();    let clear = test_wrapper        .query_selector("button").unwrap()        .unwrap()        .unchecked_into::&lt;web_sys::HtmlElement&gt;();    // Now we click the `clear` button    clear.click();    // The reactive system is built on top of an asynchronous system, so changes will not be reflected in the DOM synchronously    // To detect changes here, we will yield briefly after each change,    // allowing the side effects that update the view to run    tick().await;    // Now let's test the &lt;div&gt; against the expected value    // We can do this by testing its `outerHTML`    assert_eq!(div.outer_html(), {        // It should be as if we created it with the value 0, right?        let (value, _set_value) = signal(0);        // We can remove event listeners since they won't be rendered to HTML        view! {            &lt;div&gt;                &lt;button&gt;"Clear"&lt;/button&gt;                &lt;button&gt;"-1"&lt;/button&gt;                &lt;span&gt;"Value: " {value} "!"&lt;/span&gt;                &lt;button&gt;"+1"&lt;/button&gt;            &lt;/div&gt;        }        // Leptos supports multiple back-end renderers for HTML elements        // Here, .into_view() is just a convenient way to specify "use the regular DOM renderer"        .into_view()        // Views are lazy—they describe a DOM tree but have not yet created it        // Calling .build() will actually construct the DOM elements        .build()        // .build() returns an ElementState, which is a smart pointer to the DOM element.        // So we can still call .outer_html(), which accesses outerHTML on the actual DOM element        .outer_html()    });    // There is actually a simpler way...    // We can directly compare with &lt;SimpleCounter initial_value=0&gt;    assert_eq!(test_wrapper.inner_html(), {        let comparison_wrapper = document.create_element("section").unwrap();        let _dispose = mount_to(            comparison_wrapper.clone().unchecked_into(),            || view! { &lt;SimpleCounter initial_value=0 step=1/&gt;},        );        comparison_wrapper.inner_html()    });}

Using Playwright in the <span>counters</span> example

These tests use the popular JavaScript testing tool Playwright to run end-to-end tests on the same example, utilizing libraries and testing methods familiar to many with frontend development experience.

Example Test

test.describe("Increment Count", () =&gt; {  test("should increase the total count", async ({ page }) =&gt; {    const ui = new CountersPage(page);    await ui.goto();    await ui.addCounter();    await ui.incrementCount();    await ui.incrementCount();    await ui.incrementCount();    await expect(ui.total).toHaveText("3");  });});

Using Gherkin/Cucumber Testing in the <span>todo_app_sqlite</span> example

You can integrate any testing tools you like into this process. This example uses Cucumber, a natural language-based testing framework.

@add_todo功能: 添加待办事项
    背景:        前提我看到应用
    @add_todo-see    场景: 应该看到待办事项        前提我将待办事项设置为 Buy Bread        当我点击添加按钮        那么我看到名为 Buy Bread 的待办事项
    # @allow.skipped    @add_todo-style    场景: 应该看到待处理的待办事项        当我添加一个名为 Buy Oranges 的待办事项        那么我看到待处理的待办事项

The definitions of these actions are implemented in Rust code.

use crate::fixtures::{action, world::AppWorld};use anyhow::{Ok, Result};use cucumber::{given, when};#[given("I see the app")]#[when("I open the app")]async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {    let client = &world.client;    action::goto_path(client, "").await?;    Ok(())}#[given(regex = "^I add a todo as (.*)$")]#[when(regex = "^I add a todo as (.*)$")]async fn i_add_a_todo_titled(world: &mut AppWorld, text: String) -> Result<()> {    let client = &world.client;    action::add_todo(client, text.as_str()).await?;    Ok(())}// etc.

Learn More

Feel free to check the CI setup in the Leptos repository to see how to use these tools in your own applications. All of these testing methods are regularly run against actual Leptos example applications.

Leave a Comment