提高 UI 自动化的页面对象

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

如果你读过我以前的一些帖子,很可能你已经看过我的一些关于 自动化测试中设计模式的 文章。我在博客中提到的最突出的模式之一是 页面对象模式 。它对于创建快速、健壮和可读的 UI 自动化是如此基础,以至于我什至专门写了第二篇 文章 。在我的团队中,我们已经在不同的项目中应用了几年。在此过程中,我们提出了几种不同的页面对象变体。我相信,与之前介绍的代码相比,其中一些代码会带来更易读和更可维护的代码。

页面对象 v.2.0 – 接口

我非常喜欢这种模式,以至于我什至 为页面对象创建了一个流畅的 api 。但是,我认为它的使用有点麻烦,至少对我来说是这样。事实证明,我不是 流畅界面 的忠实拥护者。如果您阅读过我关于该主题的文章,您可能已经发现我建议将页面用作 单例 。尽管如此,我认为还有更多“更清洁”的方法可以做到这一点。基本单例类和所有其他基本通用页面使代码更难读且不易掌握。我们通过应用 ioc 容器 解决了这个特殊问题。不幸的是,由于两个通用参数,页面对象的继承树仍然相对复杂。

ioc 容器 – 以前的版本


 public class basepage<m>
    where m : basepageelementmap, new()
{
    protected readonly string url;
public basepage(string url)
{
    this.url = url;
}

public basepage()
{
    this.url = null;
}

protected m map
{
    get
    {
        return new m();
    }
}

public virtual void navigate(string part = "")
{
    driver.browser.navigate().gotourl(string.concat(url, part));
}

}

public class basepage<m, v> : basepage<m> where m : basepageelementmap, new() where v : basepagevalidator<m>, new() { public basepage(string url) : base(url) { }

public basepage()
{
}

public v validate()
{
    return new v();
}

}

在对断言方法进行了几次讨论之后,它们是否应该成为页面对象的一部分,我开始考虑如何改进它们。一方面,我希望断言被重用,另一方面,它们不应该直接成为页面对象的一部分。我认为这是因为它们是存在具有附加泛型参数的第二个基类的原因。此外,还有一个新要求——页面需要可以互换。总之,如果你有一个页面 a,它后来被弃用,它的替代品是页面 a1。在不破坏所有周围代码的情况下交换这两种实现应该很容易。

接口-改进版本


 public class basepage<m>
    where m : basepageelementmap, new()
{
    protected readonly string url;
public basepage(string url)
{
    this.url = url;
}

public basepage()
{
    this.url = null;
}

protected m map
{
    get
    {
        return new m();
    }
}

public virtual void navigate(string part = "")
{
    driver.browser.navigate().gotourl(string.concat(url, part));
}

}

public class basepage<m, v> : basepage<m> where m : basepageelementmap, new() where v : basepagevalidator<m>, new() { public basepage(string url) : base(url) { }

public basepage()
{
}

public v validate()
{
    return new v();
}

}

为了解决最后提到的问题,我们引入了一个新的参与者页面对象模式页面的接口。它定义了页面应该能够执行的操作。


 public class basepage<m>
    where m : basepageelementmap, new()
{
    protected readonly string url;
public basepage(string url)
{
    this.url = url;
}

public basepage()
{
    this.url = null;
}

protected m map
{
    get
    {
        return new m();
    }
}

public virtual void navigate(string part = "")
{
    driver.browser.navigate().gotourl(string.concat(url, part));
}

}

public class basepage<m, v> : basepage<m> where m : basepageelementmap, new() where v : basepagevalidator<m>, new() { public basepage(string url) : base(url) { }

public basepage()
{
}

public v validate()
{
    return new v();
}

}

现在所有依赖于页面的代码都可以使用页面作为接口。这意味着如果你必须更换它,新版本只需要实现相同的接口。

我们所做的第二个改进与之前称为验证器类有关。一般来说,他们持有断言方法,所以我们决定做的第一件事就是重命名它们以后缀 asserter 结尾。我们这样做是因为在生产代码中,验证器用于不同的工作,比如验证用户的输入,而不是断言。

