这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

指南和建议

Selenium项目的一些测试指南和建议.

关于"最佳实践"的注解:我们有意在本文档中避免使用"最佳实践"的说辞. 没有一种方法可以适用于所有情况. 我们更喜欢"指南和建议"的想法. 我们鼓励您通读这些内容, 并仔细地确定哪种方法适用于您的特定环境.

由于许多原因, 功能测试很难正确完成. 即便应用程序的状态, 复杂性, 依赖还不够让测试变得 足够复杂, 操作浏览器(特别是跨浏览器的兼容性测试)就已经使得写一个好的测试变成一种挑战.

Selenium提供了一些工具使得功能测试用户更简单的操作浏览器, 但是这些工具并不能帮助你来写一个好的 架构的测试套件. 这章我们会针对怎么来做web页面的功能测试的自动化给出一些忠告, 指南和建议.

这章记录了很多历年来成功的使用Selenium的用户的常用的软件设计模式.

1 - 页面对象模型

注意:本页内容合并自多个来源,包括 Selenium wiki

概述

在 Web 应用的 UI 中,总有一些区域是测试需要与之交互的。 页面对象仅将这些区域建模为测试代码中的对象。 这减少了重复代码的数量,意味着如果 UI 发生变化, 只需在一处进行修复。

页面对象模型是一种设计模式,它在测试自动化领域日益流行, 旨在增强测试的可维护性并减少代码重复。页面对象是一个 面向对象的类,它作为被测应用 (AUT) 某个页面的接口。 测试在需要与该页面的 UI 交互时,会使用此页面对象类的 方法。好处在于,如果该页面的 UI 发生变化,测试本身 不需要更改,只需更改页面对象中的代码即可。随后, 所有支持新 UI 的更改都集中在一个地方。

优势

  • 测试代码与页面特定代码(如定位器,或使用 UI 映射时的定位器使用方式)和布局之间有清晰的分离。
  • 页面所提供的服务或操作有单一的存储库, 而不是将这些服务分散在各处测试中。

在以上两种情况下,这使得因 UI 变更而需要进行的所有修改 都可以在一处完成。关于此技术的有用信息可以在众多博客中找到, 因为这种"测试设计模式"正被广泛使用。 我们鼓励希望了解更多信息的读者在互联网上搜索关于此主题的博客。 许多人已经就这一设计模式撰写了文章, 并能提供超出本用户指南范围的有用提示。 为了帮助您入门,我们将用一个简单的示例来说明页面对象。

示例

首先,考虑一个不使用页面对象的典型测试自动化示例:

/***
 * Tests login feature
 */
public class Login {

  public void testLogin() {
    // fill login data on sign-in page
    driver.findElement(By.name("user_name")).sendKeys("userName");
    driver.findElement(By.name("password")).sendKeys("my supersecret password");
    driver.findElement(By.name("sign-in")).click();

    // verify h1 tag is "Hello userName" after login
    driver.findElement(By.tagName("h1")).isDisplayed();
    assertThat(driver.findElement(By.tagName("h1")).getText(), is("Hello userName"));
  }
}

这种方式存在两个问题。

  • 测试方法与 AUT 的定位器(在本例中为 ID)之间没有分离;两者 交织在同一个方法中。如果 AUT 的 UI 更改了其标识符、布局, 或登录的输入和处理方式,测试本身必须随之更改。
  • ID 定位器会分散在多个测试中,散布在所有需要 使用此登录页面的测试里。

应用页面对象模型后,此示例可以重写为 以下登录页面的页面对象示例。

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

/**
 * Page Object encapsulates the Sign-in page.
 */
public class SignInPage {
  protected WebDriver driver;

  // <input name="user_name" type="text" value="">
  private By usernameBy = By.name("user_name");
  // <input name="password" type="password" value="">
  private By passwordBy = By.name("password");
  // <input name="sign_in" type="submit" value="SignIn">
  private By signinBy = By.name("sign_in");

  public SignInPage(WebDriver driver){
    this.driver = driver;
     if (!driver.getTitle().equals("Sign In Page")) {
      throw new IllegalStateException("This is not Sign In Page," +
            " current page is: " + driver.getCurrentUrl());
    }
  }

  /**
    * Login as valid user
    *
    * @param userName
    * @param password
    * @return HomePage object
    */
  public HomePage loginValidUser(String userName, String password) {
    driver.findElement(usernameBy).sendKeys(userName);
    driver.findElement(passwordBy).sendKeys(password);
    driver.findElement(signinBy).click();
    return new HomePage(driver);
  }
}

