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()。显式等待更可靠、更快速。
- 每个测试方法只测试一个用户旅程——端到端测试成本较高。让每个测试覆盖有意义的场景,但保持彼此独立。