起初知道 PageModel,我是看了这篇文章
个人很喜欢这种思路——不管是 APP 还是 Web 的框架,对于产品都有足够立体化的封装。一个长期维护同一产品的测试&测试开发工程师会比较喜欢这种模式。在这篇文章中我不会谈关键字驱动和数据维护,只说一下我觉得可以怎样去利用关键字和数据愉快的执行 UI 自动化用例。在此我以 Webdriver + java 为例子,App 的原理也一样,其他编程语言的原理也差不多,重要的是思路。
首先我们要封装一个页面,这是最基本的步骤。
BasePage.java
public class BasePage {
WebDriver driver;
public BasePage() {
//默认构造方法
}
/**
*
* @param driver
* 带driver的构造方法
*/
public BasePage(WebDriver driver) {
this.driver = driver;
}
/**
*
* @param driver
* 初始化页面 - 利用反射
* *此处为了简单易懂,就用allRequiredData代替所有需要的数据。数据是怎么来的我们不讨论,反正能得到就行了。
*/
public void initPage(Object allRequiredData) {
//获取当前的类
Class clazz = this.getClass();
List<Field> fList = new ArrayList<Field>();
//由于具体的页面之间可能还有继承关系,所以先拿到所有的Fields,直到父类为Object ,多取一点也不要钱
for (; clazz != Object.class; clazz = clazz.getSuperclass()) {
ConvertUtils.appendArrayToList(fList, clazz.getDeclaredFields());
}
//这里埋下伏笔,总之我取到了我当前操作所需要初始化的所有fileds
List<String> workingElements = Reflections.getUsedFiledsName(
this.getClass(), allRequiredData.keyword);
final Field[] f = fList.toArray(new Field[fList.size()]);
for (int i = 0; i < f.length; i++) {
//如果在所有fileds的列表中,发现当前遍历到的并不是我所需要的,就不执行初始化
if (!workingElements.contains(f[i].getName())) {
continue;
}
//这里就执行元素的初始化,省略一些代码细节
WebElement element = new WebDriverWait(driver, 10)
.until(new ExpectedCondition<WebElement>() {
@Override
public WebElement apply(WebDriver d) {
//此处可以随意扩展用xpath或是其他方式初始化。由于我只用xpath,就不写了。
try {
WebElement e = d.findElement(By
.xpath(allRequiredData.xpath));
return e;
} catch (TimeoutException e) {
logger.error("某元素未被找到 "
+ e.getMessage());
return null;
} catch (NoSuchElementException e) {
// DO NOTHING. 继续循环
return null;
}
}
});
//....此处省略具体的细节
//我们利用反射,将当前成功初始化而得到的WebElement的对象赋值,交付给当前的页面
Reflections.setFieldValue(this, f[i].getName(),
element);
}
}
}
//接下来还可以在当前基类中定义一些公共的方法,让客户端不经过初始化就可以直接调用。这些方法直接通过driver操作
//比如ClickByxpath,byName等
public void commonMethod1() {
driver.click(By.xpath("//xxxxxx"));
//..省略具体细节
}
public void commonMethod2() {
//..省略具体细节
}
}
页面的基类就算定义完成了,接下来写一个子类的例子,看一下运作方式。
SubPage.java
public class SubPage extends BasePage {
public SubPage(WebDriver driver) throws Exception {
super();
this.driver = driver;
}
//一系列WebElement
WebElement element1;
WebElement element2;
WebElement element3;
//一系列页面具体方法
@ContainsElements(values = {"element1"})
public void click_element1() {
element1.click();
}
//..省略具体方法若干
}
子页面就是如此简洁,只要你写好注释,很容易维护。要注意的是@ContainsElements(values = {"element1"}) 中 ContainsElements 是一个自定义注解,这个注解在初始化的时候告诉初始化方法,我的这次执行要用到哪些元素(上面埋伏笔的地方 Reflections.getUsedFiledsName(this.getClass(),allRequiredData.keyword))。想象一下如果每次初始化都涉及全部元素而不去指定要用的元素,那么当一个页面高度封装后,随便做一个简单的点击操作就要让 WebDriver 去加载上百个元素,那你这个自动化也就别做了,没什么意思。
这样一来,PageModel 的最重要部分——Page 基本上就做好了。我们还要解决 driver 的启动问题。没错,我们首先需要一个接口来定义规则。试想一下,在生产环境中,我们的 driver 可能会是 FirefoxDriver, ChromeDriver,IEDriver,AndroidDriver,iOSDriver.如果没有一个接口统一定义规范,那就不能愉快的创建 driver 了。所以:
Resolver.java
public interface Resolver {
//由于我使用的是Selenium Grid,所以连hub就Ok了
public void connectHub();
//当然啦,Resolver最重要的是要给我们创建好的driver
public WebDriver getDriver();
}
随后就要定义浏览器实现
ChromeResolver.java
//这里是自定义注释,我用了一些工厂方法方便扩展,读者可以忽略
@BrowserCode(value = "2")
public class ChromeResolver implements Resolver{
WebDriver driver;
@Override
public void connectHub() {
DesiredCapabilities dc = DesiredCapabilities.chrome();
dc.setBrowserName("chrome");
dc.setPlatform(Platform.WINDOWS);
//你可以做一些你想做的全局的事情,比如在运行的同时去抓取Server端的js错误等。你能想得到的都可以做.
LoggingPreferences loggingprefs = new LoggingPreferences();
loggingprefs.enable(LogType.BROWSER, Level.SEVERE);
dc.setCapability(CapabilityType.LOGGING_PREFS, loggingprefs);
try {
URL url = new URL(BrowserFactory.getHub(osNum));
try {
driver = new RemoteWebDriver(url, dc);
} catch(Exception e) {
LogUtils.postContentAsFile(Exceptions.getStackTraceAsString(e));
} catch(Throwable t) {
LogUtils.postContentAsFile(Exceptions.getStackTraceAsString((Exception) t));
}
//可以加隐式等待,也可以不加,全看你的业务需求
driver.manage().timeouts().implicitlyWait(2, TimeUnit.SECONDS);
} catch (MalformedURLException e) {
log.error("Chrome创建hub URL出错 " + e.getMessage());
e.printStackTrace();
}
}
@Override
public WebDriver getDriver() {
return driver;
}
}
其他的浏览器也是类似的。这里我省略了一些业务逻辑没贴出来,比如你不同的操作系统跑不同的浏览器这些。总之,driver 到手了。接下来就是执行者了:
MainRunner.java
public class MainRunner implements Runnable {
//同样,用一个对象代替所有的数据,不管他是怎么来的
Object AllData;
WebDriver driver;
//省略构造方法
@Override
public void run() {
Resolver r = null;
//这里是浏览器工厂。总之是得到了对应的Resolver,比如ChromeResolver的实体
r = BrowserFactory.getInstance()
.getBrowserResolver(os, browser);
//链接hub
r.connectHub();
//得到driver
driver = r.getDriver();
//最初的操作
driver.get(AllData.startUrl);
driver.manage().window().maximize();
//得到我们要用的步骤,其实就是数据列表了,json也好,xml也好, excel也好。 反正弄成个List了.
RuntimeStepList runtimeStepList = AllData.runtimeStepList;
for (i = 0; i < runtimeStepList.size(); i++) {
//前后省略由于业务需要的各种细节操作,就执行这个步骤
performSingleStep(runtimeStepList.get(i));
}
//后续处理,省略...
}
//执行的具体逻辑
private Object performSingleStep(RunTimeStep current)
throws ClassNotFoundException, NoSuchMethodException,
SecurityException, InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException, ValidationException {
// 反射需要类全名,给个包前缀
String packageName = "com.xxx.xxx.xxx";
// 1.获取具体Page类
Class page = Class.forName(packageName + current.getPageObject());
// 2.调用具体类构造函数创建实例,并调用父类的初始化方法
Constructor con = page.getConstructor(WebDriver.class);
//o也就是当前页面的实体,通过反射创建
Object o = con.newInstance(driver);
Integer pid = current.getPageObjectId();
// 如果是调用公共方法,则不用初始化任何page.页面方法才初始化
if (!current.getPageObject().equals("BasePage")) {
//这里就是通过反射去调用刚才的初始化方法了,参数太多,反正就是用数据去初始化Page.
Reflections.invokeMethod(o, "initPage", new Class[] {
Integer.class, XPathService.class, String.class, String.class, Boolean.class},
new Object[] { pid, xps, current.getKeyword(), judgeSite() , isMobile});
}
// 3.反射调用具体方法
String keyword = current.getKeyword();
String paramString = current.getParameters();
//处理参数
Object[] parameters = handleParameter(paramString);
boolean np = current.getParameters().equals("");
//根据关键字去调用具体的方法,也就是上面的click_element1等。
Object result = Reflections.invokeMethod(o, keyword, composeParamArray(np ? 0
: parameters.length), np ? null : parameters);
//这下面的不用在意了,是某些对结果的处理
// 4.作单步结果处理
current.setPass(1);
rtss.save(current);
//5.若runtimeStep的ExtraFlag为1, 则截图并保存
if(current.getExtraFlag() == 1) {
byte[] sByte = ((TakesScreenshot) driver)
.getScreenshotAs(OutputType.BYTES);
rtss.savePic(current, sByte);
}
return result;
}
}
我的 PageModel 的核心框架也就这样了。最近硬是把移动端和 PC 端的执行全部揉到一起了,揉的时候才发现以前写的东西扩展性太一般。这个轮子肯定有不少的缺点,毕竟编程能力有限。大家愉快地看一下就好,如果你能从中有点收获,也请写出来让大家都体会一下。共同进步!