而首页的页面对象可能如下所示。

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

/**
 * Page Object encapsulates the Home Page
 */
public class HomePage {
  protected WebDriver driver;

  // <h1>Hello userName</h1>
  private By messageBy = By.tagName("h1");

  public HomePage(WebDriver driver){
    this.driver = driver;
    if (!driver.getTitle().equals("Home Page of logged in user")) {
      throw new IllegalStateException("This is not Home Page of logged in user," +
            " current page is: " + driver.getCurrentUrl());
    }
  }

  /**
    * Get message (h1 tag)
    *
    * @return String message text
    */
  public String getMessageText() {
    return driver.findElement(messageBy).getText();
  }

  public HomePage manageProfile() {
    // Page encapsulation to manage profile functionality
    return new HomePage(driver);
  }
  /* More methods offering the services represented by Home Page
  of Logged User. These methods in turn might return more Page Objects
  for example click on Compose mail button could return ComposeMail class object */
}

现在,登录测试将按如下方式使用这两个页面对象。

/***
 * Tests login feature
 */
public class TestLogin {

  @Test
  public void testLogin() {
    SignInPage signInPage = new SignInPage(driver);
    HomePage homePage = signInPage.loginValidUser("userName", "password");
    assertThat(homePage.getMessageText(), is("Hello userName"));
  }

}

页面对象的设计方式有很大的灵活性, 但要获得所需的测试代码可维护性,有一些基本规则需要遵循。

页面对象中的断言

页面对象本身不应进行验证或断言。 这是测试的一部分,应始终在测试代码中, 而非页面对象中。 页面对象将包含页面的表示, 以及页面通过方法提供的服务, 但不应包含与被测内容相关的代码。

有一个唯一的验证可以、也应当在页面对象中进行, 那就是验证页面以及页面上的关键元素是否已正确加载。 此验证应在实例化页面对象时完成。 在上面的示例中,SignInPage 和 HomePage 的构造函数都会检查预期页面是否可用并准备好 接收测试的请求。

页面组件对象

页面对象不一定需要自身表示页面的所有部分。 这在早期就已被Martin Fowler 指出, 他当时首次提出了"面板对象"这一术语。

用于页面对象的相同原则可以用来 创建"页面 组件 对象"(后来的称呼),它表示页面的离散区块, 并 可以包含在页面对象中。 这些组件对象可以提供对这些离散区块内部元素的引用, 以及利用它们所提供的功能或行为的方法。

例如,一个产品页面有多个产品。

<!-- Products Page -->
<div class="header_container">
    <span class="title">Products</span>
</div>

<div class="inventory_list">
    <div class="inventory_item">
    </div>
    <div class="inventory_item">
    </div>
    <div class="inventory_item">
    </div>
    <div class="inventory_item">
    </div>
    <div class="inventory_item">
    </div>
    <div class="inventory_item">
    </div>
</div>

每个产品都是产品页面的一个组件。

<!-- Inventory Item -->
<div class="inventory_item">
    <div class="inventory_item_name">Backpack</div>
    <div class="pricebar">
        <div class="inventory_item_price">$29.99</div>
        <button id="add-to-cart-backpack">Add to cart</button>
    </div>
</div>

产品页面拥有(HAS-A)一个产品列表。这种对象关系称为组合。 简单来说,某个事物 另一个事物 组成

public abstract class BasePage {
    protected WebDriver driver;

    public BasePage(WebDriver driver) {
        this.driver = driver;
    }
}

// Page Object
public class ProductsPage extends BasePage {
    public ProductsPage(WebDriver driver) {
        super(driver);
        // No assertions, throws an exception if the element is not loaded
        new WebDriverWait(driver, Duration.ofSeconds(3))
            .until(d -> d.findElement(By.className("header_container")));
    }

    // Returning a list of products is a service of the page
    public List<Product> getProducts() {
        return driver.findElements(By.className("inventory_item"))
            .stream()
            .map(e -> new Product(e)) // Map WebElement to a product component
            .toList();
    }

    // Return a specific product using a boolean-valued function (predicate)
    // This is the behavioral Strategy Pattern from GoF
    public Product getProduct(Predicate<Product> condition) {
        return getProducts()
            .stream()
            .filter(condition) // Filter by product name or price
            .findFirst()
            .orElseThrow(() -> new RuntimeException("Product not found")); // Error thrown during actual test run
    }
}

