端到端 UI 测试

端到端 UI 测试模拟真实用户在 Web 浏览器中的交互,验证整个应用程序工作流程。这种测试的运行速度快,并容易多次执行,与手动测试相比更节省时间。

在 Jmix 中,我们可以使用 Masquerade 对应用程序进行端到端 UI 测试。该测试库可对 Jmix UI 的所有部分使用页面对象模式(Page Object pattern),包括视图、组件、对话框、通知以及复合组件。基于 Selenium WebDriverSelenide

Masquerade 仅能用于 Jmix 2.6+ 的应用程序。

安装 Masquerade

请按以下步骤安装 Masquerade:

  1. 确保项目使用的是 Jmix 2.6+ 版本。如果不是,请参阅 项目升级 部分先升级依赖的 Jmix 版本。

  2. build.gradle 文件中添加 testImplementation

    build.gradle
    testImplementation 'io.jmix.masquerade:jmix-masquerade'

使用 Selenide

Masquerade 是基于 Selenide 的,因此可以使用 Selenide 的任何方法。 例如在自动化测试时需要登录:

public class SelenideTest {

    @Test
    void selenideLogin() {
        Selenide.open("/");

        $(byId("vaadinLoginUsername")).shouldHave(value("admin"));
        $(byChained(byId("vaadinLoginUsername"), byTagName("input")))
                .setValue("")
                .setValue("admin");

        $(byId("vaadinLoginPassword")).shouldHave(value("admin"));
        $(byChained(byId("vaadinLoginPassword"), byTagName("input")))
                .setValue("")
                .setValue("admin");

        $(byCssSelector("[slot='submit']")).click();
    }
}

标准的 Selenide.$ 方法(比如,byId)需要传入一个 CSS 选择器作为参数。然后返回一个 SelenideElement 对象,这个对象可以进一步操作。

关于这些方法,请参阅 Selenide API

用 Masquerade 创建测试用例

Masquerade 强调对 Jmix 的各种组件使用 包装器,以便更好地组织和管理测试。我们创建一个使用 Masquerade 登录 Jmix 应用程序的用例。有两个步骤:

1. 创建一个视图包装器

一个基本的 Jmix 项目在创建时就带有登录视图。创建一个 视图包装器 表示该视图及其被测组件:

  • src/test/java 目录的 com.company.testproject 包内,创建一个新包,名为 view。然后,创建一个名为 LoginView 的 Java 类:

    TestProject/
    └── src/
        ├── main/
        └── test/
            └── java/
                └── com.company.testproject
                    └── test_support
                        └── view
                           └── LoginView.java
  • 为每个需要测试的组件添加 组件包装器 及其 get 方法:

    LoginView.java
    @TestView
    public class LoginView extends View<LoginView> {
    
        @FindBy(css = "[slot='submit']")
        private Button button;
    
        @FindBy(id = "vaadinLoginUsername")
        private TextField username;
    
        @FindBy(id = "vaadinLoginPassword")
        private PasswordField password;
    
        public Button getButton() {
            return button;
        }
    
        public TextField getUsernameField() {
            return username;
        }
    
        public PasswordField getPasswordField() {
            return password;
        }
    }

2. 创建测试类

测试类在登录场景中需要调用包装器的方法。请按照下面的步骤创建:

  • src/test/java 目录的 com.company.testproject 包下,创建一个名为 ui_autotest 的新包。然后创建一个名为 LoginUiTest 的类:

    TestProject/
    └── src/
        ├── main/
        └── test/
            └── java/
                ├── com.company.testproject
                │   └── test_support
                │       └── view
                │          └── LoginView.java
                └────── ui_autotest
                        └── LoginUiTest.java
  • 定义测试用例中的操作序列:

    public class LoginUiTest {
    
        @Test
        public void loginAsAdmin() {
    
            Selenide.open("/"); (1)
    
            LoginView loginView = $j(LoginView.class); (2)
    
            loginView.getUsernameField()
                    .shouldHave(value("admin"))
                    .setValue("")
                    .setValue("admin");
    
            loginView.getPasswordField()
                    .shouldHave(value("admin"))
                    .setValue("")
                    .setValue("admin");
    
            loginView.getButton()
                    .shouldHave(text("Log in"))
                    .click();
        }
    
    }
    1 使用 Selenide 的标准方法打开登录页。
    2 使用 Masquerade.$j 方法选择视图包装器并调用其方法。

