Java Selenium框架的开发和优化教程-01
1.背景
在我给我们公司写的爬虫框架和UI自动化测试框架中,都使用到了Java+Selenium,而随着框架功能体积的增大,很自然地需要对Selenium进行进一步的封装和优化,以应对愈发复杂的需求。
下面我把这两个项目中,对于selenium的使用、封装和优化的方式抽出来写成一个Maven的demo项目,进行详细介绍,供大家参考。
2.项目一览
2.1 项目结构
这个demo的Maven项目是我精简过的,所以非常简单,结构如下图:
核心代码:
Selenium操作页面的流程是:初始化浏览器的driver,启动浏览器 -> 定位界面的元素 -> 操作界面元素。
所以我把这三大步分布封装成图中的三个类:
- UiActions - 对UI基础操作的封装
- UiContext - 对浏览器上下文的封装
- UiFinder - 对UI元素查找器的封装
在下文中会对核心代码有详细的介绍。
具体实现
这里我以百度为例,简单演示了封装好的框架是如何操作浏览器并执行百度搜索的过程。
浏览器驱动
浏览器驱动我使用的是Chrome Driver,放在resources路径下是方便框架直接进行调用。
2.2 集成浏览器驱动WebDriver
首先,需要下载WebDriver。各浏览器下载地址:
Firefox浏览器驱动:geckodriver
Chrome浏览器驱动:chromedriver taobao备用地址
IE浏览器驱动:IEDriverServer
Edge浏览器驱动:MicrosoftWebDriver
Opera浏览器驱动:operadriver
PhantomJS浏览器驱动:phantomjs
打开taobao备用地址,我下载的是74.0.3729.6版本(当然你们也可以选择更新的版本),如下图:
然后下载chromedriver_win32.zip并解压,如下图:
然后将其中的chromedriver.exe放到项目的resource\WebDriver路径下即可。
2.3 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.javaseleniumdemo</groupId>
<artifactId>javaseleniumdemo</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!-- selenium-java -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.141.59</version>
</dependency>
<!--可以引入日志 @Slf4j注解-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
<!--用于截屏保存文件-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
</project>
3.代码示例
3.1 对浏览器上下文的封装
UiContext这个类即为对浏览器上下文的封装,它需要包括初始化浏览器、设置浏览器特性、获取浏览器上下文以及关闭浏览器释放资源等功能,代码如下:
package com.javaseleniumdemo.core;
import org.openqa.selenium.UnexpectedAlertBehaviour;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.support.ui.WebDriverWait;
/**
* @author Joy
*/
class UiContext {
private static UiContext uiContext;
private static WebDriver driver;
private static WebDriverWait wait ;
/**
* description: 获取UI上下文的driver
* @return WebDriver
*/
public static WebDriver getDriver() {
WebDriver d = null;
try {
d = UiContext.getUiContext().driver;
} catch (Exception e){
e.printStackTrace();
}
return d;
}
/**
* description: 获取UI上下文的driver等待时间
* @return WebDriverWait
*/
public static WebDriverWait getDriverWait() {
WebDriverWait time = null;
try {
time = UiContext.getUiContext().wait;
} catch (Exception e){
e.printStackTrace();
}
return time;
}
/**
* description: 用于初始化context
* @return UiContext
*/
public static UiContext initialUiContext() {
try {
if(uiContext == null){
uiContext = new UiContext();
System.setProperty("webdriver.chrome.driver", "src/Main/resources/WebDriver/chromedriver.exe");
ChromeOptions chromeOptions = new ChromeOptions();
// 当在服务器上运行时,需要使用此句将浏览器设为无头模式
// chromeOptions.addArguments("--headless");
DesiredCapabilities capabilities = DesiredCapabilities.chrome();
capabilities.setCapability(ChromeOptions.CAPABILITY, chromeOptions);
// 使浏览器可以处理alert
capabilities.setCapability(CapabilityType.UNEXPECTED_ALERT_BEHAVIOUR, UnexpectedAlertBehaviour.IGNORE);
uiContext.driver = new ChromeDriver(capabilities);
// 使浏览器全屏
// uiContext.driver.manage().window().fullscreen();
uiContext.driver.manage().deleteAllCookies();
uiContext.wait = new WebDriverWait(driver, 30, 1);
driver.manage().window().maximize();
Thread.sleep(2000);
System.out.println("UiContext初始化成功,ChromeDriver创建成功。");
}
} catch (Exception e){
System.out.println("UiContext初始化失败:" + e.getMessage());
e.printStackTrace();
}
return uiContext;
}
/**
* description: 获取浏览器上下文
* @return UiContext
*/
public static UiContext getUiContext() {
if(uiContext == null){
initialUiContext();
}
return uiContext;
}
/**
* description: 浏览器退出并释放资源
*/
public static void dispose() {
if (driver != null){
driver.quit();
driver = null;
uiContext = null;
}
}
/**
* description: 浏览器关闭
*/
public static void close(){
if (driver != null){
driver.close();
}
}
}
3.2 对UI元素查找器的封装
UiFinder这个类即为UI元素查找器的封装。了解Selenium的同学们都知道,Selenium在查找界面元素的时候都是通过特定的selector来定位的,它提供了8种selector:
- id
- name
- class name
- tag name
- link text
- partial link text
- xpath
- css selector
这8种定位方式在selenium中所对应的方法为:
- findElement(By.id())
- findElement(By.name())
- findElement(By.className())
- findElement(By.tagName())
- findElement(By.linkText())
- findElement(By.partialLinkText())
- findElement(By.xpath())
- findElement(By.cssSelector())
我的想法是,将这些selector进行统一的封装,封装成一个公共的UiFinder类,在新建界面元素的查找时,只需实例化UiFinder,调用其构造函数,将selector的类型以及值传进去即可。生成的UiFinder对象就可以被UI操作的各种方法所直接调用。代码如下:
package com.javaseleniumdemo.core;
import org.openqa.selenium.By;
import org.openqa.selenium.support.How;
/**
* @author Joy
*/
public class UiFinder {
public By by;
public How how;
public String selector;
public UiFinder(How how, String selector)
{
this.how = how;
this.selector = selector;
this.convertToBy(how, selector);
}
private void convertToBy(How how, String selector) {
switch (how) {
case ID:
this.by = By.id(selector);
break;
case XPATH:
this.by = By.xpath(selector);
break;
case CSS:
this.by = By.cssSelector(selector);
break;
case CLASS_NAME:
this.by = By.className(selector);
break;
case NAME:
this.by = By.name(selector);
break;
case TAG_NAME:
this.by = By.tagName(selector);
break;
case LINK_TEXT:
this.by = By.linkText(selector);
break;
case PARTIAL_LINK_TEXT:
this.by = By.partialLinkText(selector);
break;
default:
break;
}
}
}
3.3 对UI基础操作的封装
UiActions这个类即为对UI基础操作的封装,包括查找元素、点击元素、输入字符串、页面导航、截屏等等等等。
之所以要在selenium提供的基础方法上再封装一层,不光可以应对更加复杂的页面控件以及页面操作,最重要的是,还可以将显隐式等待和异常捕获等等封装到操作方法中,这样可以极大地提高界面操作的稳定性和容错性,也能大大降低业务实现代码的冗余度。代码如下:
package com.javaseleniumdemo.core;
import org.apache.commons.io.FileUtils;
import org.openqa.selenium.*;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedCondition;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Set;
/**
* @author Joy
*/
public class UiActions {
private static UiContext uiContext;
private static WebDriver driver;
static {
try {
uiContext = UiContext.getUiContext();
} catch (Exception e){
e.printStackTrace();
}
}
/**
* description: 用于为UI操作之间留出1秒的间隔
*/
public static void operationInterval(long millis) throws InterruptedException {
Thread.sleep(millis);
}
/**
* description: 初始化WebDriver
*/
public static void initialUiContext(){
uiContext.initialUiContext();
driver = UiContext.getDriver();
}
/**
* description: 关闭WebDriver,释放资源
*/
public static void disposeUiContext(){
uiContext.dispose();
}
/**
* description: 在界面寻找Element的基础方法,集成了隐式等待及异常处理
* @param finder
* @return WebElement
*/
public static WebElement findElement(final UiFinder finder){
WebElement result = null;
try{
if(finder.by != null){
result = uiContext.getDriverWait().until(new ExpectedCondition<WebElement>() {
@Override
public WebElement apply(WebDriver driver) {
return driver.findElement(finder.by);
}
});
}
} catch (UnhandledAlertException f){
String alertText = UiActions.acceptAlert();
System.out.println("Alert消息:" + alertText + " !");
} catch (Exception e){
e.printStackTrace();
System.out.println(e.getMessage());
}
return result;
}
/**
* description: 在界面寻找一组Element的基础方法,集成了隐式等待及异常处理
* @param finder: 注意此finder应该为可以定位到一组element的finder
* @return List<WebElement>
*/
public static List<WebElement> findElements(final UiFinder finder){
List<WebElement> results = null;
try{
if(finder.by != null){
results = uiContext.getDriverWait().until(new ExpectedCondition<List<WebElement>>() {
@Override
public List<WebElement> apply(WebDriver driver) {
return driver.findElements(finder.by);
}
});
}
} catch (UnhandledAlertException f){
String alertText = UiActions.acceptAlert();
System.out.println("Alert消息:" + alertText + " !");
} catch (Exception e){
e.printStackTrace();
System.out.println(e.getMessage());
}
return results;
}
/**
* description: 寻找,然后点击页面元素
* @param finder
*/
public static void click(UiFinder finder) throws InterruptedException {
WebElement element = findElement(finder);
Actions actions = new Actions(driver);
actions.moveToElement(element);
actions.click(element);
actions.build().perform();
operationInterval(1000);
}
/**
* description: 寻找,然后清空,再在输入框内输入text
* @param finder,text
*/
public static void sendKeys(UiFinder finder, String text) throws InterruptedException {
WebElement element = findElement(finder);
try {// 因为SendKeys默认是追加,所以先清空
element.clear();
}
catch (Exception e){
element.sendKeys("");
}
element.sendKeys(text);
operationInterval(1000);
}
/**
* description: 用于在dropdown menu中寻找并点击指定的option
* @param dropDownFinder,optionFinder
*/
public static void selectDropDownValueByText(UiFinder dropDownFinder, UiFinder optionFinder) throws InterruptedException {
//定位下拉框,并点击打开下拉框
WebElement dropDownElement = findElement(dropDownFinder);
dropDownElement.click();
operationInterval(1000);
WebElement optionElement = findElement(optionFinder);
optionElement.click();
}
/**
* description: 用于导航到指定的url
* @param url
*/
public static void navigateTo(String url) throws InterruptedException {
driver.get(url);
operationInterval(1000);
}
/**
* description: 用于判断页面是否存在指定的Element
* @param finder
* @return Boolean
*/
public static Boolean probeElement(UiFinder finder){
WebElement result = null;
try {
result = findElement(finder);
}catch (NoSuchElementException e){
System.out.println("Element not found : " + finder.toString());
e.printStackTrace();
}catch (Exception e){
e.printStackTrace();
}
return result != null;
}
/**
* description: 用于在页面的frame之间跳转,注意,只能从外部跳入frame,或者从父frame跳到子frame或更下级
* @param finder
*/
public static void switchToFrame(UiFinder finder) throws InterruptedException {
driver.switchTo().frame(findElement(finder));
operationInterval(1000);
}
/**
* description: 用于在页面的frame之间跳转,注意,只能从子frame跳到上面一层的父frame
*/
public static void switchToParentFrame() throws InterruptedException {
driver.switchTo().parentFrame();
operationInterval(1000);
}
/**
* description: 从任一frame中跳出,回到主content
*/
public static void switchToDefaultContent() throws InterruptedException {
driver.switchTo().defaultContent();
operationInterval(1000);
}
/**
* description: 当页面跳转产生多个窗口时,使用此方法,通过窗口的index来进行切换
* @param i
*/
public static void switchToWindow(Integer i){
//获得所有窗口句柄
Set<String> handles = driver.getWindowHandles();
if(handles != null && handles.size() > 1){
String handle = handles.toArray()[i].toString();
driver.switchTo().window(handle);
}
}
/**
* description: 关闭当前窗口
*/
public static void closeCurrentWindow(){
driver.close();
}
/**
* description: 关闭所有窗口
*/
public static void closeAllWindows(){
//获得所有窗口句柄
Set<String> handles = driver.getWindowHandles();
for (String handle: handles) {
driver.switchTo().window(handle);
driver.close();
}
}
/**
* description: 获取页面
*/
public static String getPageSource(){
return driver.getPageSource();
}
/**
* description: 检查页面的元素是否为enabled的(常用于判断是否还有下一页)
* @param finder
*/
public static Boolean checkIfElementIsEnabled(UiFinder finder){
Boolean result = false;
WebElement element = findElement(finder);
String classValue = element.getAttribute("class") == null? "" : element.getAttribute("class");
String disabledValue = element.getAttribute("disabled") == null? "" : element.getAttribute("disabled");
result = element.isEnabled()
&& !classValue.contains("disabled")
&& !classValue.contains("Disabled")
&& !disabledValue.contains("disabled");
return result;
}
/**
* description: 获取页面的元素的text
* @param finder
*/
public static String getElementText(UiFinder finder){
return findElement(finder).getText();
}
/**
* description: catch页面的alert弹窗,并accept
*/
public static String acceptAlert(){
String alertMessage = "";
try {
Alert alert = driver.switchTo().alert();
alertMessage = alert.getText();
alert.accept();
} catch(NoAlertPresentException e){
e.printStackTrace();
}
return alertMessage;
}
/**
* description: 截屏
*/
public static void captureScreenshot(){
File srcFile = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);
try {
FileUtils.copyFile(srcFile,new File("screenshot.png"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.编写业务代码,检查运行结果
由于篇幅不宜太长,所以我把“如何调用上述代码,检查运行结果”等内容放到下一篇博客《Java Selenium框架的开发和优化教程-02》中再做描述~~