Product 组件对象在产品页面对象内部使用。

public abstract class BaseComponent {
    protected WebElement root;

    public BaseComponent(WebElement root) {
        this.root = root;
    }
}

// Page Component Object
public class Product extends BaseComponent {
    // The root element contains the entire component
    public Product(WebElement root) {
        super(root); // inventory_item
    }

    public String getName() {
        // Locating an element begins at the root of the component
        return root.findElement(By.className("inventory_item_name")).getText();
    }

    public BigDecimal getPrice() {
        return new BigDecimal(
                root.findElement(By.className("inventory_item_price"))
                    .getText()
                    .replace("$", "")
            ).setScale(2, RoundingMode.UNNECESSARY); // Sanitation and formatting
    }

    public void addToCart() {
        root.findElement(By.id("add-to-cart-backpack")).click();
    }
}

现在,产品测试将按如下方式使用页面对象和页面组件对象。

public class ProductsTest {
    @Test
    public void testProductInventory() {
        var productsPage = new ProductsPage(driver); // page object
        var products = productsPage.getProducts();
        assertEquals(6, products.size()); // expected, actual
    }
    
    @Test
    public void testProductPrices() {
        var productsPage = new ProductsPage(driver);

        // Pass a lambda expression (predicate) to filter the list of products
        // The predicate or "strategy" is the behavior passed as parameter
        var backpack = productsPage.getProduct(p -> p.getName().equals("Backpack")); // page component object
        var bikeLight = productsPage.getProduct(p -> p.getName().equals("Bike Light"));

        assertEquals(new BigDecimal("29.99"), backpack.getPrice());
        assertEquals(new BigDecimal("9.99"), bikeLight.getPrice());
    }
}

页面和组件由各自的对象表示。这两个对象只有提供其服务的方法, 这与实际应用相匹配,也是面向对象编程的核心原则。 当构建应用程序时,它们并不是由一个庞大的页面实体构成的。 它们是由包含在页面中的组件构建的。页面组件对象实现了相同的方法。

您甚至可以 将组件对象嵌套在其他组件对象中,用于更复杂的 页面。如果 AUT 中的某个页面有多个组件,或 整个站点中使用的公共组件(例如导航栏),那么这 可能会提高可维护性并减少代码重复。

测试中使用的其他设计模式

还有其他一些设计模式也可用于测试。讨论所有这些模式 超出了本用户指南的范围。在这里,我们只是想介绍 这些概念,让读者了解可以做的一些事情。正如 前面提到的,许多人已经在这一主题上撰写了博客,我们鼓励 读者搜索关于这些主题的博客。

实现说明

页面对象可以被看作同时面向两个方向。面向测试开发者的一面, 它们表示特定页面提供的服务。 背对开发者的一面,它们应该是唯一对页面(或页面的一部分)的 HTML 结构有深入了解的东西。 最简单的理解方式是将页面对象上的方法视为提供页面所提供的"服务",而不是暴露页面的细节和机制。 举例来说,想想任何基于 Web 的电子邮件系统的收件箱。 它提供的服务包括撰写新邮件、选择阅读单封邮件,以及列出收件箱中邮件的主题行。 这些功能如何实现对测试而言不应重要。

因为我们鼓励测试开发者尝试思考他们正在交互的服务而非实现, 所以页面对象应尽量少地暴露底层的 WebDriver 实例。 为此,页面对象上的方法可以返回另一个页面对象、另一个页面组件对象, 甚至自身。这意味着我们可以有效地对用户在应用程序中的旅程进行建模。 这也意味着,如果页面之间的关联方式发生变化 (例如,登录页面在用户首次登录某服务时要求其更改密码,而以前并不这样做), 只需更改相应方法的签名就会导致测试无法编译。 换言之,当我们更改页面之间的关系并在页面对象中反映这一变化时, 无需运行测试就能知道哪些测试会失败。

这种方法的一个后果是,可能需要对(例如)成功和失败的登录分别建模; 或者一次点击可能根据应用程序的状态产生不同的结果。 当发生这种情况时,通常在页面对象上有多个方法:

public class LoginPage {
    public HomePage loginAs(String username, String password) {
        // ... clever magic happens here
    }
    