之后,我们应用的另一项重大重构是将断言者的方法用作页面接口的扩展方法。因此,它们可以用作具体页面提供的普通方法。


 public class basepage<m>
    where m : basepageelementmap, new()
{
    protected readonly string url;
public basepage(string url)
{
    this.url = url;
}

public basepage()
{
    this.url = null;
}

protected m map
{
    get
    {
        return new m();
    }
}

public virtual void navigate(string part = "")
{
    driver.browser.navigate().gotourl(string.concat(url, part));
}

}

public class basepage<m, v> : basepage<m> where m : basepageelementmap, new() where v : basepagevalidator<m>, new() { public basepage(string url) : base(url) { }

public basepage()
{
}

public v validate()
{
    return new v();
}

}

所提供实现的唯一缺点是您始终需要创建断言元素的包装器方法,因为您无法通过其接口直接访问页面的元素映射。然而,这些想法导致消除了第二个基页和附加的通用参数。


 public class basepage<m>
    where m : basepageelementmap, new()
{
    protected readonly string url;
public basepage(string url)
{
    this.url = url;
}

public basepage()
{
    this.url = null;
}

protected m map
{
    get
    {
        return new m();
    }
}

public virtual void navigate(string part = "")
{
    driver.browser.navigate().gotourl(string.concat(url, part));
}

}

public class basepage<m, v> : basepage<m> where m : basepageelementmap, new() where v : basepagevalidator<m>, new() { public basepage(string url) : base(url) { }

public basepage()
{
}

public v validate()
{
    return new v();
}

}

正如您从示例中看到的那样,与以前的版本相比,继承模型得到了简化。

提供的解决方案的使用很简单。


 public class basepage<m>
    where m : basepageelementmap, new()
{
    protected readonly string url;
public basepage(string url)
{
    this.url = url;
}

public basepage()
{
    this.url = null;
}

protected m map
{
    get
    {
        return new m();
    }
}

public virtual void navigate(string part = "")
{
    driver.browser.navigate().gotourl(string.concat(url, part));
}

}

public class basepage<m, v> : basepage<m> where m : basepageelementmap, new() where v : basepagevalidator<m>, new() { public basepage(string url) : base(url) { }

public basepage()
{
}

public v validate()
{
    return new v();
}

}

测试使用与之前提供的版本相同的页面。这里唯一微妙的细节是,如果你想能够使用扩展断言方法,你需要为命名空间添加 using 语句,它们的类在。





页面对象 v.2.1 – 公共地图,跳过界面

正如副标题所指出的,下一代页面对象将其元素映射暴露给使用该页面的代码。此外,我们认为为每个特定页面创建接口是一种开销。此外,我们发现大多数情况下替代页面的界面应该与旧页面不同,因为对页面应用了不同的更改。所以我们所做的第一个细微调整是在基页类中,将地图属性标记为公共。


 public class basepage<m>
    where m : basepageelementmap, new()
{
    protected readonly string url;
public basepage(string url)
{
    this.url = url;
}

public basepage()
{
    this.url = null;
}

protected m map
{
    get
    {
        return new m();
    }
}

public virtual void navigate(string part = "")
{
    driver.browser.navigate().gotourl(string.concat(url, part));
}

}

public class basepage<m, v> : basepage<m> where m : basepageelementmap, new() where v : basepagevalidator<m>, new() { public basepage(string url) : base(url) { }

public basepage()
{
}

public v validate()
{
    return new v();
}

}

第二个交替与断言者类有关。现在他们不扩展页面的界面,而是扩展页面本身。


 public class basepage<m>
    where m : basepageelementmap, new()
{
    protected readonly string url;
public basepage(string url)
{
    this.url = url;
}

public basepage()
{
    this.url = null;
}

protected m map
{
    get
    {
        return new m();
    }
}

public virtual void navigate(string part = "")
{
    driver.browser.navigate().gotourl(string.concat(url, part));
}

}

public class basepage<m, v> : basepage<m> where m : basepageelementmap, new() where v : basepagevalidator<m>, new() { public basepage(string url) : base(url) { }

public basepage()
{
}

public v validate()
{
    return new v();
}

}

