testbench-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

End-to-End Browser Testing with Vaadin TestBench

使用Vaadin TestBench进行端到端浏览器测试

Use the Vaadin MCP tools (
search_vaadin_docs
) to look up the latest documentation whenever uncertain about a specific API detail. Always set
vaadin_version
to
"25"
and
ui_language
to
"java"
.
Note: TestBench requires a commercial Vaadin subscription.
当不确定特定API细节时,请使用Vaadin MCP工具(
search_vaadin_docs
)查阅最新文档。请始终将
vaadin_version
设置为
"25"
ui_language
设置为
"java"
注意: TestBench需要Vaadin商业订阅。

What TestBench Is

TestBench是什么

TestBench runs your Vaadin application in a real browser (Chrome, Firefox, etc.) and lets you write Java tests that interact with it like a user would. It's built on Selenium but provides a high-level API specifically designed for Vaadin components.
TestBench会在真实浏览器(Chrome、Firefox等)中运行你的Vaadin应用,并允许你编写Java测试,模拟用户操作与应用交互。它基于Selenium构建,但提供了专为Vaadin组件设计的高级API。

When to Use TestBench (vs. UI Unit Tests)

何时使用TestBench(对比UI单元测试)

Use end-to-end TestBench tests for:
  • Critical user paths — login, checkout, payment flows
  • Client-side behavior — JavaScript functionality, custom web components
  • Visual regression — screenshot comparison to catch unintended UI changes
  • Cross-browser testing — verify behavior across Chrome, Firefox, Safari
  • Integration with external systems — SSO, OAuth flows
For everything else, prefer UI unit tests (see the
ui-unit-testing
skill) — they're faster and less flaky.
以下场景适合使用TestBench端到端测试:
  • 关键用户路径 — 登录、结账、支付流程
  • 客户端行为 — JavaScript功能、自定义Web组件
  • 可视化回归 — 通过截图对比捕获意外的UI变更
  • 跨浏览器测试 — 在Chrome、Firefox、Safari等浏览器中验证行为一致性
  • 与外部系统集成 — SSO、OAuth流程
其他场景优先选择UI单元测试(参考
ui-unit-testing
技能)——这类测试速度更快且更稳定。

Core Concepts

核心概念

Elements

Elements(元素)

An
Element
class represents a DOM element — either a built-in HTML element (
<div>
,
<span>
) or a Vaadin web component (
<vaadin-button>
,
<vaadin-grid>
). Elements provide component-specific methods for interaction.
Every Vaadin component has a corresponding Element class:
ButtonElement
,
TextFieldElement
,
GridElement
,
ComboBoxElement
, etc.
Element
类代表一个DOM元素——可以是内置HTML元素(
<div>
<span>
)或Vaadin Web组件(
<vaadin-button>
<vaadin-grid>
)。Elements提供了组件专属的交互方法。
每个Vaadin组件都有对应的Element类:
ButtonElement
TextFieldElement
GridElement
ComboBoxElement
等。

ElementQuery

ElementQuery(元素查询)

ElementQuery
finds elements on the page. Use the
$()
method:
java
// Find by type
ButtonElement button = $(ButtonElement.class).first();

// Find by ID
TextFieldElement name = $(TextFieldElement.class).id("name");

// Find all buttons
List<ButtonElement> buttons = $(ButtonElement.class).all();

// Wait for element to appear
ButtonElement btn = $(ButtonElement.class).waitForFirst();

// Find by attribute
$(DivElement.class).attribute("class", "active").first();

// Nested query — find inside another element
VerticalLayoutElement layout = $(VerticalLayoutElement.class).id("content");
ButtonElement innerBtn = layout.$(ButtonElement.class).first();
Key methods on ElementQuery:
  • id("id")
    — find by id (returns single element)
  • first()
    — first match
  • last()
    — last match
  • get(n)
    — nth match
  • all()
    — all matches as list
  • exists()
    — boolean check
  • waitForFirst()
    — waits until a match appears
  • attribute("name", "value")
    — filter by attribute
ElementQuery
用于查找页面上的元素。使用
$()
方法:
java
// 按类型查找
ButtonElement button = $(ButtonElement.class).first();

// 按ID查找
TextFieldElement name = $(TextFieldElement.class).id("name");

// 查找所有按钮
List<ButtonElement> buttons = $(ButtonElement.class).all();

// 等待元素出现
ButtonElement btn = $(ButtonElement.class).waitForFirst();

// 按属性查找
$(DivElement.class).attribute("class", "active").first();