    public LoginPage loginAsExpectingError(String username, String password) {
        //  ... failed login here, maybe because one or both of the username and password are wrong
    }
    
    public String getErrorMessage() {
        // So we can verify that the correct error is shown
    }
}

上面的代码展示了一个要点:测试而非页面对象应负责对页面状态进行断言。例如:

public void testMessagesAreReadOrUnread() {
    Inbox inbox = new Inbox(driver);
    inbox.assertMessageWithSubjectIsUnread("I like cheese");
    inbox.assertMessageWithSubjectIsNotUnread("I'm not fond of tofu");
}

可以重写为:

public void testMessagesAreReadOrUnread() {
    Inbox inbox = new Inbox(driver);
    assertTrue(inbox.isMessageWithSubjectIsUnread("I like cheese"));
    assertFalse(inbox.isMessageWithSubjectIsUnread("I'm not fond of tofu"));
}

当然,与每条指南一样,也有例外情况, 页面对象中常见的一个例外是在实例化页面对象时检查 WebDriver 是否在正确的页面上。 这在下面的示例中有所体现。

最后,页面对象不必表示整个页面,可以由页面对象组件组成。 这些组件可以表示在站点或页面中频繁出现的部分,例如站点导航。 核心原则是,在你的测试套件中只有一个地方了解特定页面(或页面的一部分)的 HTML 结构。

总结

  • 公共方法表示页面或组件提供的服务
  • 尽量不要暴露页面或组件的内部细节
  • 通常不要进行断言
  • 方法返回其他页面对象、页面组件对象,或可选地返回自身(用于流畅语法)
  • 不必始终表示整个页面
  • 同一操作的不同结果建模为不同的方法

示例

public class LoginPage {
    private final WebDriver driver;

    public LoginPage(WebDriver driver) {
        this.driver = driver;

        // Check that we're on the right page.
        if (!"Login".equals(driver.getTitle())) {
            // Alternatively, we could navigate to the login page, perhaps logging out first
            throw new IllegalStateException("This is not the login page");
        }
    }

    // The login page contains several HTML elements that will be represented as WebElements.
    // The locators for these elements should only be defined once.
        By usernameLocator = By.id("username");
        By passwordLocator = By.id("passwd");
        By loginButtonLocator = By.id("login");

    // The login page allows the user to type their username into the username field
    public LoginPage typeUsername(String username) {
        // This is the only place that "knows" how to enter a username
        driver.findElement(usernameLocator).sendKeys(username);

        // Return the current page object as this action doesn't navigate to a page represented by another PageObject
        return this;	
    }

    // The login page allows the user to type their password into the password field
    public LoginPage typePassword(String password) {
        // This is the only place that "knows" how to enter a password
        driver.findElement(passwordLocator).sendKeys(password);

        // Return the current page object as this action doesn't navigate to a page represented by another PageObject
        return this;	
    }

    // The login page allows the user to submit the login form
    public HomePage submitLogin() {
        // This is the only place that submits the login form and expects the destination to be the home page.
        // A separate method should be created for the instance of clicking login whilst expecting a login failure. 
        driver.findElement(loginButtonLocator).submit();

        // Return a new page object representing the destination. Should the login page ever
        // go somewhere else (for example, a legal disclaimer) then changing the method signature
        // for this method will mean that all tests that rely on this behaviour won't compile.
        return new HomePage(driver);	
    }

    // The login page allows the user to submit the login form knowing that an invalid username and / or password were entered
    public LoginPage submitLoginExpectingFailure() {
        // This is the only place that submits the login form and expects the destination to be the login page due to login failure.
        driver.findElement(loginButtonLocator).submit();

        // Return a new page object representing the destination. Should the user ever be navigated to the home page after submitting a login with credentials 
        // expected to fail login, the script will fail when it attempts to instantiate the LoginPage PageObject.
        return new LoginPage(driver);	
    }

    // Conceptually, the login page offers the user the service of being able to "log into"
    // the application using a user name and password. 
    public HomePage loginAs(String username, String password) {
        // The PageObject methods that enter username, password & submit login have already defined and should not be repeated here.
        typeUsername(username);
        typePassword(password);
        return submitLogin();
    }
}

2 - 领域特定语言

领域特定语言 (DSL) 是一种为用户提供解决问题的表达方式的系统. 它使用户可以按照自己的术语与系统进行交互, 而不仅仅是通过程序员的语言.

