Web UI自動化落地&一些思考與理解
前言
從八月到現(xiàn)在差不多四個月的時間,我這邊投入了一部分精力到UI自動化建設(shè)上面,目前來看成效還是可以的。在這個過程中也加深了對UI自動化的理解,所以總結(jié)下自己對UI自動化的認(rèn)識吧。
背景
七月底接手的項(xiàng)目是個流程系統(tǒng),每次發(fā)布都要點(diǎn)檢公共用例,國內(nèi)海外加起來有七個環(huán)境,每個差不多點(diǎn)檢半小時,上線壓力很大。然后Vmail使用頻次很高,之前還出過一些問題,加上一些優(yōu)化需求每次發(fā)布也要同步點(diǎn)檢。
然后就考慮做自動化來解放手工。因?yàn)轫?xiàng)目是個web流程系統(tǒng),比較注重前端的功能保障,加上前端頁面基本穩(wěn)定,大部分都是優(yōu)化功能,而項(xiàng)目本身的架構(gòu)也是前后端不分離的,所以比較適合UI自動化來做。
實(shí)施
1,準(zhǔn)備工作
確定了UI自動化之后,就要根據(jù)自身項(xiàng)目特點(diǎn)來實(shí)現(xiàn)需求。
首先是考慮哪些用例需要轉(zhuǎn)化成自動化腳本,第一版的用例集主要是平臺通用功能用例集和Vmail用例集,篩選過后大概有100來個檢查點(diǎn)。
然后一套代碼可以在多個環(huán)境下運(yùn)行,我這邊通過在testng.xml配置好對應(yīng)的參數(shù)和在測試基類BaseTest中進(jìn)行對應(yīng)環(huán)境的初始化工作,然后Jenkins中配置好各個環(huán)境一鍵執(zhí)行。
再就是可維護(hù)性和擴(kuò)展性,眾所周知,UI自動化的維護(hù)工作一直是個非常頭疼的問題,前期框架如果沒有搭好,后期維護(hù)會讓人做的想放棄,所以在搭建框架初期我就引入了PO模式和面向動作驅(qū)動(幾個關(guān)鍵字驅(qū)動組合而成的,顆粒度更大一級)。
2,框架搭建
2.1,環(huán)境搭建
環(huán)境搭建時,主要使用以下技術(shù):
SVN:管理代碼工程
TestNG:作為測試框架
Selenium3:Web UI自動化框架
Maven:管理依賴包
Log4j:管理日志
Dom4j:解析xml元素庫
ZTestReport:非常直觀的測試報(bào)告展示
2.2,界面模式和無頭模式
這個自動化工具因?yàn)檫€要給到運(yùn)維那邊使用,所以需要把自動化部署到Linux服務(wù)器上,因此要用到無頭模式,而Linux的webdriver驅(qū)動和windows不同,所以我這邊通過在testng.xml中配置操作系統(tǒng)和是否開啟無頭模式