Jmix 测试 ID

Masquerade 还可以帮助为每个通过 UiComponents 工厂创建的组件生成一个特殊的 j-test-id(Jmix 测试 id)。 这种 id 使得在页面中识别元素更加容易。如需启用该功能,请设置 jmix.ui.ui-test-modetrue

application.properties
jmix.ui.ui-test-mode = true
生成 ID 过程可能会影响性能。因此,建议只在应用程序的测试 profile 中使用该参数。

默认的测试 id 值与组件的 id 一致:

masquerade j test id

如果组件没有 id,则会基于其数据绑定、action or text 属性生成。

通过 j-test-id 操作组件时,可以使用 Masquerade.$j 方法:

$j("myButton").click();

包装器

包装器是表示 Jmix UI 中不同部分并封装与它们交互的类。可帮助将测试场景与被测 UI 的复杂度进行分离。包装器有五种类型。

视图包装器

视图包装器封装了一个视图,提供简化且一致的接口来与其组件进行交互。视图包装器不必包含相应视图的所有组件 - ,而只需添加测试所需的组件。

视图包装器的一个简单示例:

@TestView(id = "MyView") (1)
public class MyView extends View<MyView> { (2)

    @TestComponent
    private EntityComboBox entityComboBox;

    @TestComponent(path = "myButton")
    private Button button;

    @FindBy(xpath = "//vaadin-text-area[@class='my-text-area']")
    private TextArea textArea;

    public EntityComboBox getEntityComboBox() {
        return entityComboBox;
    }

    public Button getButton() {
        return button;
    }
}
1 视图包装器需要带 io.jmix.masquerade.TestView 注解。id 值为视图 @ViewController 注解中指定的 视图 ID:
@Route(value = "my-test-view", layout = MainView.class)
@ViewController(id = "MyView")
@ViewDescriptor(path = "my-test-view.xml")
public class MyView extends StandardView {

}

如果没有设置 ID,则使用视图的类名作为 id。

2 继承 io.jmix.masquerade.sys.View 类,泛型设置为包装器类本身。以便在编写测试用例时提供流式 API。

组件包装器

组件包装器封装了视图包装器中的组件。组件包装器类属性需要使用 @TestComponent@FindBy 注解。参考以下示例:

@TestComponent
private EntityComboBox entityComboBox;

@TestComponent(path = "myButton")
private Button button;

@FindBy(xpath = "//vaadin-text-area[@class='my-text-area']")
private TextArea textArea;
  • @TestComponent 表示该字段是一个组件包装器。如果没有指定 path 的值,则默认使用对应 web 元素的 j-test-id 属性值。

  • @FindBy 指定该字段使用的 CSS 选择器。

所有可用的组件包装器在 io.jmix.masquerade.component 包中可查询。

组件包装器提供与组件交互的多种方法。例如,可以打开 EntityCombobox 的选项弹窗或对话框窗口:

@Test
public void testEntityComboBox() {
    EntityComboBox entityComboBox = openMyView().getEntityComboBox();

    entityComboBox.shouldHave(label("EntityComboBox"))
            .setValue("[admin]")
            .shouldHave(value("[admin]"))
            .clickItemsOverlay()
            .shouldHave(visibleItems("[admin]", "[test]", "[test1]"))
            .shouldHave(visibleItemsCount(3))
            .shouldHave(visibleItemsContains("[test]"));

    sleep(3000);

    entityComboBox.getItemsOverlay()
            .select("[test]");
    sleep(3000);

    entityComboBox.shouldHave(value("[test]"))
            .triggerActionWithView(UserListDialog.class, HasActions.LOOKUP)
            .selectAdmin();

    sleep(3000);
    entityComboBox.shouldHave(value("[admin]"));
}

