BDD - SpecFlow Page Object Model
引言
前面文章《 BDD - SpecFlow Web UI 测试实践 》就运用到 Page Object Model,简称 POM,POM 是一种模式,结合 Selenium 用来抽取 Web UI,使得 UI 自动测试更易实现,今天就来详细介绍一下 POM 。
POM 优势
当运用 Selenium 时,总是用 WebElements 来访问不页面上不同的元素。可以利用 WebDriver 类中的方法 FindElement 和 FindElements 来定位到这些 WebElements。
如果你总是直接在 automation code 里直接使用这些方法,会造成大量的重复代码。这个时候就得考虑用 POM 了,将调用 FindElement(s) 方法隐藏在一个类里。
使用 POM 的好处:
- 使得这些隐藏 FindElement(s) 方法的类重用
- 如果 UI Element 有变化,只需修改一个地方
- 减少 Binding 类对 HTML 结构的依赖
POM 简单实现
HTML:
<input id="txtUrl" name="Url" type="text" value="">
Code:
public class PageObject
{
private IWebDriver _webDriver;
public PageObject(IWebDriver webDriver)
{
_webDriver = webDriver;
}
public IWebElement txtUrl => _webDriver.FindElement(By.Id("txtUrl"));
}
public 构造函数中传入 WebDriver 实例,这个 WebDriver 依赖注入,详情参考 《 BDD - SpecFlow Context Injection 上下文依赖注入 》,当访问 TxtUrl 属性时,WebDriver 将在整个 Page 搜索 id 为 txtUrl 的 Element。注意,这里没有涉及到缓存
,也就是说每次访问 TxtUrl 属性时,WebDriver 都会重新搜索一遍。
POM 缓存实现
HTML:
<input id="txtUrl" name="Url" type="text" value="">
Code:
public class PageObject
{
private IWebDriver _webDriver;
private Lazy<IWebElement> _txtUrl;
public PageObject(IWebDriver webDriver)
{
_webDriver = webDriver;
_txtUrl = new Lazy<IWebElement>(() => _webDriver.FindElement(By.Id("txtUrl")));
}
public IWebElement txtUrl => _txtUrl.Value;
}
还是在 public 构造函数中传入 WebDriver 实例,进行 WebDriver 依赖注入。只是利用 Lazy 这种简单的方式将 FindElement 方法的结果缓存起来。
只有第一次访问 txtUrl 属性时,才会触发调用 FindElement 方法,后面访问 txtUrl 属性时,只会返回第一次访问时缓存的值。这样有助于节省自动化执行时间,因为 WebDriver 针对同一 Element 只需搜索一次。
注意
,如果你使用上面的缓存模式,当心页面和页面元素的生命周期。如果页面有发生变化,不要用老的 Element 对象。
POM 层次实现
Parent - Child 关系
HTML:
<div class='A'>
<div class='B'/>
</div>
<div class='B'>
</div>
Code:
public class ParentPageObject
{
private IWebDriver _webDriver;
public ParentPageObject(IWebDriver webDriver)
{
_webDriver = webDriver;
}
public IWebElement WebElement => _webDriver.FindElement(By.ClassName("A"));
public ChildPageObject Child => new ChildPageObject(WebElement);
}
public class ChildPageObject
{
private IWebElement _webElement;
private Lazy<IWebElement> _txtUrl;
public ChildPageObject(IWebElement webElement)
{
_webElement = webElement;
}
public IWebElement WebElement => _webElement.FindElement(By.ClassName("B"));
}
本例中,我们稍微调整一下 HTML 文档,两个 div 元素有相同的 class 属性 B 值,但是我们只想为具有 class A 属性的 div 元素及其子元素创建 PageObject。
就好比一个或多个页面有相同的元素组件一样,我们可以分层次创建 POM 类。我们可以将元素组件单独创建 POM 类,作为 ParentPageObject 类的 ChildPageObject。
如果我们用相同的 WebDriver.FindElement 方法,我们将得到与 A div在同一层的 div元素。
但是每个 WebElement 都有 FindElement(s) 方法,使得我们可以在整个 HTML 文档的局部范围内定位元素。
所以我们可以通过传 parent- WebElement 到 ChildPageObject 类,只定位 A-div 内部的 B-div。
Component 关系
上面是采用 Parent - Child 层次结构,当然也可以 Component 层次结构,将HTML 文档稍微改一下,div A 和 div B 分别是 component,HomePageObject 分别包含 ComponentAPageObject 和 ComponentBPageObject。
HTML:
<div class='A'>
</div>
<div class='B'>
</div>
Code:
public class HomePageObject
{
private IWebDriver _webDriver;
public ParentPageObject(IWebDriver webDriver)
{
_webDriver = webDriver;
}
public ComponentAPageObject componentA => new ComponentAPageObject(_webDriver);
public ComponentBPageObject componentB => new ComponentBPageObject(_webDriver);
}
public class ComponentAPageObject
{
private IWebDriver _webDriver;
public ParentPageObject(IWebDriver webDriver)
{
_webDriver = webDriver;
}
public IWebElement WebElement => _webDriver.FindElement(By.ClassName("A"));
}
public class ComponetBPageObject
{
private IWebDriver _webDriver;
public ParentPageObject(IWebDriver webDriver)
{
_webDriver = webDriver;
}
public IWebElement WebElement => _webDriver .FindElement(By.ClassName("B"));
}