SuperAPI AI 工具集

testbench-testing

测试与质量

查看项目

更新于 10-16 06:26

技能介绍

Vaadin TestBench 端到端浏览器测试

用途

TestBench 在真实浏览器(Chrome、Firefox 等)中运行 Vaadin 应用,允许编写 Java 测试,像用户一样与应用交互。它构建于 Selenium 之上,提供专为 Vaadin 组件设计的高层 API。当用户要求编写端到端测试或针对 Vaadin 25 的浏览器测试时使用该技能。若不确定具体 API 细节,可使用 Vaadin MCP 工具(search_vaadin_docs)查阅最新文档,并将 vaadin_version 设为 "25"、ui_language 设为 "java"。

注意:TestBench 需要商业版 Vaadin 订阅。

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

端到端 TestBench 测试适用于:

  • 关键用户路径——登录、结账、支付流程
  • 客户端行为——JavaScript 功能、自定义 Web 组件
  • 视觉回归——通过截图对比捕获非预期的 UI 变化
  • 跨浏览器测试——验证在 Chrome、Firefox、Safari 中的行为
  • 与外部系统集成——SSO、OAuth 流程

其他场景优先使用 UI 单元测试(参见 ui-unit-testing 技能)——它们更快且更少不稳定。

核心概念

Elements

Element 类表示一个 DOM 元素,可以是内置 HTML 元素(如 <div>、<span>)或 Vaadin Web 组件(如 <vaadin-button>、<vaadin-grid>)。Element 提供组件特定的交互方法。每个 Vaadin 组件都有对应的 Element 类:ButtonElement、TextFieldElement、GridElement、ComboBoxElement 等。

ElementQuery

ElementQuery 用于查找页面上的元素,使用 $() 方法:

// 按类型查找
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")——按属性过滤

编写测试

基本测试(JUnit 5)

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 测试

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 封装对特定视图或组件的交互,使测试方法保持简洁且可维护。若 UI 发生变化,只需更新 Page Object,而不必修改每个测试。

创建 Page Object

Page Object 继承 TestBenchElement 并使用 @Element("tag-name"):

@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();
    }
}

在测试中使用 Page Object

@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());
}

匹配策略

使用 @Attribute 将 Page Object 匹配到 DOM 元素:

// 按 class 属性匹配(contains 适用于多值属性)
@Element("div")
@Attribute(name = "class", contains = "my-view")

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

处理复杂组件

Grid

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

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

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

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

// 滚动到某一行
grid.scrollToRow(50);

ComboBox

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

Dialog

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

最佳实践

  • 除最简单的测试外,均使用 Page Objects——让测试可读且可维护,测试方法应读起来像用户故事。
  • 使用 waitForFirst() 而非 first()——当元素可能不会立即出现时(如导航后、异步加载)。
  • 为关键组件分配 ID——component.setId("login-button") 使其在测试中易于查找。优先使用 ID 而非位置查询。
  • 让端到端测试聚焦于关键路径——使用 UI 单元测试进行全量覆盖,端到端测试用于验证用户旅程,而非每个边界情况。
  • 在 CI 中使用无头浏览器运行测试——Chrome headless 是最可靠的选择。
  • 不要 sleep,要等待——使用 waitForFirst() 或 waitUntil() 替代 Thread.sleep()。显式等待更可靠、更快速。
  • 每个测试方法只测试一个用户旅程——端到端测试成本较高。让每个测试覆盖有意义的场景,但保持彼此独立。