// 嵌套查询——在另一个元素内查找
VerticalLayoutElement layout = $(VerticalLayoutElement.class).id("content");
ButtonElement innerBtn = layout.$(ButtonElement.class).first();
ElementQuery的核心方法:
  • id("id")
    — 按ID查找(返回单个元素)
  • first()
    — 返回第一个匹配元素
  • last()
    — 返回最后一个匹配元素
  • get(n)
    — 返回第n个匹配元素
  • all()
    — 返回所有匹配元素的列表
  • exists()
    — 检查元素是否存在(返回布尔值)
  • waitForFirst()
    — 等待直到匹配元素出现
  • attribute("name", "value")
    — 按属性过滤元素

Writing Tests

编写测试用例

Basic test (JUnit 5)

基础测试(JUnit 5)

java
public class LoginTest extends BrowserTestBase {

    @BrowserTest
    public void loginWithValidCredentials() {
        $(TextFieldElement.class).id("username").setValue("admin");
        $(PasswordFieldElement.class).id("password").setValue("secret");
        $(ButtonElement.class).id("login").click();

        // Verify navigation to dashboard
        assertTrue($(DivElement.class).id("dashboard").exists());
    }
}
java
public class LoginTest extends BrowserTestBase {

    @BrowserTest
    public void loginWithValidCredentials() {
        $(TextFieldElement.class).id("username").setValue("admin");
        $(PasswordFieldElement.class).id("password").setValue("secret");
        $(ButtonElement.class).id("login").click();

        // 验证是否导航到仪表盘
        assertTrue($(DivElement.class).id("dashboard").exists());
    }
}

JUnit 4 test

JUnit 4测试用例

java
public class LoginTest extends TestBenchTestCase {

    @Before
    public void setup() throws Exception {
        setDriver(new ChromeDriver());
        getDriver().get("http://localhost:8080");
    }

    @Test
    public void loginWithValidCredentials() {
        $(TextFieldElement.class).id("username").setValue("admin");
        $(PasswordFieldElement.class).id("password").setValue("secret");
        $(ButtonElement.class).id("login").click();

        assertTrue($(DivElement.class).id("dashboard").exists());
    }

    @After
    public void teardown() {
        getDriver().quit();
    }
}
java
public class LoginTest extends TestBenchTestCase {

    @Before
    public void setup() throws Exception {
        setDriver(new ChromeDriver());
        getDriver().get("http://localhost:8080");
    }

    @Test
    public void loginWithValidCredentials() {
        $(TextFieldElement.class).id("username").setValue("admin");
        $(PasswordFieldElement.class).id("password").setValue("secret");
        $(ButtonElement.class).id("login").click();

        assertTrue($(DivElement.class).id("dashboard").exists());
    }

    @After
    public void teardown() {
        getDriver().quit();
    }
}

Page Objects

Page Object(页面对象)

Page objects encapsulate interaction with a specific view or component, keeping test methods clean and maintainable. If the UI changes, only the page object needs updating — not every test.
页面对象封装了与特定视图或组件的交互逻辑,让测试方法更简洁且易于维护。当UI发生变更时,只需更新页面对象,无需修改所有测试用例。

Creating a page object

创建页面对象

A page object extends
TestBenchElement
and uses
@Element("tag-name")
:
java
@Element("div")
@Attribute(name = "class", contains = "login-view")
public class LoginViewElement extends TestBenchElement {

    public void login(String username, String password) {
        $(TextFieldElement.class).id("username").setValue(username);
        $(PasswordFieldElement.class).id("password").setValue(password);
        $(ButtonElement.class).id("login").click();
    }

    public boolean isLoginFailed() {
        return $(DivElement.class).attribute("class", "error").exists();
    }
}
页面对象需继承
TestBenchElement
并使用
@Element("tag-name")
注解:
java
@Element("div")
@Attribute(name = "class", contains = "login-view")
public class LoginViewElement extends TestBenchElement {

    public void login(String username, String password) {
        $(TextFieldElement.class).id("username").setValue(username);
        $(PasswordFieldElement.class).id("password").setValue(password);
        $(ButtonElement.class).id("login").click();
    }

    public boolean isLoginFailed() {
        return $(DivElement.class).attribute("class", "error").exists();
    }
}

Using the page object in tests

在测试中使用页面对象

java
@BrowserTest
public void loginSuccess() {
    LoginViewElement loginView = $(LoginViewElement.class).waitForFirst();
    loginView.login("admin", "secret");

    assertTrue($(DashboardViewElement.class).exists());
}

