背景

起初知道 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 端的执行全部揉到一起了,揉的时候才发现以前写的东西扩展性太一般。这个轮子肯定有不少的缺点,毕竟编程能力有限。大家愉快地看一下就好,如果你能从中有点收获,也请写出来让大家都体会一下。共同进步!


↙↙↙阅读原文可查看相关链接,并与作者交流