我发现了一篇关于@FindBy 如何工作以及如何在基于 Selenium (WebDriver) 的测试中使用 FieldDecorator 的非常有趣的帖子:http://habrahabr.ru/post/134462/。
帖子的作者是Роман Оразмагомедов (Roman Orazmagomedof)。
这里我就如何使用 FieldDecorator 做更多的解释。此外,我将展示原始实现的扩展版本,它具有额外的功能,允许通过使用 ExpectedCondition 接口等待装饰字段准备好。
设定目标
Selenium 页面对象模式的大多数插图都使用 WebElement 接口来定义页面的字段:
public class APageObject {
@FindBy(id="fieldOne_id")
WebElement fieldOne;
@FindBy(xpath="fieldTwo_xpath")
WebElement fieldTwo;
<RESTO OF THE Page IMPLEMENTATION>
}
我想要:
a) 一个页面是一个更通用的容器,能够将多个表单组合在一起。
b) 使用纯 java 对象而不是 WebElement 接口在页面上声明字段。
c) 有一种简单的方法来确定页面上的元素是否可以使用。
例如:
public class PageObject {
private APageForm formA;
<OTHER FORMS DECLARATIONS >
public void init(final WebDriver driver) {
this.driver = driver;
formA = new APageForm());
PageFactory.initElements(new SomeDecorator(driver), formA);
<OTHER FORMS INITIALIZATION>
}
<THE REST OF the PAGE IMPLEMENTATION>
}
其中 APageForm 看起来类似于 APageObject,但有一点不同——表单中的每个字段都由专用的 java 类定义。
public class APageForm {
@FindBy(id="fieldOne_id")
FieldOne fieldOne;
@FindBy(xpath="fieldTwo_xpath")
FieldTwo fieldTwo;
<REST OF THE FORM IMPLEMENTATION>
}
还有两点需要记住:
a) 这种方法应该使用 Selenium ExpectedCondition;
b) 这种方法应该有助于区分“数据传递”和“数据断言”之间的代码。
-
元素
公共接口元素{
public boolean isVisible();
public void click();
public ExpectedCondition<WebElement> isReady();
}
这个接口应该扩展为更复杂的元素,如按钮、链接、标签等。例如:
public interface TextField extends Element {
public TextField clear();
public TextField enterText(String text);
public ExpectedCondition<WebElement> isReady();
}
每个元素都应该提供 isReady() 以避免使用 Thread.sleep()。
每个元素的实现都应该继承 AbstractElement 类:
public abstract class AbstractElement implements Element {
protected WebElement wrappedElement;
protected AbstractElement (final WebElement el) {
this.wrappedElement = el;
}
@Override
public boolean isVisible() {
return wrappedElement.isDisplayed();
}
@Override
public void click() {
wrappedElement.click();
}
public abstract ExpectedCondition<WebElement> isReady();
}
例如:
public class ApplicationTextField extends AbstractElement implements TextField {
public ApplicationTextField(final WebElement el) {
super(el);
}
@Override
public TextField clear() {
wrappedElement.clear();
return this;
}
@Override
public TextField enterText(String text) {
char[] letters = text.toCharArray();
for (char c: letters) {
wrappedElement.sendKeys(Character.toString(c));
// because it is typing too fast...
try {
Thread.sleep(70);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return this;
}
@Override
public ExpectedCondition<WebElement> isReady() {
return ExpectedConditions.elementToBeClickable(wrappedElement);
}
}
以下接口描述了一个元素工厂:
public interface ElementFactory {
public <E extends Element> E create(Class<E> containerClass, WebElement wrappedElement);
}
元素工厂的实现是:
public class DefaultElementFactory implements ElementFactory {
@Override
public <E extends Element> E create(final Class<E> elementClass,
final WebElement wrappedElement) {
E element;
try {
element = findImplementingClass(elementClass)
.getDeclaredConstructor(WebElement.class)
.newInstance(wrappedElement);
}
catch (InstantiationException e) { throw new RuntimeException(e);}
catch (IllegalAccessException e) { throw new RuntimeException(e);}
catch (IllegalArgumentException e) {throw new RuntimeException(e);}
catch (InvocationTargetException e) {throw new RuntimeException(e);}
catch (NoSuchMethodException e) { throw new RuntimeException(e);}
catch (SecurityException e) {throw new RuntimeException(e);}
return element;
}
private <E extends Element> Class<? extends E> findImplementingClass (final Class<E> elementClass) {
String pack = elementClass.getPackage().getName();
String className = elementClass.getSimpleName();
String interfaceClassName = pack+"."+className;
Properties impls = TestingProperties.getTestingProperties().getImplementations();
if (impls == null) throw new RuntimeException("Implementations are not loaded");
String implClassName = impls.getProperty(interfaceClassName);
if (implClassName == null) throw new RuntimeException("No implementation found for interface "+interfaceClassName);
try {
return (Class<? extends E>) Class.forName(implClassName);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Unable to load class for "+implClassName,e);
}
}
}
工厂读取属性文件以使用所需的元素实现:
com.qamation.web.elements.Button = tests.application.elements.ApplicationButton
com.qamation.web.elements.Link = tests.application.elements.ApplicationLink
com.qamation.web.elements.TextField = tests.application.elements.ApplicationTextField
com.qamation.web.elements.Label=tests.application.elements.ApplicationLabel
元素工厂将由 FieldDecorator 接口的实现使用。我将在下面讨论这个。
此时元素的部分覆盖完成。总结如下:
每个元素都由一个扩展元素接口的接口描述。
每个元素的实现都扩展了 AbstractElement 类并完成了 isReady() 以及其他必需的方法。
所需元素的实现应在属性文件中定义。
元素工厂将实例化一个元素并通过装饰器将其传递给 PageFactory.initElement()。
起初看起来很复杂。
创建和使用简单元素来建模复杂的表单和页面变得非常方便。
- 容器。
容器是一种将元素和其他容器放在一起的工具,以便为复杂的 Web 表单和页面建模。
容器结构与元素类似,但更简单。
容器由接口定义:
public interface Container {
public void init(WebElement wrappedElement);
public ExpectedCondition<Boolean> isReady(WebDriverWait wait);
}
容器有它的 AbstractContainer 基类:
public abstract class AbstractContainer implements Container{
private WebElement wrappedElement;
@Override
public void init(WebElement wrappedElement) {
this.wrappedElement = wrappedElement;
}
public abstract ExpectedCondition<Boolean> isReady(final WebDriverWait wait);
}
注意容器的init方法很重要:方法的参数是WebElement接口的一个实例。
与元素类似,容器应该实现 isReady() 方法。不同之处在于返回类型:ExpectedCondition。
容器的“就绪”状态取决于容器中包含的元素的组合。
使用布尔类型将多个条件组合成一个是合乎逻辑的。
这是一个容器的例子:
public class LoginContainer extends AbstractContainer{
@FindBy(id="Email")
private TextField username;
@FindBy(id="Passwd" )
private TextField password;
@FindBy(id="signIn")
private Button submitButton;
public void login(final String username, final String password) {
this.username.clear().enterText(username);
this.password.clear().enterText(password);
this.submitButton.press();
}
@Override
public ExpectedCondition<Boolean> isReady(final WebDriverWait wait) {
return new ExpectedCondition<Boolean>() {
@Override
public Boolean apply(final WebDriver driver) {
ExpectedCondition isUserNameFieldReady = username.isReady();
ExpectedCondition isPasswordFieldReady = password.isReady();
ExpectedCondition isSubmitButtonReady = submitButton.isReady();
try {
wait.until(isUserNameFieldReady);
wait.until(isPasswordFieldReady);
wait.until(isSubmitButtonReady);
return new Boolean(true);
}
catch (TimeoutException ex) {
return new Boolean(false);
}
}
};
}
}
由接口定义的容器工厂:
public interface ContainerFactory {
public <C extends Container> C create(Class<C> wrappingClass, WebElement wrappedElement);
}
容器工厂的实现比元素工厂简单得多:
public class DefaultContainerFactory implements ContainerFactory {
@Override
public <C extends Container> C create(final Class<C> wrappingClass,
final WebElement wrappedElement) {
C container;
try {
container = wrappingClass.newInstance();
}
catch (InstantiationException e){throw new RuntimeException(e);}
catch (IllegalAccessException e){throw new RuntimeException(e);}
container.init(wrappedElement);
return container;
}
}
以下是容器的简短摘要:
容器用于将元素和其他容器组合成一个单元。
容器的实现应该从 AbstructContainer 类扩展而来。它应该实现 isReady() 和容器所需的其他方法。
容器工厂将通过装饰器实例化并传递给 PageFactory.initElement()。
- 页面
页面是 WebDriver 实例和容器之间的桥梁。页面有助于将 WebDriver 与测试活动、测试数据供应和测试结果验证分离。
一个Page是由一个接口定义的,类似于Container:
public interface Page {
public void init(WebDriver driver);
}
容器和页面的区别在于init():
public abstract class AbstractPage implements Page {
protected WebDriver driver;
@Override
public void init(WebDriver driver) {
this.driver = driver;
}
}
页面的 init 方法将 WebDriver 实例作为参数。
页面实现应该扩展 AbstractPage 类。例如,一个简单的 gmail 页面:
public interface GMailPage extends Page {
public NewEmail startNewEmail();
}
public class DefaultGMailPage extends AbstractPage implements GMailPage {
private LeftMenueContainer leftMenue;
public void init(final WebDriver driver) {
this.driver = driver;
leftMenue = new LeftMenueContainer();
PageFactory.initElements(new DefaultWebDecorator(driver), leftMenue);
WebDriverWait wait = new WebDriverWait(driver,TestingProperties.getTestingProperties().getTimeOutGeneral());
ExpectedCondition<Boolean> isEmailFormReady = leftMenue.isReady(wait);
wait.until(isEmailFormReady);
}
@Override
public NewEmail startNewEmail() {
leftMenue.pressCompose();
NewEmailWindowContainer newEmail = new NewEmailWindowContainer();
PageFactory.initElements(new DefaultWebDecorator(driver), newEmail);
WebDriverWait wait = new WebDriverWait(driver,TestingProperties.getTestingProperties().getTimeOutGeneral());
ExpectedCondition<Boolean> isNewEmailReady=newEmail.isReady(wait);
wait.until(isNewEmailReady);
return newEmail;
}
}
组件汇总:
元素 -> 抽象元素 -> 元素的实现 -> 元素工厂
容器 -> AbstractContainer -> 容器工厂
页面 -> 抽象页面。
- 装饰器
当 PageFactory.initElements() 调用提供的装饰器时,上述构造变得活跃。
一个基本的实现已经存在——DefaultFieldDecorator。让我们使用它。
public class DefaultWebDecorator extends DefaultFieldDecorator {
private ElementFactory elementFactory = new DefaultElementFactory();
private ContainerFactory containerFactory = new DefaultContainerFactory();
public DefaultWebDecorator(SearchContext context) {
super(new DefaultElementLocatorFactory(context));
}
@Override
public Object decorate(ClassLoader classLoader, Field field) {
ElementLocator locator = factory.createLocator(field);
WebElement wrappedElement = proxyForLocator(classLoader, locator);
if (Container.class.isAssignableFrom(field.getType())) {
return decorateContainer(field, wrappedElement);
}
if (Element.class.isAssignableFrom(field.getType())) {
return decorateElement(field, wrappedElement);
}
return super.decorate(classLoader, field);
}
private Object decorateContainer(final Field field, final WebElement wrappedElement) {
Container container = containerFactory.create((Class<? extends Container>)field.getType(), wrappedElement);
PageFactory.initElements(new DefaultWebDecorator(wrappedElement), container);
return container;
}
private Object decorateElement(final Field field, final WebElement wrappedElement) {
Element element = elementFactory.create((Class<? extends Element>)field.getType(), wrappedElement);
return element;
}
}
请注意,decorateContainer() 直到所有子元素和容器都未初始化后才会退出。
现在,让我们看一个简单的测试,在 gmail 页面上按下 Compose 按钮并检查屏幕上是否出现新的电子邮件窗口:
public class NewEmailTest {
private WebDriver driver;
@BeforeTest
public void setUp() {
driver = new FirefoxDriver();
driver.manage().window().maximize();
}
@AfterTest
public void tearDown() {
driver.close();
}
@Test (dataProvider = "inputAndOutput", dataProviderClass = com.qamation.data.provider.TestDataProvider.class)
public void startNewEmailTest(DataBlock data) {
DefaultHomePage homePage = new DefaultHomePage();
driver.manage().deleteAllCookies();
driver.get(data.getInput()[0]);
homePage.init(driver);
NewEmail newEmail = homePage.signIn().login(data.getInput()[1], data.getInput()[2]).startNewEmail();
for (String[] sa : data.getExpectedResults()) {
WebElement el = driver.findElement(By.xpath(sa[0]));
Assert.assertTrue(el.isDisplayed());
}
}
}
从 Eclipse 运行测试时,需要使用以下 VM 参数:
-DpropertiesFile=testing.properties
可以在此处找到有关 QA 和 QA 自动化的源代码和其他几篇文章
http://qamation.blogspot.com