@BrowserTest
public void loginFailure() {
    LoginViewElement loginView = $(LoginViewElement.class).waitForFirst();
    loginView.login("admin", "wrong");

    assertTrue(loginView.isLoginFailed());
}
java
@BrowserTest
public void loginSuccess() {
    LoginViewElement loginView = $(LoginViewElement.class).waitForFirst();
    loginView.login("admin", "secret");

    assertTrue($(DashboardViewElement.class).exists());
}

@BrowserTest
public void loginFailure() {
    LoginViewElement loginView = $(LoginViewElement.class).waitForFirst();
    loginView.login("admin", "wrong");

    assertTrue(loginView.isLoginFailed());
}

Matching strategies

匹配策略

Use
@Attribute
to match page objects to DOM elements:
java
// Match by class attribute (contains for multi-value attributes)
@Element("div")
@Attribute(name = "class", contains = "my-view")

// Auto-match by simple class name (removes Element/PageObject suffix)
@Element("div")
@Attribute(name = "class", contains = Attribute.SIMPLE_CLASS_NAME)
使用
@Attribute
注解将页面对象与DOM元素匹配:
java
// 按class属性匹配(contains适用于多值属性)
@Element("div")
@Attribute(name = "class", contains = "my-view")

// 按简单类名自动匹配(移除Element/PageObject后缀)
@Element("div")
@Attribute(name = "class", contains = Attribute.SIMPLE_CLASS_NAME)

Working with Complex Components

处理复杂组件

Grid

Grid(表格)

java
GridElement grid = $(GridElement.class).first();

// Get row count
int rowCount = grid.getRowCount();

// Get cell content
String name = grid.getCell(0, 0).getText();

// Click a row
grid.getRow(0).click();

// Scroll to a row
grid.scrollToRow(50);
java
GridElement grid = $(GridElement.class).first();

// 获取行数
int rowCount = grid.getRowCount();

// 获取单元格内容
String name = grid.getCell(0, 0).getText();

// 点击某一行
grid.getRow(0).click();

// 滚动到指定行
grid.scrollToRow(50);

ComboBox

ComboBox(下拉框)

java
ComboBoxElement combo = $(ComboBoxElement.class).id("country");
combo.openPopup();
combo.selectByText("Finland");
java
ComboBoxElement combo = $(ComboBoxElement.class).id("country");
combo.openPopup();
combo.selectByText("Finland");

Dialog

Dialog(对话框)

java
// Dialogs overlay the main content
DialogElement dialog = $(DialogElement.class).waitForFirst();
dialog.$(ButtonElement.class).id("confirm").click();
java
// 对话框会覆盖主内容
DialogElement dialog = $(DialogElement.class).waitForFirst();
dialog.$(ButtonElement.class).id("confirm").click();

Best Practices

最佳实践

  1. Use Page Objects for all but the simplest tests — they make tests readable and maintainable. Your test methods should read like a user story.
  2. Use
    waitForFirst()
    instead of
    first()
    — when elements might not be immediately present (after navigation, async loading).
  3. Assign IDs to key components
    component.setId("login-button")
    makes them easy to find in tests. Prefer IDs over positional queries.
  4. Keep end-to-end tests focused on critical paths — use UI unit tests for comprehensive coverage. End-to-end tests should verify the journey, not every edge case.
  5. Run tests in CI with headless browsers — Chrome headless is the most reliable option.
  6. Don't sleep, wait — use
    waitForFirst()
    or
    waitUntil()
    instead of
    Thread.sleep()
    . Explicit waits are more reliable and faster.
  7. Test one user journey per test method — end-to-end tests are expensive. Make each test cover a meaningful scenario, but keep them independent.
  1. 除最简单的测试外,均使用Page Object — 它们让测试用例更易读且易于维护。测试方法的逻辑应像用户故事一样清晰。
  2. 使用
    waitForFirst()
    替代
    first()
    — 当元素可能不会立即出现时(如导航后、异步加载时)。
  3. 为关键组件分配ID
    component.setId("login-button")
    能让测试更易定位元素。优先使用ID而非位置查询。
  4. 端到端测试聚焦关键路径 — 全面测试请使用UI单元测试。端到端测试应验证完整用户流程,而非每个边缘场景。
  5. 在CI中使用无头浏览器运行测试 — Chrome无头模式是最可靠的选择。
  6. 不要用sleep,改用等待方法 — 使用
    waitForFirst()
    waitUntil()
    替代
    Thread.sleep()
    。显式等待更可靠且效率更高。
  7. 每个测试方法验证一个用户流程 — 端到端测试成本较高。每个测试应覆盖一个有意义的场景,且保持测试之间的独立性。