Testing with Selenium/WebDriver and the Page Object Pattern – 用Selenium/WebDriver和页面对象模式测试

最后修改: 2017年 6月 19日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

In this article, we’re going to build on the previous writeup and continue to improve our Selenium/WebDriver testing by introducing the Page Object pattern.

在本文中,我们将在前文的基础上,通过引入页面对象模式,继续改进我们的Selenium/WebDriver测试。

2. Adding Selenium

2.添加硒元素

Let’s add a new dependency to our project to write simpler, more readable assertions:

让我们给我们的项目添加一个新的依赖关系,以编写更简单、更可读的断言。

<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
</dependency>

The latest version can be found in the Maven Central Repository.

最新版本可以在Maven中央仓库中找到。

2.1. Additional Methods

2.1.其他方法

In the first part of the series, we used a few additional utility methods which we’re going to be using here as well.

在该系列的第一部分,我们使用了一些额外的实用方法,我们在这里也将使用这些方法。

We’ll start with the navigateTo(String url) method – which will help us navigate through different pages of the application:

我们将从navigateTo(String url) 方法开始–它将帮助我们在应用程序的不同页面中导航。

public void navigateTo(String url) {
    driver.navigate().to(url);
}

Then, the clickElement(WebElement element) – as the name implies – will take care of performing the click action on a specified element:

然后,clickElement(WebElement element)–顾名思义–将负责对一个指定的元素执行点击动作。

public void clickElement(WebElement element) {
    element.click();
}

3. Page Object Pattern

3.页面对象模式

Selenium gives us a lot of powerful, low-level APIs we can use to interact with the HTML page.

Selenium为我们提供了许多强大的、低级别的API,我们可以用来与HTML页面互动。

However, as the complexity of our tests grows, interacting with the low-level, raw elements of the DOM is not ideal. Our code will be harder to change, may break after small UI changes, and will be, simply put, less flexible.

然而,随着我们测试的复杂性的增加,与DOM的低级原始元素进行交互并不理想。我们的代码将更难改变,在小的UI变化后可能会中断,而且简单地说,会更不灵活。

Instead, we can utilize simple encapsulation and move all of these low-level details into a page object.

相反,我们可以利用简单的封装,将所有这些低层次的细节移到一个页面对象中。

Before we start writing our first-page object, it’s good to have a clear understanding of the pattern – as it should allow us to emulate the interaction of a user with our application.

在我们开始编写我们的第一页对象之前,最好能对该模式有一个清楚的了解–因为它应该允许我们模拟用户与我们的应用程序的交互。

The page object will behave as a sort of interface, that will encapsulate the details of our pages or elements and will expose a high-level API to interact with that element or page.

页面对象将表现为一种接口,它将封装我们的页面或元素的细节,并将暴露一个高级的API来与该元素或页面互动。

As such, an important detail is to provide descriptive names for our methods (ex. clickButton(), navigateTo()), as it would be easier for us to replicate an action taken by the user and will generally lead to a better API when we’re chaining steps together.

因此,一个重要的细节是为我们的方法提供描述性的名字(例如:clickButton(), navigateTo()),因为这将使我们更容易复制用户采取的行动,并且在我们将步骤串联起来时,通常会导致一个更好的API。

Ok, so now, let’s go ahead and create our page object – in this case, our home page:

好了,现在,让我们继续,创建我们的页面对象–在这种情况下,我们的主页。

public class BaeldungHomePage {

    private SeleniumConfig config;
 
    @FindBy(css = ".nav--logo_mobile")
    private WebElement title;
    @FindBy(css = ".menu-start-here > a")
    private WebElement startHere;

    // ...

    public StartHerePage clickOnStartHere() {
        config.clickElement(startHere);

        StartHerePage startHerePage = new StartHerePage(config);
        PageFactory.initElements(config.getDriver(), startHerePage);

        return startHerePage;
    }
}

Notice how our implementation is dealing with the low-level details of the DOM and exposing a nice, high-level API.

请注意我们的实现是如何处理DOM的低级细节并暴露出一个漂亮的高级API。

For example, the @FindBy annotation, allows us to pre-populate our WebElements, this can also be represented using the By API:

例如,@FindBy注解,允许我们预先填充我们的WebElements,这也可以用By API来表示。

private WebElement title = By.cssSelector(".header--menu > a");