您的用户通常并不关心您网站的外观. 他们不在乎装饰, 动画或图形. 他们希望借助于您的系统, 以最小的难度使新员工融入整个流程; 他们想预订去阿拉斯加的旅行; 他们想以折扣价配置和购买独角兽. 您作为测试人员的工作应尽可能接近"捕捉”这种思维定势. 考虑到这一点, 我们开始着手"建模”您正在工作的应用程序, 以使测试脚本 (发布前用户仅有的代理) “说话”并代表用户.

在Selenium中, DSL通常由方法表示, 其编写方式使API简单易读-它们使开发人员和干系人 (用户, 产品负责人, 商业智能专家等) 之间能够产生汇报.

好处

  • 可读: 业务关系人可以理解.
  • 可写: 易于编写, 避免不必要的重复.
  • 可扩展: 可以 (合理地) 添加功能而无需打破约定以及现有功能.
  • 可维护: 通过将实现细节排除在测试用例之外, 您可以很好地隔离 AUT* 的修改.

Java

以下是Java中合理的DSL方法的示例. 为简便起见, 假定 driver 对象是预定义的并且可用于该方法.

/**
 * Takes a username and password, fills out the fields, and clicks "login".
 * @return An instance of the AccountPage
 */
public AccountPage loginAsUser(String username, String password) {
  WebElement loginField = driver.findElement(By.id("loginField"));
  loginField.clear();
  loginField.sendKeys(username);

  // Fill out the password field. The locator we're using is "By.id", and we should
  // have it defined elsewhere in the class.
  WebElement passwordField = driver.findElement(By.id("password"));
  passwordField.clear();
  passwordField.sendKeys(password);

  // Click the login button, which happens to have the id "submit".
  driver.findElement(By.id("submit")).click();

  // Create and return a new instance of the AccountPage (via the built-in Selenium
  // PageFactory).
  return PageFactory.newInstance(AccountPage.class);
}

此方法完全从测试代码中抽象出输入字段, 按钮, 单击甚至页面的概念. 使用这种方法, 测试人员要做的就是调用此方法. 这给您带来了维护方面的优势: 如果登录字段曾经更改过, 则只需更改此方法-而非您的测试.

public void loginTest() {
    loginAsUser("cbrown", "cl0wn3");

    // Now that we're logged in, do some other stuff--since we used a DSL to support
    // our testers, it's as easy as choosing from available methods.
    do.something();
    do.somethingElse();
    Assert.assertTrue("Something should have been done!", something.wasDone());

    // Note that we still haven't referred to a button or web control anywhere in this
    // script...
}

郑重强调: 您的主要目标之一应该是编写一个API, 该API允许您的测试解决 当前的问题, 而不是UI的问题. 用户界面是用户的次要问题–用户并不关心用户界面, 他们只是想完成工作. 您的测试脚本应该像用户希望做的事情以及他们想知道的事情的完整清单那样易于阅读. 测试不应该考虑UI如何要求您去做.

*AUT: 待测系统

3 - 生成应用程序状态

Selenium不应用于准备测试用例. 测试用例中所有重复性动作和准备工作, 都应通过其他方法来完成.
例如, 大多数Web UI都具有身份验证 (诸如一个登录表单) . 在每次测试之前通过Web浏览器进行登录的消除, 将提高测试的速度和稳定性. 应该创建一种方法来获取对 AUT* 的访问权限 (例如, 使用API登录并设置Cookie) . 此外, 不应使用Selenium创建预加载数据来进行测试的方法.
如前所述, 应利用现有的API为 AUT* 创建数据. *AUT: 待测系统

4 - 模拟外部服务

消除对外部服务的依赖性将大大提高测试的速度和稳定性.

5 - 改善报告

Selenium并非旨在报告测试用例的运行状态. 利用单元测试框架的内置报告功能是一个好的开始. 大多数单元测试框架都有可以生成xUnit或HTML格式的报告. xUnit报表很受欢迎, 可以将其结果导入到持续集成(CI)服务器, 例如Jenkins、Travis、Bamboo等. 以下是一些链接, 可获取关于几种语言报表输出的更多信息.

Allure Report: fancy reports (language and framework agnostic)

NUnit 3 Console Runner

NUnit 3 Console Command Line

xUnit getting test results in TeamCity

xUnit getting test results in CruiseControl.NET

xUnit getting test results in Azure DevOps

6 - 避免共享状态

