什么是 Page Object ?

Page Object 是在基于UI的自动化测试中经常被用到的一种设计模式。其目的是对在测试代码中被反复使用的页面元素和操作进行封装,从而去除重复代码,使其更容易被理解和维护。如果需要了解更多有关 Page Object 的内容,可以参考 https://martinfowler.com/bliki/PageObject.html。

Page Object 是如何被用错的? 

虽然 Page Object 被广泛使用,但是经常能见到这个模式被用错。让我们先看看下面的 gist,这是一个误用的例子。

public class AddBudgetPage {
    @Autowired
    UiDriver driver;
    public void open() {
        driver.navigateTo("/budgets/add");
    }
    public void inputMonth(String month) {
        driver.inputTextByName(month, "month");
    }
    public void inputAmount(int amount) {
        driver.inputTextByName(String.valueOf(amount), "amount");
    }
    public void save() {
        driver.clickByText("Save");
    }
}
public class BudgetSteps {
    @Autowired
    AddBudgetPage addBudgetPage;
    @When("^add a budget with month \"([^\"]*)\" and amount (\\d+)$")
    public void add_a_budget_with_month_and_amount(String month, int amount) throws Throwable {
        addBudgetPage.open();
        addBudgetPage.inputMonth(month);
        addBudgetPage.inputAmount(amount);
        addBudgetPage.save();
    }
}

先来解释一下这个代码。AddBudgetPage 是一个 Page Object,他提供了四个方法(open, inputMonth, inputAmount, save)来操作添加预算的页面。而这些方法则被 BudgetSteps 中的 add_a_budget_with_month_and_amount 方法来使用,以此来实现 UI 自动化测试中添加预算这个动作。BudgetSteps 是 Cucumber JVM 中定义的 Step 文件,关于 Cucumber JVM 可以参考 https://github.com/cucumber/cucumber-jvm,这里就不深入了。AddBudgetPage 中的那个 driver,则是对 Selenium web driver 的一个封装,与本文所提及的内容无关。

那么,这个 AddBudgetPage 到底哪里错了?其实就是那四个方法有问题。这些方法实际上是“添加预算”这个业务动作的“分解操作”,是实现细节。从封装的角度来看,以 inputMonth 为例,inputMonth 只是封装了输入预算所在月份的操作细节,即表单中月份输入框的 name 是 month 这个细节。这种粒度的封装可以避免将来输入框 name 修改带来的麻烦,但如果添加预算的业务变了,比如增加了一个叫“部门”的新属性,那么就不得不为 AddBudgetPage 增加一个新方法 inputDepartment。同时,所有用到 AddBudgetPage 的 Step 代码都需要修改,从而丧失了 Page Object 封装操作细节的好处。

如此误用为什么会发生呢?

让我们先来看看为什么会发生这样的误用。除了写UI自动化测试的人自身对这个模式理解不足之外,另一个重要的原因是代码作者是从页面操作(也就是自动化测试)的角度,而非业务角度出发来写的。这是个很常见的问题,测试人员接触需求往往要比开发人员晚(不是一起讨论的需求),对需求的理解和开发或产品不一致。这种情况下,写UI自动化测试的目的仅仅是为了回归测试,而不是在迭代内来验证需求实现的正确性。

要解决这个根本问题,并不是单纯改善Page Object 的代码就能做到的。这需要产品,开发和测试一起来对需求进行协作和澄清,达成共识。这是实例化需求所解决的问题,先不在这里展开了。

Page Object 的正确使用姿势 

以 AddBudgetPage 为例,正确的姿势是提供“添加预算”这个业务方法。

public class AddBudgetPage {
    @Autowired
    UiDriver driver;
    public void addBudget(String month, int amount) {
        driver.navigateTo("/budgets/add");
        driver.inputTextByName(month, "month");
        driver.inputTextByName(String.valueOf(amount), "amount");
        driver.clickByText("Save");
    }
}
public class BudgetSteps {
    @Autowired
    AddBudgetPage addBudgetPage;
    @When("^add a budget with month \"([^\"]*)\" and amount (\\d+)$")
    public void add_a_budget_with_month_and_amount(String month, int amount) throws Throwable {
        addBudgetPage.addBudget(month, amount);
    }
}

如上所示,AddBudgetPage 只有一个 addBudget 方法,封装了“添加预算”的所有实现细节。这样,BudgetSteps 里面只需求调用这一个方法就好了。你可能会说,如果增加新的“部门”属性,BudgetSteps 依然要多传递一个 department 参数给 addBudget 方法。是的,上面代码的确有这个问题,那是因为没有对 Budget 数据进行包装。结合 Cucumber JVM 的 feature 文件中的表格,以及数据绑定能力,就可以解决这个问题,真正实现 BudgetSteps 和 addBudget 方法不会受到新属性的影响。有兴趣的话,可以参考后面的代码示例 https://github.com/nerds-odd-e/bbuddy/blob/master/src/cucumber/java/com/odde/bbuddy/acceptancetest/steps/AccountAddSteps.java

最后,除了通过实例化需求来解决需求协作问题之外,UI自动化测试的“预先”设计也很重要。Page Object 及其正确使用姿势,建议一开始就采用,如果未能一开始就采用,建议立即整改。虽然通过重构总是可以修正这个的错误,但是考虑到UI自动化测试运行的时间较长(相比单元测试),重构通常会伴随着长时间的等待,最终被大部分人放弃。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注