组件包装器还提供了特殊的条件用于检查组件的状态。这些条件可以在 io.jmix.masquerade.condition 包中查询。

对话框窗口包装器

对话框(DialogWindow)窗口包装器与视图包装器类似,但继承自 io.jmix.masquerade.sys.DialogWindow。这个类提供对话框的特殊操作,例如关闭对话框或检查标题。示例:

@TestView(id = "User.list")
public class UserListDialog extends DialogWindow<UserListDialog> {

    public UserListDialog selectAdmin() {
        $(By.xpath("//*[@id=\"usersDataGrid\"]/vaadin-grid-cell-content[22]"))
                .click();

        $(byChained(getBy(), byUiTestId("selectButton")))
                .shouldBe(VISIBLE)
                .shouldBe(ENABLED)
                .click();
        return this;
    }
}

通知包装器

通知(Notification)包装器是一个特殊类型的组件包装器。示例:

@Test
public void notificationTest() {
    MainView mainView = loginAsAdmin();

    UserListView userListView = mainView.openItem(UserListView.class,
            "applicationListItem", "user.listListItem");

    userListView.showUsername();

    Notification notification = $j(Notification.class);
    notification
            .shouldBe(VISIBLE)
            .shouldHave(notificationPosition(Notification.Position.BOTTOM_END))
            .shouldHave(notificationTheme(Notification.Theme.SUCCESS))
            .shouldHave(notificationTitle("Username:"))
            .should(notificationTitleContains("name:"))
            .shouldHave(notificationMessage("test"))
            .should(notificationMessageContains("te"));

    sleep(3000);

    notification.shouldNotBe(EXIST);
}

如果打开了多个通知,可以用 xpath 选择所需要的:

$j(Notification.class, xpath("{xpath-to-notification}"));

复合包装器

复合(Composite)包装器封装了视图的一部分,也称为 fragment。访问 fragment 的方式依据其是否存在于视图包装器而不同。

假设我们有一个视图,由两个 fragment 组成,但是只有一个添加到了视图包装器中:

@TestView
public class FragmentsView extends View<FragmentsView> {

    @TestComponent
    private TestFragment1 testFragment1Root;

    public TestFragment1 getTestFragment1() {
        return testFragment1Root;
    }
}

复合包装器如下:

public class TestFragment1 extends Composite<TestFragment1> { (1)

    @TestComponent
    private TextField testFragment1TextField;

    public TextField getTestField() {
        return testFragment1TextField;
    }
}
@TestComponent(path = {"FragmentsView", "testFragment2Root"}) (2)
public class TestFragment2 extends Composite<TestFragment2> {

    @TestComponent
    private TextField testFragment2TextField;

    public TextField getTestField() {
        return testFragment2TextField;
    }
}
1 复合包装器必须继承自 io.jmix.masquerade.sys.Composite
2 复合包装器可以选择性的带上 @TestComponent 注解,以提供 Masquerade.$j 所需要的路径值。

在视图包装器访问 fragment 时,与组件一样使用:

FragmentsView fragmentsView = openFragmentsView();

fragmentsView.getTestFragment1()
        .getTestField()
        .shouldHave(value(""))
        .setValue("Fragment_1")
        .shouldHave(value("Fragment_1"));

测试第二个 fragment 时,由于不存在于视图控制器中,只能通过 Masquerade.$j 选取:

FragmentsView fragmentsView = openFragmentsView();

$j(TestFragment2.class)
        .getTestField()
        .shouldHave(value(""))
        .setValue("Fragment_2")
        .shouldHave(value("Fragment_2"));