前言
从八月到现在差不多四个月的时间,我这边投入了一部分精力到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自动化落地到公司项目的实际使用上了,跟学习时练手的小项目完全不一样,会有各种各样的坑等着你去解决,实践才是检验能力的唯一标准!