尽管在多个地方都提到过, 但这点仍值得被再次提及. 确保测试相互隔离.

  • 不要共享测试数据. 想象一下有几个测试, 每个测试都会在选择操作执行之前查询数据库中的有效订单. 如果两个测试采用相同的顺序, 则很可能会出现意外行为.

  • 清理应用程序中过时的数据, 这些数据可能会被其他测试. 例如无效的订单记录.

  • 每次测试都创建一个新的WebDriver实例. 这在确保测试隔离的同时可以保障并行化更为简单.

    • If you choose pytest as your test runner, this can be easily done by yielding your driver in a global fixture. This way each test gets its own driver instance, and you can ensure that drivers always quit after a test is finished (pass or fail).

7 - 使用定位器的提示

何时使用哪些定位器以及如何在代码中最好地管理它们.

这里有一些 支持的定位策略 的例子 .

一般来说,如果 HTML 的 id 是可用的、唯一的且是可预测的,那么它就是在页面上定位元素的首选方法。它们的工作速度非常快,可以避免复杂的 DOM 遍历带来的大量处理。

如果没有唯一的 id,那么最好使用写得好的 CSS 选择器来查找元素。XPath 和 CSS 选择器一样好用,但是它语法很复杂,并且经常很难调试。尽管 XPath 选择器非常灵活,但是他们通常未经过浏览器厂商的性能测试,并且运行速度很慢。

基于链接文本和部分链接文本的选择策略有其缺点,即只能对链接元素起作用。此外,它们在 WebDriver 内部调用 querySelectorAll 选择器。

标签名可能是一种危险的定位元素的方法。页面上经常出现同一标签的多个元素。这在调用 findElements(By) 方法返回元素集合的时候非常有用。

建议您尽可能保持定位器的紧凑性和可读性。使用 WebDriver 遍历 DOM 结构是一项性能花销很大的操作,搜索范围越小越好。

8 - 测试的独立性

将每个测试编写为独立的单元. 以不依赖于其他测试完成的方式编写测试:

例如有一个内容管理系统, 您可以借助其创建一些自定义内容, 这些内容在发布后作为模块显示在您的网站上, 并且CMS和应用程序之间的同步可能需要一些时间.

测试模块的一种错误方法是在测试中创建并发布内容, 然后在另一测试中检查该模块. 这是不可取的, 因为发布后内容可能无法立即用于其他测试.

与之相反的事, 您可以创建在受影响的测试中打开和关闭的打桩内容, 并将其用于验证模块. 而且, 对于内容的创建, 您仍然可以进行单独的测试.

9 - 考虑使用Fluent API

Martin Fowler创造了术语 “Fluent API”. Selenium已经在其 FluentWait 类中实现了类似的东西, 这是对标准 Wait 类的替代. 您可以在页面对象中启用Fluent API设计模式, 然后使用如下代码段查询Google搜索页面:

driver.get( "http://www.google.com/webhp?hl=en&amp;tab=ww" );
GoogleSearchPage gsp = new GoogleSearchPage(driver);
gsp.setSearchString().clickSearchButton();

Google页面对象类具有这种流畅行为后可能看起来像这样:

public abstract class BasePage {
    protected WebDriver driver;

    public BasePage(WebDriver driver) {
        this.driver = driver;
    }
}

public class GoogleSearchPage extends BasePage {
    public GoogleSearchPage(WebDriver driver) {
        super(driver);
        // Generally do not assert within pages or components.
        // Effectively throws an exception if the lambda condition is not met.
        new WebDriverWait(driver, Duration.ofSeconds(3)).until(d -> d.findElement(By.id("logo")));
    }

    public GoogleSearchPage setSearchString(String sstr) {
        driver.findElement(By.id("gbqfq")).sendKeys(sstr);
        return this;
    }

    public void clickSearchButton() {
        driver.findElement(By.id("gbqfb")).click();
    }
}

10 - 每次测试都刷新浏览器

每次测试都从一个干净的已知状态开始. 理想情况下, 为每次测试打开一个新的虚拟机. 如果打开新虚拟机不切实际, 则至少应为每次测试启动一个新的WebDriver. 对于Firefox, 请使用您已知的配置文件去启动WebDriver. 大多数浏览器驱动器,像GeckoDriver和ChromeDriver那样,默认都会以干净的已知状态和一个新的用户配置文件开始。

WebDriver driver = new FirefoxDriver();