测试中用法的唯一区别是现在您可以直接在测试中访问地图的元素。


 public class basepage<m>
    where m : basepageelementmap, new()
{
    protected readonly string url;
public basepage(string url)
{
    this.url = url;
}

public basepage()
{
    this.url = null;
}

protected m map
{
    get
    {
        return new m();
    }
}

public virtual void navigate(string part = "")
{
    driver.browser.navigate().gotourl(string.concat(url, part));
}

}

public class basepage<m, v> : basepage<m> where m : basepageelementmap, new() where v : basepagevalidator<m>, new() { public basepage(string url) : base(url) { }

public basepage()
{
}

public v validate()
{
    return new v();
}

}

在我看来,只有当细节框架的逻辑隐藏在页面对象后面时,这才是一个好主意。我的意思是在真正打字之前清除文本输入、执行 javascript 调用等操作。

页面对象 v.2.3 – 部分页面

在我们设计的页面对象中,我最喜欢的是元素、页面逻辑和断言被放置在不同的文件中。我相信这会使页面对象更易于理解和阅读。此外,它还降低了相关的可维护性成本。

为了进一步简化继承树,我们决定使用部分类而不是通用基类。通过将地图属性公开的小调整,我们认为将地图元素直接放在页面中将是一个绝妙的主意。但是,如果将它们组合在同一个文件中,该设计将具有与 webdriver 相同的缺点。它会导致更大的文件,其中元素会与页面的服务方法混合在一起。我们不想要那个。解决方案是将元素放在一个单独的文件中,但现在它是主页的一个部分类。


 public class basepage<m>
    where m : basepageelementmap, new()
{
    protected readonly string url;
public basepage(string url)
{
    this.url = url;
}

public basepage()
{
    this.url = null;
}

protected m map
{
    get
    {
        return new m();
    }
}

public virtual void navigate(string part = "")
{
    driver.browser.navigate().gotourl(string.concat(url, part));
}

}

public class basepage<m, v> : basepage<m> where m : basepageelementmap, new() where v : basepagevalidator<m>, new() { public basepage(string url) : base(url) { }

public basepage()
{
}

public v validate()
{
    return new v();
}

}

这就是新页面地图的样子。它使用主页类中定义的驱动程序实例。


 public class basepage<m>
    where m : basepageelementmap, new()
{
    protected readonly string url;
public basepage(string url)
{
    this.url = url;
}

public basepage()
{
    this.url = null;
}

protected m map
{
    get
    {
        return new m();
    }
}

public virtual void navigate(string part = "")
{
    driver.browser.navigate().gotourl(string.concat(url, part));
}

}

public class basepage<m, v> : basepage<m> where m : basepageelementmap, new() where v : basepagevalidator<m>, new() { public basepage(string url) : base(url) { }

public basepage()
{
}

public v validate()
{
    return new v();
}

}

主页类也有一些小的变化。现在它继承了不需要通用元素映射参数的基本页面的更简单版本。

新页面对象的用法与之前介绍的相同,唯一的区别是可以直接从页面实例访问页面元素。


 public class basepage<m>
    where m : basepageelementmap, new()
{
    protected readonly string url;
public basepage(string url)
{
    this.url = url;
}

public basepage()
{
    this.url = null;
}

protected m map
{
    get
    {
        return new m();
    }
}

public virtual void navigate(string part = "")
{
    driver.browser.navigate().gotourl(string.concat(url, part));
}

}

public class basepage<m, v> : basepage<m> where m : basepageelementmap, new() where v : basepagevalidator<m>, new() { public basepage(string url) : base(url) { }

public basepage()
{
}

public v validate()
{
    return new v();
}

}

优点

所提出的想法的最大优点之一是页面类的单一职责。

在面向对象的编程中,单一职责原则指出每个类都应该对功能的单个部分负责,并且该职责应该完全由类封装。

在当前的实现中,map 类只负责定位元素,asserter 类负责声明事物,而页面本身负责提供服务方法。

源代码

您可以从我的 github 存储库 下载完整的源代码。

如果您喜欢我的出版物,请随时 订阅

另外,点击这些分享按钮。 谢谢你!

参考: