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

然后在BaseTest中處理
if (isHeadless) {
logger.info("初始化無頭模式WebDriver驅動");
if (Objects.equals(runSystem, "linux")) {
logger.info("執行環境為Linux系統");
initLinuxWebDriverOnHeadless(browserType);
} else if (Objects.equals(runSystem, "windows")) {
logger.info("執行環境為Windows系統");
initWindowsWebDriverOnHeadless(browserType);
} else {
throw new RuntimeException("暫不支持您的操作系統");
}
// chrome無頭模式設置最大化無效,需要設置分辨率
logger.info("無頭模式--分辨率1920*1080");
driver.manage().window().setSize(new org.openqa.selenium.Dimension(1920, 1080));
} else {
logger.info("初始化界面模式WebDriver驅動");
initWindowsWebDriverOnUI(browserType);
logger.info("初始化Java Robot實例");
initJavaRobot();
logger.info("界面模式--最大化窗口");
driver.manage().window().maximize();
}
Linux下無頭模式WebDriver驅動初始化
/**
* 初始化Linux無頭模式WebDriver驅動
* @param browserType
*/
private void initLinuxWebDriverOnHeadless(String browserType) {
if ("chrome".equalsIgnoreCase(browserType)) {
// 設置chromedriver系統環境變量
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)) {
// 設置geckodriver系統環境變量
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驅動
* @param browserType
*/
private void initWindowsWebDriverOnUI(String browserType) {
if ("chrome".equalsIgnoreCase(browserType)) {
// 設置chromedriver系統環境變量
System.setProperty("webdriver.chrome.driver", "chromedriver.exe");
// 初始化谷歌瀏覽器驅動
logger.info("啟動chrome界面模式");
driver = new ChromeDriver();
} else if ("firefox".equalsIgnoreCase(browserType)) {
// 設置geckodriver系統環境變量
System.setProperty("webdriver.gecko.driver", "geckodriver.exe");
// 初始化火狐瀏覽器驅動
logger.info("啟動firefox界面模式");
driver = new FirefoxDriver();
} else {
throw new RuntimeException("目前暫不支持" + browserType + "瀏覽器,請使用Chrmoe/Firefox/IE");
}
}
/**
* 初始化Windows無頭模式WebDriver驅動
* @param browserType
*/
private void initWindowsWebDriverOnHeadless(String browserType) {
if ("chrome".equalsIgnoreCase(browserType)) {
// 設置chromedriver系統環境變量
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)) {
// 設置geckodriver系統環境變量
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執行器JavascriptExecutor和java機器人Robot
jsExe = (JavascriptExecutor) driver;// 有些Selenium很難實現或者實現不了的,可以通過執行一段js腳本來達到所需。
robot = new Robot();// 機器人類可以模擬鼠標和鍵盤動作,但是在linux系統無圖形化界面下使用不了。
2.3,PO模式實現
頁面對象: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 { // 關鍵字 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; } }
貼一點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="回復" by="cssSelector" path="#btnReply" timeout="10"/>
<UIElement keyword="轉發" by="cssSelector" path="#btnForwar" timeout="10"/>
<UIElement keyword="刪除" by="cssSelector" path="#btnDel" timeout="10"/>
<UIElement keyword="標記為" 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);
// 創建一個集合保存所有的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 {
// 關閉資源
try {
if (null != fis)
fis.close();
} catch (IOException e) {
logger.error("資源" + fis.getClass().getName() + "關閉失敗!");
e.printStackTrace();
}
}
return pageObjList;
}
/**
* @param pageKeyword
* @param uiElementKeyword
* @param flag 1:當元素出現時通過關鍵字獲取單個元素
* 2:當元素可見時通過關鍵字獲取單個元素
* 3:當元素出現時通過關鍵字獲取多個元素
* 4:當元素可見時通過關鍵字獲取多個元素
* @return
* @throws InterruptedException
*/
public static List<WebElement> getElementsByKeyword(String pageKeyword, String uiElementKeyword, int flag) throws InterruptedException {
List<WebElement> elements = null;
for (Page page : pageObjList) {
// 匹配頁面關鍵字
if (pageKeyword.equals(page.getKeyword())) {
List<UIElement> uiElements = page.getUiElements();
for (UIElement uiElement : uiElements) {
// 匹配元素關鍵字
if (uiElement.getKeyword().equals(uiElementKeyword)) {
String by = uiElement.getBy();
String path = uiElement.getPath();
Integer timeout = uiElement.getTimeout();
// 根據傳的flag調用對應的獲取元素方式
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;
}
/**
* 當元素出現時通過關鍵字獲取單個元素
*
* @param pageKeyword po頁面關鍵字
* @param uiElementKeyword 頁面元素關鍵字
* @return WebElement
*/
public static WebElement getElementsByKeywordWhenPresent(String pageKeyword, String uiElementKeyword) throws InterruptedException {
return getElementsByKeyword(pageKeyword, uiElementKeyword, 1).get(0);
}
/**
* 當元素可見時通過關鍵字獲取單個元素
*
* @param pageKeyword po頁面關鍵字
* @param uiElementKeyword 頁面元素關鍵字
* @return WebElement
*/
public static WebElement getElementsByKeywordWhenVisible(String pageKeyword, String uiElementKeyword) throws InterruptedException {
return getElementsByKeyword(pageKeyword, uiElementKeyword, 2).get(0);
}
/**
* 當元素出現時通過關鍵字獲取多個元素
*
* @param pageKeyword po頁面關鍵字
* @param uiElementKeyword 頁面元素關鍵字
* @return WebElement
*/
public static List<WebElement> getElementsByKeywordWhenPresent(String pageKeyword, String uiElementKeyword) throws InterruptedException {
return getElementsByKeyword(pageKeyword, uiElementKeyword, 3);
}
/**
* 當元素可見時通過關鍵字獲取多個元素
*
* @param pageKeyword po頁面關鍵字
* @param uiElementKeyword 頁面元素關鍵字
* @return WebElement
*/
public static List<WebElement> getElementsByKeywordWhenVisible(String pageKeyword, String uiElementKeyword) throws InterruptedException {
return getElementsByKeyword(pageKeyword, uiElementKeyword, 4);
}
/**
* 當元素出現時定位單個元素
*
* @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;
}
/**
* 當元素可見時定位單個元素
*
* @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;
}
/**
* 當元素出現時定位多個元素
*
* @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;
}
/**
* 當元素可見時定位多個元素
*
* @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,不同環境的配置管理
<!-- 無頭模式,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類中處理
// 讀取所有要新建的表單路徑,並根據環境設置對應的新建路徑
Map<String, Object> newFormModules = PropertiesUtils.load("NewFormModules.properties"); //根據不同環境URL來判斷讀取對應json字典文件,並設置V郵件url if (configUrl.contains("in")) { logger.info("測試環境為*BPM"); dictMap = PropertiesUtils.load("IN_HomePageModuleName.properties"); vMailUrl = inVmailUrl; newFormModulePath = (String) newFormModules.get("in"); } else if (configUrl.contains("id")) { logger.info("測試環境為*BPM");
dictMap = PropertiesUtils.load("ID_HomePageModuleName.properties"); vMailUrl = idVmailUrl; newFormModulePath = (String) newFormModules.get("id"); } else if (configUrl.contains("bd")) { logger.info("測試環境為*BPM");
dictMap = PropertiesUtils.load("BD_HomePageModuleName.properties"); vMailUrl = bdVmailUrl; newFormModulePath = (String) newFormModules.get("bd"); } else if (configUrl.contains("us")) { logger.info("測試環境為*BPM");
dictMap = PropertiesUtils.load("US_HomePageModuleName.properties"); vMailUrl = usVmailUrl; newFormModulePath = (String) newFormModules.get("us"); } else { logger.info("測試環境為*BPM"); dictMap = PropertiesUtils.load("CN_HomePageModuleName.properties"); vMailUrl = cnVmailUrl; newFormModulePath = (String) newFormModules.get("cn"); }
全局變量和測試執行時間記錄
// 日志記錄器
public static Logger logger = Logger.getLogger(BaseTest.class);
// 保存首頁模塊字典信息集合
public static Map<String, Object> dictMap = new LinkedHashMap<>(); // 全局瀏覽器驅動對象 public static WebDriver driver; // js腳本執行器 public static JavascriptExecutor jsExe; // 全局Robot對象 public static Robot robot; // Vmail地址 public static String vMailUrl; // 新建表單路徑 public static String newFormModulePath; // 記錄本次測試執行時間 private long beginTime; private long endTime; // 定義一個集合,方法名:失敗截圖path的映射關系, public static Map<String, Object> methodMappingFailScreenShotPath = new HashMap<>(); // 記錄開始測試時間 @BeforeSuite public void initSuite() { logger.info("———————————測試開始———————————"); beginTime = System.currentTimeMillis(); } //統計測試執行時間 @Parameters(value = {"userCode"}) @AfterSuite public void tearDowm(String userCode) throws InterruptedException { // 有失敗/跳過用例發送V消息 if (ZTestReport.testsFail > 0 || ZTestReport.testsSkip > 0) { SendMsgTest.send(userCode); } endTime = System.currentTimeMillis(); long exeTime = endTime - beginTime; long exeSecond = exeTime / 1000; logger.info("本次測試執行時間為:" + exeSecond + "秒"); logger.info("———————————測試結束———————————"); }
2.5,分層解耦
先看工程目錄

各層功能已經在圖上標示。可以看到單獨抽出了一個頁面動作層,以前很多測試人員寫UI自動化,都是流水賬式的把各種定位、操作、斷言全部寫在一條用例里面,這樣其實存在幾點問題:
1,可讀性差,一屏幕全是定位、操作,需要看完一整條用例才知道這條用例是做什么的;
2,可維護性差,后期一處小改動,可能會涉及到幾條甚至幾十條用例都需要修改;
3,復用性基本沒有,其實對頁面的很多操作,都可以封裝成通用功能
4,難以調試,出現問題排查成本高
5,耦合帶來的其他各類問題
2.6,下面以一個用例為例,展示下代碼的結構:
測試用例:審批文檔
@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); // 進入我的待辦 logger.info("進入我的待辦"); UpComingAction.clickUpComing(); WindowAction.refreshWindow(); // 文檔審批 for (int i = 1; i < FormAction.reviewCount; i++) {//小於審批次數:因為最后一個節點是抄送,不需要審批 // 審批文檔 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();
}
待辦動作:
/**
* 點擊“待辦”
*
* @throws InterruptedException
*/
public static void clickUpComing() throws InterruptedException {
// 點擊待辦
UILibraryUtils.getElementsByKeywordWhenVisible("導航欄", "待辦").click(); }
刷新窗口動作:
/**
* 刷新當前窗口
*
* @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 {// 點擊待辦后有兩種樣式,第一種當面頁打開,第二種新開一個窗口 // 默認為第一種,try起來點擊跳轉到新頁面,如果被catch住說明本來就是在新頁面打開的待辦 UILibraryUtils.getElementsByKeywordWhenVisible("待辦頁面", "跳轉新待辦頁").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后返回到原始窗口,避免影響后續用例 WindowAction.refreshWindow(); driver.close(); } driver.switchTo().window(firstHandle); } } } }
可以看到,測試用例是由幾個動作組裝而成的,一個審批只要10行代碼,非常簡潔。而且,隨着后期用例的拓張,不斷豐富的動作庫會讓你的寫腳本的效率大大提高。
2.7,失敗重試和失敗截圖
另外,由於UI自動化的不穩定性,失敗有時是無可避免的,因此我這里也引入了失敗重試機制。
創建一個RetryAnalyzer實現IRetryAnalyzer,重寫retry方法
package *.uiauto.util;
import org.testng.IRetryAnalyzer; import org.testng.ITestResult; public class RetryAnalyzer implements IRetryAnalyzer { private int retry_count = 0; // 重試次數基值 private int max_retry_count = 2; // 最大重試次數 @Override public boolean retry(ITestResult iTestResult) { // 是否需要重試 if (retry_count < max_retry_count) { retry_count++; return true; } return false; } public void reset() { retry_count = 0; } }
創建一個RetryListener監聽器實現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); } } }
創建一個TestNGListener繼承TestListenerAdapter重寫onTestSuccess()和onTestFailure()
@Override
public void onTestSuccess(ITestResult tr) { super.onTestSuccess(tr); // 對於dataProvider的用例,每次成功后,重置Retry次數 RetryAnalyzer retry = (RetryAnalyzer) tr.getMethod().getRetryAnalyzer(); retry.reset(); } @Override public void onTestFailure(ITestResult tr) { super.onTestFailure(tr); // 對於dataProvider的用例,每次失敗后,重置Retry次數 RetryAnalyzer retry = (RetryAnalyzer) tr.getMethod().getRetryAnalyzer(); retry.reset(); }
testng.xml中配置監聽器
<listeners>
<!-- 失敗重試監聽器 -->
<listener class-name="*.uiauto.listener.RetryListener"></listener>
<!-- 監聽器(失敗截圖等) -->
<listener class-name="*.uiauto.listener.TestNGListener"></listener>
<!-- 測試報告監聽器 -->
<listener class-name="*.uiauto.util.ZTestReport"></listener>
<!--<listener class-name="*.uiauto.util.ExtentTestNGIReporterListener"></listener>-->
</listeners>
重試次數自己根據需要配置,當然了,失敗重試也有一定的局限性,設計用例時需要考慮每條用例失敗了以后再次跑的時候不會受到其他用例影響。
另一個就是失敗截圖了,我用的ZTestReport並不支持失敗截圖展示,所以做了二次修改。思路就是將當前用例名稱和失敗截圖的圖片路徑建立映射關系存到map集合中,然后在報告生成時根據用例名稱獲取圖片路徑,放到a標簽href屬性中,點擊就可以查看失敗截圖了。
var failScreenShotHref = "";
if (typeof(n["failScreenShotHref"]) != "undefined" && n["failScreenShotHref"].valueOf("string") && n["failScreenShotHref"] != '') { failScreenShotHref = "<td><a href='" + n["failScreenShotHref"] + "'>點擊查看" + "</a></td>" } else { failScreenShotHref = "<td>無</td>"; }
在TestNGListener的OnTestFailure方法中加入
// 失敗截圖把方法名傳進去存入集合,建立方法名:失敗截圖路徑映射關系
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(); } } } /** * 獲取當前時間 */ 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腳本執行器來執行一段js達到要的效果
坑二:無頭模式下,Chrome瀏覽器最大化size無效,后面在Testerhome里看到了是要設置分辨率才行,沒事多逛逛論壇還是有收獲的
坑三:海外環境服務器有時網絡很慢,失敗率比較高,因此引入失敗重試
坑四:第一次在Linux上搭建環境遇到的各種坑,服務器沒有外網,依賴手動導入,Linux權限等等各種問題,感慨:會搜索真的是非常重要
還有一些小坑就不一一贅述了,還是那句話,要善於搜索
4,成果
首先就是解放了每次發版的三四個小時的手工點檢工作
第二就是平台組開發提測/改完bug后的自動化回歸驗證
第三就是臨時一些需要補發的版本可以在發布完就構建運行驗證
第四個就是個人價值的體現和能力的提升了
5,總結與思考
UI自動化,一個很重要的點就是把各種action封裝好,提高適配性,后面用例很多就是基於各種action的拼接組裝
另外js我覺得非常有必要學一下(Cypress框架就是基於js的),起碼jQuery庫的一些api要會用,以下是我常用到過的幾個api:鼠標移到該元素上:$("#id").mouseover()、鼠標右擊:$("#id").contextmenu()、鼠標單擊:$("#id").click()、鼠標雙擊:$("#id").dblclick()、取元素集合的第一個元素:$("#id").eq(0)、xpath取最后一個:span[last()] 等等。
這也算是第一次真正把UI自動化落地到公司項目的實際使用上了,跟學習時練手的小項目完全不一樣,會有各種各樣的坑等着你去解決,實踐才是檢驗能力的唯一標准!