然后在BaseTest中處理
if (isHeadless) {
logger.info("初始化無頭模式WebDriver驅(qū)動");
if (Objects.equals(runSystem, "linux")) {
logger.info("執(zhí)行環(huán)境為Linux系統(tǒng)");
initLinuxWebDriverOnHeadless(browserType);
} else if (Objects.equals(runSystem, "windows")) {
logger.info("執(zhí)行環(huán)境為Windows系統(tǒng)");
initWindowsWebDriverOnHeadless(browserType);
} else {
throw new RuntimeException("暫不支持您的操作系統(tǒng)");
}
// chrome無頭模式設(shè)置最大化無效,需要設(shè)置分辨率
logger.info("無頭模式--分辨率1920*1080");
driver.manage().window().setSize(new org.openqa.selenium.Dimension(1920, 1080));
} else {
logger.info("初始化界面模式WebDriver驅(qū)動");
initWindowsWebDriverOnUI(browserType);
logger.info("初始化Java Robot實(shí)例");
initJavaRobot();
logger.info("界面模式--最大化窗口");
driver.manage().window().maximize();
}
Linux下無頭模式WebDriver驅(qū)動初始化
/**
* 初始化Linux無頭模式WebDriver驅(qū)動
* @param browserType
*/
private void initLinuxWebDriverOnHeadless(String browserType) {
if ("chrome".equalsIgnoreCase(browserType)) {
// 設(shè)置chromedriver系統(tǒng)環(huán)境變量
System.setProperty("webdriver.chrome.driver", "chromedriver");
// Chrome headless模式:無瀏覽器界面模式
logger.info("啟動chrome無頭模式");
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addArguments("--no-sandbox");
chromeOptions.addArguments("--disable-dev-shm-usage");
chromeOptions.addArguments("--headless");
driver = new ChromeDriver(chromeOptions);
} else if ("firefox".equalsIgnoreCase(browserType)) {
// 設(shè)置geckodriver系統(tǒng)環(huán)境變量
System.setProperty("webdriver.gecko.driver", "geckodriver");
// Firefox headless模式:無瀏覽器界面模式
logger.info("啟動firefox無頭模式");
FirefoxOptions fOptions = new FirefoxOptions();
fOptions.addArguments("--headless");
driver = new FirefoxDriver(fOptions);
} else {
throw new RuntimeException("目前暫不支持" + browserType + "瀏覽器,請使用Chrmoe/Firefox");
}
}
Windows下UI界面模式和無頭模式
/**
* 初始化Windows界面模式WebDriver驅(qū)動
* @param browserType
*/
private void initWindowsWebDriverOnUI(String browserType) {
if ("chrome".equalsIgnoreCase(browserType)) {
// 設(shè)置chromedriver系統(tǒng)環(huán)境變量
System.setProperty("webdriver.chrome.driver", "chromedriver.exe");
// 初始化谷歌瀏覽器驅(qū)動
logger.info("啟動chrome界面模式");
driver = new ChromeDriver();
} else if ("firefox".equalsIgnoreCase(browserType)) {
// 設(shè)置geckodriver系統(tǒng)環(huán)境變量
System.setProperty("webdriver.gecko.driver", "geckodriver.exe");
// 初始化火狐瀏覽器驅(qū)動
logger.info("啟動firefox界面模式");
driver = new FirefoxDriver();
} else {
throw new RuntimeException("目前暫不支持" + browserType + "瀏覽器,請使用Chrmoe/Firefox/IE");
}
}
/**
* 初始化Windows無頭模式WebDriver驅(qū)動
* @param browserType
*/
private void initWindowsWebDriverOnHeadless(String browserType) {
if ("chrome".equalsIgnoreCase(browserType)) {
// 設(shè)置chromedriver系統(tǒng)環(huán)境變量
System.setProperty("webdriver.chrome.driver", "chromedriver.exe");
// Chrome headless模式:無瀏覽器界面模式
logger.info("啟動chrome無頭模式");
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addArguments("--headless");
driver = new ChromeDriver(chromeOptions);
} else if ("firefox".equalsIgnoreCase(browserType)) {
// 設(shè)置geckodriver系統(tǒng)環(huán)境變量
System.setProperty("webdriver.gecko.driver", "geckodriver.exe");
// Firefox headless模式:無瀏覽器界面模式
logger.info("啟動firefox無頭模式");
FirefoxOptions fOptions = new FirefoxOptions();
fOptions.addArguments("--headless");
driver = new FirefoxDriver(fOptions);
} else {
throw new RuntimeException("目前暫不支持" + browserType + "瀏覽器,請使用Chrmoe/Firefox");
}
}
這邊還用到了兩個類庫:js執(zhí)行器JavascriptExecutor和java機(jī)器人Robot
jsExe = (JavascriptExecutor) driver;// 有些Selenium很難實(shí)現(xiàn)或者實(shí)現(xiàn)不了的,可以通過執(zhí)行一段js腳本來達(dá)到所需。
robot = new Robot();// 機(jī)器人類可以模擬鼠標(biāo)和鍵盤動作,但是在linux系統(tǒng)無圖形化界面下使用不了。
2.3,PO模式實(shí)現(xiàn)
頁面對象:Page.java
package *.uiauto.bean; import java.util.List;
@Getter
@Setter
public class Page { private String keyword; private List<UIElement> uiElements; public Page() { super(); } public Page(String keyword, List<UIElement> uiElements) { super(); this.keyword = keyword; this.uiElements = uiElements; } }
UI元素對象:UIElement.java
package *.uiauto.bean;
@Getter
@Setter public class UIElement { // 關(guān)鍵字 private String keyword; // 定位方式 private String by; // 定位path private String path; // 定位時間 private Integer timeout; public UIElement() { } public UIElement(String keyword, String by, String path, Integer timeout) { this.keyword = keyword; this.by = by; this.path = path; this.timeout = timeout; } }
貼一點(diǎn)xml庫
<?xml version="1.0" encoding="UTF-8"?>
<Pages>
<Page keyword="通知">
<UIElement keyword="通知frame" by="cssSelector" path="#Tab_Common_List_TabItemFrame" timeout="10"/>
<UIElement keyword="第一條通知" by="cssSelector" path="#container > div.content > div.e-body > table > tbody > tr.focus > td.title > div" timeout="10"/>
<UIElement keyword="刷新" by="cssSelector" path="#btnRefresh" timeout="10"/>
<UIElement keyword="回復(fù)" by="cssSelector" path="#btnReply" timeout="10"/>
<UIElement keyword="轉(zhuǎn)發(fā)" by="cssSelector" path="#btnForwar" timeout="10"/>
<UIElement keyword="刪除" by="cssSelector" path="#btnDel" timeout="10"/>
<UIElement keyword="標(biāo)記為" by="cssSelector" path="#btnMark" timeout="10"/>
<UIElement keyword="移動到" by="cssSelector" path="#btnMove" timeout="10"/>
<UIElement keyword="近6個月" by="partialLinkText" path="近6個月" timeout="10"/>
<UIElement keyword="近1個月" by="partialLinkText" path="近1個月" timeout="10"/>
<UIElement keyword="未讀" by="cssSelector" path="#checkBox > div > label" timeout="10"/>
<UIElement keyword="搜索框" by="cssSelector" path="#txtSearchMail" timeout="10"/>
</Page>
</Pages>
定位工具類:UILibraryUtils.java
import org.apache.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import *.bean.Page;
import *.bean.UIElement;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class UILibraryUtils {
public static Logger logger = Logger.getLogger(UILibraryUtils.class);
// 創(chuàng)建一個集合保存所有的Page對象
public static List<Page> pageObjList = new ArrayList<>();
static {
pageObjList = loadPage("UILibrary.xml");
}
/**
* 加載UI對象庫
*
* @param filename UI庫文件
* @return
*/
public static List<Page> loadPage(String filename) {
// 解析xml文件
SAXReader reader = new SAXReader();
FileInputStream fis = null;
try {
fis = new FileInputStream(new File(filename));
Document document = reader.read(fis);
Element root = document.getRootElement();
List<Element> pageElements = root.elements("Page");
// 遍歷Page元素,封裝成Page對象并保存到集合中
for (Element pageElement : pageElements) {
String pageKeyword = pageElement.attributeValue("keyword");
List<Element> uiElements = pageElement.elements("UIElement");
List<UIElement> uiEleObjList = new ArrayList<>();
// 遍歷Page元素里所有的UIElement元素,封裝成UIElement對象并保存到集合中
for (Element uiElement : uiElements) {
String uiElementKeyword = uiElement.attributeValue("keyword");
String by = uiElement.attributeValue("by");
String path = uiElement.attributeValue("path");
int timeout = Integer.parseInt(uiElement.attributeValue("timeout"));
uiEleObjList.add(new UIElement(uiElementKeyword, by, path, timeout));
}
// 將Page對象保存到集合中
pageObjList.add(new Page(pageKeyword, uiEleObjList));
}
} catch (Exception e) {
throw new RuntimeException(String.format("讀取UILibrary.xml文件失敗"));
} finally {
// 關(guān)閉資源
try {
if (null != fis)
fis.close();
} catch (IOException e) {
logger.error("資源" + fis.getClass().getName() + "關(guān)閉失敗!");
e.printStackTrace();
}
}
return pageObjList;
}
/**
* @param pageKeyword
* @param uiElementKeyword
* @param flag 1:當(dāng)元素出現(xiàn)時通過關(guān)鍵字獲取單個元素
* 2:當(dāng)元素可見時通過關(guān)鍵字獲取單個元素
* 3:當(dāng)元素出現(xiàn)時通過關(guān)鍵字獲取多個元素
* 4:當(dāng)元素可見時通過關(guān)鍵字獲取多個元素
* @return
* @throws InterruptedException
*/
public static List<WebElement> getElementsByKeyword(String pageKeyword, String uiElementKeyword, int flag) throws InterruptedException {
List<WebElement> elements = null;
for (Page page : pageObjList) {
// 匹配頁面關(guān)鍵字
if (pageKeyword.equals(page.getKeyword())) {
List<UIElement> uiElements = page.getUiElements();
for (UIElement uiElement : uiElements) {
// 匹配元素關(guān)鍵字
if (uiElement.getKeyword().equals(uiElementKeyword)) {
String by = uiElement.getBy();
String path = uiElement.getPath();
Integer timeout = uiElement.getTimeout();
// 根據(jù)傳的flag調(diào)用對應(yīng)的獲取元素方式
switch (flag) {
case 1:
elements.add(getElementWhenPresent(by, path, timeout));
break;
case 2:
elements.add(getElementWhenVisible(by, path, timeout));
break;
case 3:
elements = getElementsWhenPresent(by, path, timeout);
break;
case 4:
elements = getElementsWhenVisible(by, path, timeout);
break;
}
}
}
}
}
return elements;
}
/**
* 當(dāng)元素出現(xiàn)時通過關(guān)鍵字獲取單個元素
*
* @param pageKeyword po頁面關(guān)鍵字
* @param uiElementKeyword 頁面元素關(guān)鍵字
* @return WebElement
*/
public static WebElement getElementsByKeywordWhenPresent(String pageKeyword, String uiElementKeyword) throws InterruptedException {
return getElementsByKeyword(pageKeyword, uiElementKeyword, 1).get(0);
}
/**
* 當(dāng)元素可見時通過關(guān)鍵字獲取單個元素
*
* @param pageKeyword po頁面關(guān)鍵字
* @param uiElementKeyword 頁面元素關(guān)鍵字
* @return WebElement
*/
public static WebElement getElementsByKeywordWhenVisible(String pageKeyword, String uiElementKeyword) throws InterruptedException {
return getElementsByKeyword(pageKeyword, uiElementKeyword, 2).get(0);
}
/**
* 當(dāng)元素出現(xiàn)時通過關(guān)鍵字獲取多個元素
*
* @param pageKeyword po頁面關(guān)鍵字
* @param uiElementKeyword 頁面元素關(guān)鍵字
* @return WebElement
*/
public static List<WebElement> getElementsByKeywordWhenPresent(String pageKeyword, String uiElementKeyword) throws InterruptedException {
return getElementsByKeyword(pageKeyword, uiElementKeyword, 3);
}
/**
* 當(dāng)元素可見時通過關(guān)鍵字獲取多個元素
*
* @param pageKeyword po頁面關(guān)鍵字
* @param uiElementKeyword 頁面元素關(guān)鍵字
* @return WebElement
*/
public static List<WebElement> getElementsByKeywordWhenVisible(String pageKeyword, String uiElementKeyword) throws InterruptedException {
return getElementsByKeyword(pageKeyword, uiElementKeyword, 4);
}
/**
* 當(dāng)元素出現(xiàn)時定位單個元素
*
* @param by 定位方式
* @param path 元素路徑
* @param timeout 定位超時時間
* @return
* @throws InterruptedException
*/
private static WebElement getElementWhenPresent(String by, String path, Integer timeout) throws InterruptedException {
WebElement element = null;
switch (by.toUpperCase()) {
case "ID":
element = WebDriverWaitUtils.getElementWhenPresent(By.id(path), timeout);
break;
case "CSSSELECTOR":
element = WebDriverWaitUtils.getElementWhenPresent(By.cssSelector(path), timeout);
break;
case "XPATH":
element = WebDriverWaitUtils.getElementWhenPresent(By.xpath(path), timeout);
break;
case "PARTIALLINKTEXT":
element = WebDriverWaitUtils.getElementWhenPresent(By.partialLinkText(path), timeout);
break;
case "LINKTEXT":
element = WebDriverWaitUtils.getElementWhenPresent(By.linkText(path), timeout);
break;
case "CLASSNAME":
element = WebDriverWaitUtils.getElementWhenPresent(By.className(path), timeout);
break;
case "NAME":
element = WebDriverWaitUtils.getElementWhenPresent(By.name(path), timeout);
break;
case "TAGNAME":
element = WebDriverWaitUtils.getElementWhenPresent(By.tagName(path), timeout);
break;
default:
logger.info("-----不支持的元素定位方式:" + by + "-----");
break;
}
return element;
}
/**
* 當(dāng)元素可見時定位單個元素
*
* @param by 定位方式
* @param path 元素路徑
* @param timeout 定位超時時間
* @return
* @throws InterruptedException
*/
private static WebElement getElementWhenVisible(String by, String path, Integer timeout) throws InterruptedException {
WebElement element = null;
switch (by.toUpperCase()) {
case "ID":
element = WebDriverWaitUtils.getElementWhenVisible(By.id(path), timeout);
break;
case "CSSSELECTOR":
element = WebDriverWaitUtils.getElementWhenVisible(By.cssSelector(path), timeout);
break;
case "XPATH":
element = WebDriverWaitUtils.getElementWhenVisible(By.xpath(path), timeout);
break;
case "PARTIALLINKTEXT":
element = WebDriverWaitUtils.getElementWhenVisible(By.partialLinkText(path), timeout);
break;
case "LINKTEXT":
element = WebDriverWaitUtils.getElementWhenVisible(By.linkText(path), timeout);
break;
case "CLASSNAME":
element = WebDriverWaitUtils.getElementWhenVisible(By.className(path), timeout);
break;
case "NAME":
element = WebDriverWaitUtils.getElementWhenVisible(By.name(path), timeout);
break;
case "TAGNAME":
element = WebDriverWaitUtils.getElementWhenVisible(By.tagName(path), timeout);
break;
default:
logger.info("-----不支持的元素定位方式:" + by + "-----");
break;
}
return element;
}
/**
* 當(dāng)元素出現(xiàn)時定位多個元素
*
* @param by 定位方式
* @param path 元素路徑
* @param timeout 定位超時時間
* @return
* @throws InterruptedException
*/
private static List<WebElement> getElementsWhenPresent(String by, String path, Integer timeout) throws InterruptedException {
List<WebElement> elements = null;
switch (by.toUpperCase()) {
case "ID":
elements = WebDriverWaitUtils.getElementsWhenPresent(By.id(path), timeout);
break;
case "CSSSELECTOR":
elements = WebDriverWaitUtils.getElementsWhenPresent(By.cssSelector(path), timeout);
break;
case "XPATH":
elements = WebDriverWaitUtils.getElementsWhenPresent(By.xpath(path), timeout);
break;
case "PARTIALLINKTEXT":
elements = WebDriverWaitUtils.getElementsWhenPresent(By.partialLinkText(path), timeout);
break;
case "LINKTEXT":
elements = WebDriverWaitUtils.getElementsWhenPresent(By.linkText(path), timeout);
break;
case "CLASSNAME":
elements = WebDriverWaitUtils.getElementsWhenPresent(By.className(path), timeout);
break;
case "NAME":
elements = WebDriverWaitUtils.getElementsWhenPresent(By.name(path), timeout);
break;
case "TAGNAME":
elements = WebDriverWaitUtils.getElementsWhenPresent(By.tagName(path), timeout);
break;
default:
logger.info("-----不支持的元素定位方式:" + by + "-----");
break;
}
return elements;
}
/**
* 當(dāng)元素可見時定位多個元素
*
* @param by 定位方式
* @param path 元素路徑
* @param timeout 定位超時時間
* @return
* @throws InterruptedException
*/
private static List<WebElement> getElementsWhenVisible(String by, String path, Integer timeout) throws InterruptedException {
List<WebElement> elements = null;
switch (by.toUpperCase()) {
case "ID":
elements = WebDriverWaitUtils.getElementsWhenVisible(By.id(path), timeout);
break;
case "CSSSELECTOR":
elements = WebDriverWaitUtils.getElementsWhenVisible(By.cssSelector(path), timeout);
break;
case "XPATH":
elements = WebDriverWaitUtils.getElementsWhenVisible(By.xpath(path), timeout);
break;
case "PARTIALLINKTEXT":
elements = WebDriverWaitUtils.getElementsWhenVisible(By.partialLinkText(path), timeout);
break;
case "LINKTEXT":
elements = WebDriverWaitUtils.getElementsWhenVisible(By.linkText(path), timeout);
break;
case "CLASSNAME":
elements = WebDriverWaitUtils.getElementsWhenVisible(By.className(path), timeout);
break;
case "NAME":
elements = WebDriverWaitUtils.getElementsWhenVisible(By.name(path), timeout);
break;
case "TAGNAME":
elements = WebDriverWaitUtils.getElementsWhenVisible(By.tagName(path), timeout);
break;
default:
logger.info("-----不支持的元素定位方式:" + by + "-----");
break;
}
return elements;
}
}
2.4,不同環(huán)境的配置管理
<!-- 無頭模式,true:無頭模式,false:界面模式 -->
<parameter name="isHeadless" value="true"></parameter>
<!-- 測試url -->
<parameter name="configUrl" value="http://*"></parameter>
<!-- *vmailUrl -->
<parameter name="cnVmailUrl" value="http://*/Portal/Email/Index"></parameter>
<!-- *vmailUrl -->
<parameter name="inVmailUrl" value="http://*/Portal/Email"></parameter>
<!-- *vmailUrl -->
<parameter name="idVmailUrl" value="http://*/Email/Index"></parameter>
<!-- *vmailUrl -->
<parameter name="bdVmailUrl" value="http://*/Portal/Email"></parameter>
<!-- *vmailUrl -->
<parameter name="usVmailUrl" value="http://*/Portal/Email"></parameter>
BaseTest類中處理
// 讀取所有要新建的表單路徑,并根據(jù)環(huán)境設(shè)置對應(yīng)的新建路徑
Map<String, Object> newFormModules = PropertiesUtils.load("NewFormModules.properties");
//根據(jù)不同環(huán)境URL來判斷讀取對應(yīng)json字典文件,并設(shè)置V郵件url
if (configUrl.contains("in")) {
logger.info("測試環(huán)境為*BPM");
dictMap = PropertiesUtils.load("IN_HomePageModuleName.properties");
vMailUrl = inVmailUrl;
newFormModulePath = (String) newFormModules.get("in");
} else if (configUrl.contains("id")) {
logger.info("測試環(huán)境為*BPM");
dictMap = PropertiesUtils.load("ID_HomePageModuleName.properties");
vMailUrl = idVmailUrl;
newFormModulePath = (String) newFormModules.get("id");
} else if (configUrl.contains("bd")) {
logger.info("測試環(huán)境為*BPM");
dictMap = PropertiesUtils.load("BD_HomePageModuleName.properties");
vMailUrl = bdVmailUrl;
newFormModulePath = (String) newFormModules.get("bd");
} else if (configUrl.contains("us")) {
logger.info("測試環(huán)境為*BPM");
dictMap = PropertiesUtils.load("US_HomePageModuleName.properties");
vMailUrl = usVmailUrl;
newFormModulePath = (String) newFormModules.get("us");
} else {
logger.info("測試環(huán)境為*BPM");
dictMap = PropertiesUtils.load("CN_HomePageModuleName.properties");
vMailUrl = cnVmailUrl;
newFormModulePath = (String) newFormModules.get("cn");
}
全局變量和測試執(zhí)行時間記錄
// 日志記錄器
public static Logger logger = Logger.getLogger(BaseTest.class);
// 保存首頁模塊字典信息集合
public static Map<String, Object> dictMap = new LinkedHashMap<>();
// 全局瀏覽器驅(qū)動對象
public static WebDriver driver;
// js腳本執(zhí)行器
public static JavascriptExecutor jsExe;
// 全局Robot對象
public static Robot robot;
// Vmail地址
public static String vMailUrl;
// 新建表單路徑
public static String newFormModulePath;
// 記錄本次測試執(zhí)行時間
private long beginTime;
private long endTime;
// 定義一個集合,方法名:失敗截圖path的映射關(guān)系,
public static Map<String, Object> methodMappingFailScreenShotPath = new HashMap<>();
// 記錄開始測試時間
@BeforeSuite
public void initSuite() {
logger.info("———————————測試開始———————————");
beginTime = System.currentTimeMillis();
}
//統(tǒng)計(jì)測試執(zhí)行時間
@Parameters(value = {"userCode"})
@AfterSuite
public void tearDowm(String userCode) throws InterruptedException {
// 有失敗/跳過用例發(fā)送V消息
if (ZTestReport.testsFail > 0 || ZTestReport.testsSkip > 0) {
SendMsgTest.send(userCode);
}
endTime = System.currentTimeMillis();
long exeTime = endTime - beginTime;
long exeSecond = exeTime / 1000;
logger.info("本次測試執(zhí)行時間為:" + exeSecond + "秒");
logger.info("———————————測試結(jié)束———————————");
}
2.5,分層解耦
先看工程目錄

各層功能已經(jīng)在圖上標(biāo)示。可以看到單獨(dú)抽出了一個頁面動作層,以前很多測試人員寫UI自動化,都是流水賬式的把各種定位、操作、斷言全部寫在一條用例里面,這樣其實(shí)存在幾點(diǎn)問題:
1,可讀性差,一屏幕全是定位、操作,需要看完一整條用例才知道這條用例是做什么的;
2,可維護(hù)性差,后期一處小改動,可能會涉及到幾條甚至幾十條用例都需要修改;
3,復(fù)用性基本沒有,其實(shí)對頁面的很多操作,都可以封裝成通用功能
4,難以調(diào)試,出現(xiàn)問題排查成本高
5,耦合帶來的其他各類問題
2.6,下面以一個用例為例,展示下代碼的結(jié)構(gòu):
測試用例:審批文檔
@Parameters({"configUrl", "username2", "password2"})
@Test(description = "審批文檔")
public void testReviewForm(String configUrl, String username2, String password2) throws InterruptedException {
logger.info("測試審批文檔");
// 登錄
logger.info("登錄"+configUrl);
driver.get(configUrl);
LoginAction.login(username2, password2);
// 進(jìn)入我的待辦
logger.info("進(jìn)入我的待辦");
UpComingAction.clickUpComing();
WindowAction.refreshWindow();
// 文檔審批
for (int i = 1; i < FormAction.reviewCount; i++) {//小于審批次數(shù):因?yàn)樽詈笠粋€節(jié)點(diǎn)是抄送,不需要審批
// 審批文檔
logger.info("審批文檔"+i+"次");
FormAction.reviewForm(FormAction.formTitle);
}
// 審批完以后查找此文檔
logger.info("斷言文檔是否審批完成");
boolean isSearched = UpComingAction.searchUpComingByFormTitle(FormAction.formTitle);
Assert.assertEquals(isSearched, false);
}
登錄動作:
/**
* 登錄動作
* @param username 用戶名
* @param password 密碼
* @throws InterruptedException
*/
public static void login(String username, String password) throws InterruptedException {
UILibraryUtils.getElementsByKeywordWhenVisible("登錄頁面","登錄用戶名").sendKeys(username);
UILibraryUtils.getElementsByKeywordWhenVisible("登錄頁面","登錄密碼").sendKeys(password);
UILibraryUtils.getElementsByKeywordWhenVisible("登錄頁面","登錄按鈕").click();
}
待辦動作:
/**
* 點(diǎn)擊“待辦”
*
* @throws InterruptedException
*/
public static void clickUpComing() throws InterruptedException {
// 點(diǎn)擊待辦
UILibraryUtils.getElementsByKeywordWhenVisible("導(dǎo)航欄", "待辦").click();
}
刷新窗口動作:
/**
* 刷新當(dāng)前窗口
*
* @throws InterruptedException
*/
public static void refreshWindow() throws InterruptedException {
driver.navigate().refresh();
Thread.sleep(2000);
}
審批動作:
/**
* 審批表單
*
* @param formTitle
* @throws InterruptedException
*/
public static void reviewForm(String formTitle) throws InterruptedException {
WebElement formEle = WebDriverWaitUtils.getElementWhenVisible10S(By.partialLinkText(formTitle));
formEle.click();
try {// 點(diǎn)擊待辦后有兩種樣式,第一種當(dāng)面頁打開,第二種新開一個窗口
// 默認(rèn)為第一種,try起來點(diǎn)擊跳轉(zhuǎn)到新頁面,如果被catch住說明本來就是在新頁面打開的待辦
UILibraryUtils.getElementsByKeywordWhenVisible("待辦頁面", "跳轉(zhuǎn)新待辦頁").click();
} catch (Exception e) {
// 切換窗口審批表單
String firstHandle = driver.getWindowHandle();
Set<String> handles = driver.getWindowHandles();
for (String handle : handles) {
if (!handle.equals(firstHandle)) {
driver.switchTo().window(handle);
try {
UILibraryUtils.getElementsByKeywordWhenVisible("文檔頁面", "同意").click();
Thread.sleep(3000);
} catch (Exception e2) {// 如果在新窗口定位失敗拋出異常,需要catch后返回到原始窗口,避免影響后續(xù)用例
WindowAction.refreshWindow();
driver.close();
}
driver.switchTo().window(firstHandle);
}
}
}
}
可以看到,測試用例是由幾個動作組裝而成的,一個審批只要10行代碼,非常簡潔。而且,隨著后期用例的拓張,不斷豐富的動作庫會讓你的寫腳本的效率大大提高。
2.7,失敗重試和失敗截圖
另外,由于UI自動化的不穩(wěn)定性,失敗有時是無可避免的,因此我這里也引入了失敗重試機(jī)制。
創(chuàng)建一個RetryAnalyzer實(shí)現(xiàn)IRetryAnalyzer,重寫retry方法
package *.uiauto.util;
import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;
public class RetryAnalyzer implements IRetryAnalyzer {
private int retry_count = 0; // 重試次數(shù)基值
private int max_retry_count = 2; // 最大重試次數(shù)
@Override
public boolean retry(ITestResult iTestResult) {
// 是否需要重試
if (retry_count < max_retry_count) {
retry_count++;
return true;
}
return false;
}
public void reset() {
retry_count = 0;
}
}
創(chuàng)建一個RetryListener監(jiān)聽器實(shí)現(xiàn)IAnnotationTransformer,重寫transform方法
package *.uiauto.listener;
import org.testng.IAnnotationTransformer;
import org.testng.IRetryAnalyzer;
import org.testng.annotations.ITestAnnotation;
import *.uiauto.util.RetryAnalyzer;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class RetryListener implements IAnnotationTransformer {
@Override
public void transform(ITestAnnotation iTestAnnotation, Class aClass, Constructor constructor, Method method) {
IRetryAnalyzer retry = iTestAnnotation.getRetryAnalyzer();
if (retry == null) {
iTestAnnotation.setRetryAnalyzer(RetryAnalyzer.class);
}
}
}
創(chuàng)建一個TestNGListener繼承TestListenerAdapter重寫onTestSuccess()和onTestFailure()
@Override
public void onTestSuccess(ITestResult tr) {
super.onTestSuccess(tr);
// 對于dataProvider的用例,每次成功后,重置Retry次數(shù)
RetryAnalyzer retry = (RetryAnalyzer) tr.getMethod().getRetryAnalyzer();
retry.reset();
}
@Override
public void onTestFailure(ITestResult tr) {
super.onTestFailure(tr);
// 對于dataProvider的用例,每次失敗后,重置Retry次數(shù)
RetryAnalyzer retry = (RetryAnalyzer) tr.getMethod().getRetryAnalyzer();
retry.reset();
}
testng.xml中配置監(jiān)聽器
<listeners>
<!-- 失敗重試監(jiān)聽器 -->
<listener class-name="*.uiauto.listener.RetryListener"></listener>
<!-- 監(jiān)聽器(失敗截圖等) -->
<listener class-name="*.uiauto.listener.TestNGListener"></listener>
<!-- 測試報(bào)告監(jiān)聽器 -->
<listener class-name="*.uiauto.util.ZTestReport"></listener>
<!--<listener class-name="*.uiauto.util.ExtentTestNGIReporterListener"></listener>-->
</listeners>
重試次數(shù)自己根據(jù)需要配置,當(dāng)然了,失敗重試也有一定的局限性,設(shè)計(jì)用例時需要考慮每條用例失敗了以后再次跑的時候不會受到其他用例影響。
另一個就是失敗截圖了,我用的ZTestReport并不支持失敗截圖展示,所以做了二次修改。思路就是將當(dāng)前用例名稱和失敗截圖的圖片路徑建立映射關(guān)系存到map集合中,然后在報(bào)告生成時根據(jù)用例名稱獲取圖片路徑,放到a標(biāo)簽href屬性中,點(diǎn)擊就可以查看失敗截圖了。
var failScreenShotHref = "";
if (typeof(n["failScreenShotHref"]) != "undefined" && n["failScreenShotHref"].valueOf("string") && n["failScreenShotHref"] != '') {
failScreenShotHref = "<td><a href='" + n["failScreenShotHref"] + "'>點(diǎn)擊查看" + "</a></td>"
} else {
failScreenShotHref = "<td>無</td>";
}
在TestNGListener的OnTestFailure方法中加入
// 失敗截圖把方法名傳進(jìn)去存入集合,建立方法名:失敗截圖路徑映射關(guān)系
ScreenShotUtils screenShotUtils = new ScreenShotUtils(BaseTest.driver, tr.getName());
screenShotUtils.getScreenShot();
截圖工具類
import org.apache.log4j.Logger;
import org.apache.maven.shared.utils.io.FileUtils;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import *.uiauto.test.BaseTest;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ScreenShotUtils {
private WebDriver driver;
// 失敗的方法名
private String failMethodName;
// 測試失敗截屏保存的路徑
private String path;
public static Logger logger = Logger.getLogger(ScreenShotUtils.class);
public ScreenShotUtils(WebDriver driver, String failMethodName) {
this.driver = driver;
this.failMethodName = failMethodName;
path = System.getProperty("user.dir") + "\\snapshot\\" + failMethodName + "_" + getCurrentTime() + ".png";
}
public void getScreenShot() {
if (driver instanceof TakesScreenshot) {
TakesScreenshot shot = (TakesScreenshot) driver;
File screen = shot.getScreenshotAs(OutputType.FILE);
File screenFile = new File(path);
try {
FileUtils.copyFile(screen, screenFile);
BaseTest.methodMappingFailScreenShotPath.put(failMethodName, path);
logger.info("截圖保存的路徑:" + path);
} catch (Exception e) {
logger.error("截圖失敗");
e.printStackTrace();
}
}
}
/**
* 獲取當(dāng)前時間
*/
public String getCurrentTime() {
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss");
String currentTime = sdf.format(date);
return currentTime;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
}
3,碰到的坑
坑一:有些元素,使用界面模式可以定位到,使用無頭模式竟然定位不到,解決方案是通過js腳本執(zhí)行器來執(zhí)行一段js達(dá)到要的效果
坑二:無頭模式下,Chrome瀏覽器最大化size無效,后面在Testerhome里看到了是要設(shè)置分辨率才行,沒事多逛逛論壇還是有收獲的
坑三:海外環(huán)境服務(wù)器有時網(wǎng)絡(luò)很慢,失敗率比較高,因此引入失敗重試
坑四:第一次在Linux上搭建環(huán)境遇到的各種坑,服務(wù)器沒有外網(wǎng),依賴手動導(dǎo)入,Linux權(quán)限等等各種問題,感慨:會搜索真的是非常重要
還有一些小坑就不一一贅述了,還是那句話,要善于搜索
4,成果
首先就是解放了每次發(fā)版的三四個小時的手工點(diǎn)檢工作
第二就是平臺組開發(fā)提測/改完bug后的自動化回歸驗(yàn)證
第三就是臨時一些需要補(bǔ)發(fā)的版本可以在發(fā)布完就構(gòu)建運(yùn)行驗(yàn)證
第四個就是個人價值的體現(xiàn)和能力的提升了
5,總結(jié)與思考
UI自動化,一個很重要的點(diǎn)就是把各種action封裝好,提高適配性,后面用例很多就是基于各種action的拼接組裝
另外js我覺得非常有必要學(xué)一下(Cypress框架就是基于js的),起碼jQuery庫的一些api要會用,以下是我常用到過的幾個api:鼠標(biāo)移到該元素上:$("#id").mouseover()、鼠標(biāo)右擊:$("#id").contextmenu()、鼠標(biāo)單擊:$("#id").click()、鼠標(biāo)雙擊:$("#id").dblclick()、取元素集合的第一個元素:$("#id").eq(0)、xpath取最后一個:span[last()] 等等。
這也算是第一次真正把UI自動化落地到公司項(xiàng)目的實(shí)際使用上了,跟學(xué)習(xí)時練手的小項(xiàng)目完全不一樣,會有各種各樣的坑等著你去解決,實(shí)踐才是檢驗(yàn)?zāi)芰Φ奈ㄒ粯?biāo)準(zhǔn)!

浙公網(wǎng)安備 33010602011771號