最近在尝试将工作中常用到的一些配置操作从手工替换成自动化脚本,为实现这个目标我尝试了 selenium IDE 和 selenium webdriver。在这篇文章中想要分享的是登录页面跳转这个简单步骤的一些操作和思考。目标操作步骤如下图所示:
鉴于公认的 selenium IDE 上手容易,我首先尝试在 selenium IDE 里录制整个过程。下图是录制出来的结果,只有 4 个 command,打开主页网址,在跳转到的登录页输入用户名、密码信息,提交信息跳转显示主页。在这个过程中,以下几个问题值得思考。
等待页面加载:selenium IDE 可以控制回放脚本的执行速度,如果执行速度过快,会有一些元素还没有加载出来就被下一步使用,这会导致下一步出现 “element not found” 错误。但 open 命令和 clickAndWait 命令,都是打开加载新的页面,却基本不会出现这种问题。回放速度的快慢并不会影响这两个命令。可以看一下这两个命令的说明:
参数变量化:直接录制的结果中,用户名、密码是直接记录在 command 对应的 value 里的。如果要复用该 test case,用户名密码可能会改变,这时候就需要重新手动修改 value 值。如果一个 test case 中有多个地方需要这些值(比如,登陆之后的校验等),在修改的时候容易出现疏漏。此时需要将这些 value 值变量化,对应的命令是 store,修改后的代码如下:
分支:按照最开始的流程图,打开主页网址的时候可能出现两种情况,主页或登录页。所以需要先进行校验,只有在显示登录页时才需要输入用户名密码。selenium IDE 中默认没有 if 这个命令,需要用 gotoIf 和 label 来代替,同时需要将要校验的条件变量化。
while 循环:流程图的最后一部分是检验用户名密码是否正确,能否顺利登陆。再添加一对 gotoIf 和 label 可实现类似 while 的功能。我在两个 gotoIf 里采用了不同形式的条件语句,可以看到它允许 ture、false 判断,也允许直接与字符串比对等。
到这里,已经用 selenium IDE 实现了流程图的所有功能。总结一下:
鉴于这些我问题,我决定把 selenium IDE 当做一个工具,而真正的自动化程序还是得靠 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(){...}
}
等待页面加载:selenium webdriver 里的等待有两种,显示等待和隐式等待。代码中的 wait.until 就是显示等待的使用。根据源码注释,get 方法会等待页面加载完全才会真正结束,这就说明 get 方法之后不需要显示等待来配合。
参数变量化:变量化用户名、密码,同时将登录部分代码 extract 成新的 private 方法,便于日后复用。
@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 闪亮登场。
这个模式指的是,将每个页面看做一个对象,这些对象有自己的属性(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的时候可以将自定义的用户名、密码作为参数传递
}
在 page object pattern 里,每个页面都可能涉及判断页面元素是否加载完成的问题。对于动态加载页面来说,这个工作量很繁琐,也不美观。selenium 提供了 LoadableComponent 基础类,作为 page object pattern 的扩展,提供更一致且优雅的解决方法。
具体方法为
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) 方法可以打开一个页面并等待其加载完成,那么通过点击按钮跳转到全新页面时,有没有类似的方法可以等待新的页面加载完成呢?
如果日后我找到更简单的实现,会继续更新的,也希望有想法的同事能为我指点迷津。
上文中提到的 “通过点击按钮跳转到全新页面时,有没有类似的方法可以等待新的页面加载完成呢?” 其实在 Page object 理念里应该已经得到了解决。比如,新打开的页面是另一个 page object,将其写成 loadable component 的形式,在 isLoaded 和 load 方法中等待并判断应该就可以实现这一点。