最近在尝试将工作中常用到的一些配置操作从手工替换成自动化脚本,为实现这个目标我尝试了 selenium IDE 和 selenium webdriver。在这篇文章中想要分享的是登录页面跳转这个简单步骤的一些操作和思考。目标操作步骤如下图所示:

使用 selenium IDE 的实现

鉴于公认的 selenium IDE 上手容易,我首先尝试在 selenium IDE 里录制整个过程。下图是录制出来的结果,只有 4 个 command,打开主页网址,在跳转到的登录页输入用户名、密码信息,提交信息跳转显示主页。在这个过程中,以下几个问题值得思考。

到这里,已经用 selenium IDE 实现了流程图的所有功能。总结一下:

鉴于这些我问题,我决定把 selenium IDE 当做一个工具,而真正的自动化程序还是得靠 selenium webdriver。

使用 selenium webdriver 的实现

与 selenium IDE 部分相对应,首先给出实现主流程的功能代码。其中 selenium IDE 命令与 selenium webdriver 方法对应:open -- get(), type -- sendKeys(), clickAndWait -- click() + wait.until()。selenium webdriver 里定位 web element 需要借助 findElement() 方法和 By 类定义的 locator。

public class TestSelenium {
    private WebDriver webDriver;
    String homePageURL = "https://...";

    @BeforeTest
    public void setup(){
        System.setProperty("webdriver.gecko.driver","/Users/.../geckodriver");
        webDriver = new FirefoxDriver();
    }

    @Test
    public void test(){
        webDriver.get(homePageURL);
        webDriver.findElement(By.id("userid")).sendKeys("Z...");
        webDriver.findElement(By.id("password")).sendKeys("W...");
        webDriver.findElement(By.id("btnActive")).click();
        WebDriverWait wait = new WebDriverWait(webDriver,60);
        wait.until(ExpectedConditions.titleIs("O..."));
    }

    @AfterTest
    public void quitDriver(){...}
}
@Test
public void test(){
    webDriver.get(homePageURL);
    signIn("Z...", "W...");
    WebDriverWait wait = new WebDriverWait(webDriver,5);
    wait.until(ExpectedConditions.titleIs("O..."));
    //等待5s,检查网页title,确认是否已登录成功跳转
}

private void signIn(String userid, String passwd) {
    webDriver.findElement(By.id("userid")).sendKeys(userid);
    webDriver.findElement(By.id("password")).sendKeys(passwd);
    webDriver.findElement(By.id("btnActive")).click();
}

@Test
public void test(){
    webDriver.get(homePageURL);

    if(!webDriver.getTitle().equalsIgnoreCase("O...")) {
    //是否需要登录
        WebDriverWait wait = new WebDriverWait(webDriver, 5);
        LocalTime localTime = LocalTime.now();
        LocalTime deadLine = localTime.plusMinutes(1);
        while(LocalTime.now().isBefore(deadLine)) {
        //限制允许登录失败重试的时间,大多数情况下应该没有必要用这个while,是我想太多
            try {
                signIn("Z...", "W..."); //如果有多个同样性质的用户作为备选登录者,这里可改进为读入数据,一个失败后,可在下个循环更换另一个用户,重试。
                wait.until(ExpectedConditions.titleIs("O..."));
                //等待5s,检查网页title,确认是否已登录成功跳转
                System.out.println(webDriver.getTitle());
                return;//如果登录成功,则进行其他操作后退出
            } catch (TimeoutException e) {
                continue;//如果登录失败,则重新登录
            }
        }
        throw new TimeoutException();//超时,仍未登录成功,抛出异常
    }
    System.out.println(webDriver.getTitle());    
}

进行到这里,其实也已经实现了流程图的全部步骤。但仍让人不满意的是 selenium webdriver 对 web element 的定位冗余、不利于复用,以及 test 包含太多逻辑。为了进一步提高代码质量,Page Object Pattern 闪亮登场。

Page Object Pattern

这个模式指的是,将每个页面看做一个对象,这些对象有自己的属性(web element 等)和方法( 信息提交、页面跳转等 action ),将这些信息放在一起,而 test 对应的方法负责想要测试的一个功能点,其内部生成对象实例、调用对象的方法、并负责校验功能是否符合预期。而 PageFactory 类则是实现 Page Object Pattern 必不可少的利器。

以我的这个登录流程为例,测试目标是输入主页网址可能产生的几种操作。这里涉及到两个页面:登录页(LoginPage)和主页(Homepage)。

public class LoginPage {
    private WebDriver  webDriver;

    @FindBy(how = How.ID, using = "userid")
    private WebElement userid;

    @FindBy(how = How.ID, using = "password")
    private WebElement passwd;

    @FindBy(how = How.ID, using = "btnActive")
    private WebElement submitbtn;

