Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

测试你的组件

Testing Your Components

测试用户界面可能相对棘手,但非常重要。本文将讨论测试 Leptos 应用的几个原则和方法。

Testing user interfaces can be relatively tricky, but really important. This article will discuss a couple principles and approaches for testing a Leptos app.

1. 使用普通的 Rust 测试来测试业务逻辑

1. Test business logic with ordinary Rust tests

在许多情况下,将逻辑从组件中抽离出来并单独测试是有意义的。对于一些简单的组件,可能没有特别的逻辑需要测试,但对于许多组件来说,使用一个可测试的包装类型并在普通的 Rust impl 块中实现逻辑是值得的。

In many cases, it makes sense to pull the logic out of your components and test it separately. For some simple components, there’s no particular logic to test, but for many it’s worth using a testable wrapping type and implementing the logic in ordinary Rust impl blocks.

例如,与其像这样直接在组件中嵌入逻辑:

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

#[component]
pub fn TodoApp() -> impl IntoView {
    let (todos, set_todos) = signal(vec![Todo { /* ... */ }]);
    // ⚠️ 这样做很难测试,因为它嵌入在组件中
    // ⚠️ this is hard to test because it's embedded in the component
    let num_remaining = move || todos.read().iter().filter(|todo| !todo.completed).sum();
}

你可以将该逻辑抽离到独立的数据结构中并进行测试:

You could pull that logic out 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 test associated with it
    let num_remaining = move || todos.read().num_remaining();
}

通常,封装在组件本身的逻辑越少,你的代码就会感觉越地道(idiomatic),测试起来也就越容易。

In general, the less of your logic is wrapped into your components themselves, the more idiomatic your code will feel and the easier it will be to test.

2. 使用端到端 (e2e) 测试来测试组件

2. Test components with end-to-end (e2e) testing

我们的 examples 目录下有几个使用了不同测试工具进行广泛端到端测试的示例。

Our examples directory has several examples with extensive end-to-end testing, using different testing tools.

了解如何使用这些工具的最简单方法是查看测试示例本身:

The easiest way to see how to use these is to take a look at the test examples themselves:

配合 counter 使用 wasm-bindgen-test

wasm-bindgen-test with counter

这是一个相当简单的手动测试设置,使用了 wasm-pack test 命令。

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

测试示例

Sample 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);

    // 首先渲染我们的计数器并将其挂载到 DOM
    // 注意我们从初始值 10 开始
    // start by rendering our counter and mounting it to the DOM
    // note that we start at the initial value of 10
    let _dispose = mount_to(
        test_wrapper.clone().unchecked_into(),
        || view! { <SimpleCounter initial_value=10 step=1/> },
    );

    // 现在我们通过遍历 DOM 提取按钮
    // 如果它们有 ID,这一步会更容易
    // now we extract the buttons by iterating over the DOM
    // this 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::<web_sys::HtmlElement>();

    // 现在让我们点击 `clear` 按钮
    // now let's click the `clear` button
    clear.click();

    // 响应式系统建立在异步系统之上,因此更改不会同步反映在 DOM 中。
    // 为了在此检测更改,我们将在每次更改后简短地 yield,允许更新视图的副作用运行。
    // the reactive system is built on top of the async system, so changes are not reflected
    // synchronously in the DOM
    // in order to detect the changes here, we'll just yield for a brief time after each change,
    // allowing the effects that update the view to run
    tick().await;

    // 现在让我们根据预期值测试 <div>
    // 我们可以通过测试其 `outerHTML` 来实现
    // now let's test the <div> against the expected value
    // we can do this by testing its `outerHTML`
    assert_eq!(div.outer_html(), {
        // 就像我们用值 0 创建它一样,对吧?
        // it's as if we're creating it with a value of 0, right?
        let (value, _set_value) = signal(0);

        // 我们可以移除事件监听器,因为它们不会渲染到 HTML
        // we can remove the event listeners because they're not rendered to HTML
        view! {
            <div>
                <button>"Clear"</button>
                <button>"-1"</button>
                <span>"Value: " {value} "!"</span>
                <button>"+1"</button>
            </div>
        }
        // Leptos 支持多种用于 HTML 元素的后端渲染器
        // 这里的 .into_view() 只是指定“使用常规 DOM 渲染器”的一种便捷方式
        // Leptos supports multiple backend renderers for HTML elements
        // .into_view() here is just a convenient way of specifying "use the regular DOM renderer"
        .into_view()
        // 视图是惰性的 —— 它们描述了一个 DOM 树,但还没有创建它
        // 调用 .build() 将实际构建 DOM 元素
        // views are lazy -- they describe a DOM tree but don't create it yet
        // calling .build() will actually build the DOM elements
        .build()
        // .build() 返回一个 ElementState,它是 DOM 元素的智能指针。
        // 所以我们仍然可以调用 .outer_html(),它访问实际 DOM 元素上的 outerHTML。
        // .build() returned an ElementState, which is a smart pointer for
        // a DOM element. So we can still just call .outer_html(), which access the outerHTML on
        // the actual DOM element
        .outer_html()
    });

    // 实际上有一种更简单的方法来做到这一点……
    // 我们可以直接针对初始值为 0 的 <SimpleCounter/> 进行测试
    // There's actually an easier way to do this...
    // We can just test against a <SimpleCounter/> with the initial value 0
    assert_eq!(test_wrapper.inner_html(), {
        let comparison_wrapper = document.create_element("section").unwrap();
        let _dispose = mount_to(
            comparison_wrapper.clone().unchecked_into(),
            || view! { <SimpleCounter initial_value=0 step=1/>},
        );
        comparison_wrapper.inner_html()
    });
}

配合 counters 使用 Playwright

Playwright with counters

这些测试使用了常见的 JavaScript 测试工具 Playwright,对同一个示例运行端到端测试。这种库和测试方法对于许多做过前端开发的人来说非常熟悉。

These tests use the common JavaScript testing tool Playwright to run end-to-end tests on the same example, using a library and testing approach familiar to many who have done frontend development before.

测试示例

Sample Test

test.describe("Increment Count", () => {
  test("should increase the total count", async ({ page }) => {
    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");
  });
});

配合 todo_app_sqlite 使用 Gherkin/Cucumber 测试

Gherkin/Cucumber Tests with todo_app_sqlite

你可以将任何你喜欢的测试工具集成到这个流程中。此示例使用了 Cucumber,这是一个基于自然语言的测试框架。

You can integrate any testing tool you’d like into this flow. This example uses Cucumber, a testing framework based on natural language.

@add_todo
Feature: Add Todo

    Background:
        Given I see the app

    @add_todo-see
    Scenario: Should see the todo
        Given I set the todo as Buy Bread
        When I click the Add button
        Then I see the todo named Buy Bread

    # @allow.skipped
    @add_todo-style
    Scenario: Should see the pending todo
        When I add a todo as Buy Oranges
        Then I see the pending todo

这些操作的定义在 Rust 代码中。

The definitions for these actions are defined 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.

了解更多

Learning More

欢迎查看 Leptos 仓库中的 CI 设置,以进一步了解如何在自己的应用中使用这些工具。所有这些测试方法都会定期针对实际的 Leptos 示例应用运行。

Feel free to check out the CI setup in the Leptos repo to learn more about how to use these tools in your own application. All of these testing methods are run regularly against actual Leptos example apps.