Of course, both are valid, however using annotations is a bit cleaner.

当然,两者都是有效的,然而使用注释会更干净一些。

Also, notice the chaining – our clickOnStartHere() method returns a StartHerePage object – where we can continue the interaction:

另外,注意到连锁反应–我们的clickOnStartHere() 方法返回一个StartHerePage 对象–在那里我们可以继续互动。

public class StartHerePage {

    // Includes a SeleniumConfig attribute

    @FindBy(css = ".page-title")
    private WebElement title;

    // constructor

    public String getPageTitle() {
        return title.getText();
    }
}

Let’s write a quick test, where we simply navigate to the page and check one of the elements:

让我们写一个快速测试,我们只需导航到该页面并检查其中的一个元素。

@Test
public void givenHomePage_whenNavigate_thenShouldBeInStartHere() {
    homePage.navigate();
    StartHerePage startHerePage = homePage.clickOnStartHere();
 
    assertThat(startHerePage.getPageTitle(), is("Start Here"));
}

It’s important to take into account that our homepage has the responsibility of:

重要的是要考虑到,我们的主页有以下责任。

  1. Based on the given browser configuration, navigate to the page.
  2. Once there, validate the content of the page (in this case, the title).

Our test is very straightforward; we navigate to the home page, execute the click on the “Start Here” element, which will take us to the page with the same name, and finally, we just validate the title is present.

我们的测试非常直接;我们导航到主页,执行点击 “从这里开始 “元素,这将把我们带到同名的页面,最后,我们只是验证标题是否存在。

After our tests run, the close() method will be executed, and our browser should be closed automatically.

在我们的测试运行后,close()方法将被执行,我们的浏览器应该被自动关闭。

3.1. Separating Concerns

3.1.分开关注的问题

Another possibility that we can take into consideration might be separating concerns (even more), by having two separate classes, one will take care of having all attributes (WebElement or By) of our page:

我们可以考虑的另一种可能性是分离关注点(甚至更多),通过拥有两个独立的类,一个将负责拥有我们页面的所有属性(WebElementBy)

public class BaeldungAboutPage {

    @FindBy(css = ".page-header > h1")
    public static WebElement title;
}

The other will take care of having all the implementation of the functionality we want to test:

另一个将负责拥有我们想要测试的功能的所有实现。

public class BaeldungAbout {

    private SeleniumConfig config;

    public BaeldungAbout(SeleniumConfig config) {
        this.config = config;
        PageFactory.initElements(config.getDriver(), BaeldungAboutPage.class);
    }

    // navigate and getTitle methods
}

If we are using attributes as By and not using the annotation feature, it is recommended to add a private constructor in our page class to prevent it from being instantiated.

如果我们使用属性作为By而不使用注释功能,建议在我们的页面类中添加一个私有构造函数,以防止它被实例化。

It’s important to mention that we need to pass the class that contains the annotations in this case the BaeldungAboutPage class, in contrast to what we did in our previous example by passing the this keyword.

值得一提的是,我们需要传递包含注解的类,在这里是BaeldungAboutPage类,这与我们在之前的例子中通过传递this关键字的做法不同。

@Test
public void givenAboutPage_whenNavigate_thenTitleMatch() {
    about.navigateTo();
 
    assertThat(about.getPageTitle(), is("About Baeldung"));
}

Notice how we can now keep all the internal details of interacting with our page in the implementation, and here, we can actually use this client at a high, readable level.

注意到我们现在可以把与我们的页面交互的所有内部细节保留在实现中,在这里,我们实际上可以在一个高的、可读的水平上使用这个客户端。

4. Conclusion

4.结论

In this quick tutorial, we focused on improving our usage of Selenium/WebDriver with the help of the Page-Object Pattern. We went through different examples and implementations, to see the practical ways of utilizing the pattern to interact with our site.

在这个快速教程中,我们集中讨论了在页面-对象模式的帮助下改善我们对Selenium/WebDriver的使用。我们通过不同的例子和实现,来了解利用该模式与我们的网站进行交互的实际方法。

As always, the implementation of all of these examples and snippets can be found over on GitHub. This is a Maven-based project so it should be easy to import and run.

一如既往,所有这些例子和片段的实现都可以在GitHub上找到。这是一个基于Maven的项目,所以它应该很容易导入和运行。