    public LoginPage(WebDriver webDriver){
        this.webDriver = webDriver;
        PageFactory.initElements(webDriver,this);
        //初始化page object,传递webdriver,查找element
        //去掉这一句会产生“NullPointerException”
        //可以将这条语句移到test方法new page object部分,同时省略类定义中的webdriver属性,具体见下文代码        
    };

    public void submitLoginInfo(String username, String password) {
        userid.sendKeys(username);
        passwd.sendKeys(password);
        submitbtn.submit();
    }
public class HomePage {
    WebDriver webDriver;
    private String username = "ZHRA-HRSPC1";
    private String password = "Welcome1";

    @FindBy(how = How.XPATH, using = "/html/body/div[2]/form/...")
    WebElement loggedUser;

    public HomePage(WebDriver webDriver, String... args) { 
        .... //构造函数
        if(args.length>1){
            username = args[0];
            password = args[1];
        }else if(args.length>0){
            username = args[0];
        }//传递用户名、密码,也可以写成构造函数重载
    }
    public void getHomePage(){    
        ....        
        webDriver.get(homePageUrl);        
        ....//同上文,设置WebDriverWait

                try {
                    loginPage.submitLoginInfo(username, password);
                    wait.until(ExpectedConditions.visibilityOf(loggedUser));//判断条件为loggedUser元素是否显示
                    return;
                } 
        ....

private WebDriver webDriver;
private HomePage homePage;

@Test
public void loginTest(){
    homePage.getHomePage();//new homePage的时候可以将自定义的用户名、密码作为参数传递
}

LoadableComponent

在 page object pattern 里,每个页面都可能涉及判断页面元素是否加载完成的问题。对于动态加载页面来说,这个工作量很繁琐,也不美观。selenium 提供了 LoadableComponent 基础类,作为 page object pattern 的扩展,提供更一致且优雅的解决方法。

具体方法为

  1. 声明类时继承 LoadableComponent<>类
  2. 覆写 load 方法和 isLoaded 方法
  3. 调用类对象的 get 方法访问页面

load 方法内包含的是访问页面的操作,isLoaded 方法内包含的是检查页面元素是否加载成功的操作。从源代码可以看出,get 方法首先调用 isLoaded 方法判断是否加载成功,没有成功的话会捕获 Error,然后调用 load 方法加载,之后会再次调用 isLoaded 方法判断,并返回。如果第二次加载仍未成功则会抛出 Error。

采用这种方法改写的 page 类及 test 方法。

public class LoginPage extends LoadableComponent<LoginPage>{

    ....

    @Override
    protected void load() {}

    @Override
    protected void isLoaded() throws Error{
        try{
            new WebDriverWait(webDriver, 30).until(ExpectedConditions.visibilityOf(userid));
        }catch (WebDriverException ex){
            throw new Error("LoginPage was not successfully loaded in 30s");
        }
    }
}
public class HomePage extends LoadableComponent<HomePage> {

   ....

    @Override
    protected void load() {
        String homePageUrl = "https://...";
        webDriver.get(homePageUrl);
    }

    @Override
    protected void isLoaded() throws Error {
        if (!webDriver.getTitle().equalsIgnoreCase("O...") && !webDriver.getTitle().equalsIgnoreCase("Sign in")){
            throw new Error("neither homepage nor login page appears");
        }

        if (webDriver.getTitle().equalsIgnoreCase("Sign in")) {
            WebDriverWait wait = new WebDriverWait(webDriver, 5);
            try {
                 loginPage.get();
                 loginPage.submitLoginInfo(username,password);
                 wait.until(ExpectedConditions.visibilityOf(loggedUser));
            } catch (TimeoutException e) {
                 throw new TimeoutException("fail to login");
            }
        }

        if (!webDriver.getTitle().equalsIgnoreCase("O...")){
            throw new TimeoutException("fail to load homepage in almost 1 minute");
        }
    }
}
@Test
    public void loginTest(){
        homePage.get();
    }

未完成的改进

至此,针对一个我工作中常见的步骤 -- 登录页面跳转,使用 selenium IDE 和 webdriver 进行了实现,同时尝试应用了 page object pattern。

但我对现在的代码仍不是很满意,只是目前为止还没有找到进一步改进的方法。譬如,selenium get(url) 方法可以打开一个页面并等待其加载完成,那么通过点击按钮跳转到全新页面时,有没有类似的方法可以等待新的页面加载完成呢?

如果日后我找到更简单的实现,会继续更新的,也希望有想法的同事能为我指点迷津。

后续更新 1

上文中提到的 “通过点击按钮跳转到全新页面时,有没有类似的方法可以等待新的页面加载完成呢?” 其实在 Page object 理念里应该已经得到了解决。比如,新打开的页面是另一个 page object,将其写成 loadable component 的形式,在 isLoaded 和 load 方法中等待并判断应该就可以实现这一点。


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