自动化测试--实现一套完全解耦的测试框架
Posted clairejing
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自动化测试--实现一套完全解耦的测试框架相关的知识,希望对你有一定的参考价值。
之前博客中已经将笔者实现的框架进行过简单介绍,在使用过程中,对以下几点提出优化:
1.页面URL和页面的定位信息保存不同的配置文件中-----------整合到一个配置文件中,相应的配置文件解析做出调整
2.将项目部署到Jenkins之后,出现Chrome驱动启动失败的问题(通过Jenkins运行时,会去找Chrome的chrome.exe)-----------修改driver的配置文件并对其解析类进行一些修改
3.在一个测试case中,对元素进行定位,每次都要传入多个参数(页面配置信息文件地址、页面名称、元素描述信息),会使得代码可读性不够高----------自定义一个TargetPage注解,将页面配置信息的文件地址、页面描述保存在其中。
4.抛弃比较丑的reportng报告,使用Allure2生成测试报告
5.不同的环境连接不同的数据库,为了解决可以读取不同数据库问题,修改了数据库配置文件,使用添加前缀的方式来指定数据库(数据量比较大,参数比较多时,笔者倾向于将测试数据放到数据库中。并且在计划使用SpringMVC实现一套自动化测试工具,这也是为其做准备)。
为了使大家能更清晰的理解该套框架,下面将UML图提供出来:
下面上源码:
1.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.claire.jing</groupId> <artifactId>UIAutoTest0928</artifactId> <version>1.0-SNAPSHOT</version> <properties> <aspectj.version>1.8.10</aspectj.version> </properties> <dependencies> <dependency> <groupId>io.qameta.allure</groupId> <artifactId>allure-testng</artifactId> <version>2.6.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>${aspectj.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java --> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>3.4.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.testng/testng --> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>6.14.3</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/dom4j/dom4j --> <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency> <!-- https://mvnrepository.com/artifact/log4j/log4j --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>3.17</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.5</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.11</version> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.10</version> <configuration> <!--设置参数命令行--> <argLine> -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" </argLine> <systemPropertyVariables> <!--是否忽略html,解释见下图。与之后在reportNg报告上显示截图相关。--> <org.uncommons.reportng.escape-output>false</org.uncommons.reportng.escape-output> </systemPropertyVariables> <!--测试失败后,是否忽略并继续测试--> <testFailureIgnore>true</testFailureIgnore> <argLine> -Dfile.encoding=UTF-8 </argLine> <suiteXmlFiles> <!--代表的是要执行的测试套件名称--> <suiteXmlFile>src/test/resources/testNG.xml</suiteXmlFile> </suiteXmlFiles> </configuration> </plugin> </plugins> <finalName>activiti-study</finalName> <resources> <resource> <directory>${basedir}</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources> </build> </project>
2.driver配置文件以及解析文件的util
<?xml version="1.0" encoding="UTF-8"?> <drivers index="0"> <!--Chrome浏览器的配置--> <driver name="org.openqa.selenium.chrome.ChromeDriver" index="0"> <properties> <property> <name>webdriver.chrome.driver</name> <value>{resourcespath}chromedriver.exe</value> </property> </properties> <options> <option> <name>C:/Users/Samps/AppData/Local/Google/Chrome/Application/chrome.exe</name> </option> </options> </driver> <!--为3.xselenium提供的配置文件,是需要driver的--> <driver name="org.openqa.selenium.firefox.FirefoxDriver" index="1"> <properties> <property> <name>webdriver.gecko.driver</name> <value>{resourcespath}geckodriver.exe</value> </property> <property> <name>webdriver.firefox.bin</name> <value>C:Program FilesMozilla Firefoxfirefox.exe</value> </property> </properties> </driver> <!--为selenium2.x提供的配置文件,内部继承了firfox的驱动,不需要设置驱动地址--> <driver name="org.openqa.selenium.firefox.FirefoxDriver" index="2"> <properties> <property> <name>webdriver.firefox.bin</name> <value>C:Program FilesMozilla Firefoxfirefox.exe</value> </property> </properties> </driver> <!--IE浏览器的配置--> <driver name="org.openqa.selenium.ie.InternetExplorerDriver" index="3"> <properties> <property> <name>webdriver.ie.driver</name> <value>{resourcespath}IEDriverServer.exe</value> </property> </properties> <capabilities> <capability> <name>InternetExplorerDriver.IGNORE_ZOOM_SETTING</name> </capability> <capability> <name>InternetExplorerDriver.INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS</name> </capability> </capabilities> </driver> </drivers>
package com.claire.jing.utils; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.remote.DesiredCapabilities; import java.io.InputStream; import java.util.List; public class GetDriverUtil { /** * example * @param args */ public static void main(String[] args) { WebDriver driver = getDriver(); driver.quit(); } /** * 启动浏览器,获取到浏览器驱动。 * 支持浏览器类型为:火狐,谷歌,IE。 * 类型设置在drivercon.xml文件中。修改根节点index值即可启动对应的浏览器 * @return */ public static WebDriver getDriver() { SAXReader reader = new SAXReader(); InputStream in = GetDriverUtil.class.getResourceAsStream("/drivercon.xml"); Document document = null; try { document = reader.read(in); } catch (DocumentException e) { e.printStackTrace(); } Element rootElement = document.getRootElement();//获取到根节点 String targetIndex = rootElement.attributeValue("index");//获取到根节点中的driver List<Element> drivers = rootElement.elements("driver");//获取到所有的driver节点 String className = null; Element targetDriver = null; for (Element driver : drivers) { String index = driver.attributeValue("index"); if (index.equals(targetIndex)){ className =driver.attributeValue("name");//获取到目标浏览器驱动的类全名 targetDriver = driver; break; } } Class<?> driverClass = null;//反射获取到驱动的类引用 try { driverClass = Class.forName(className); } catch (ClassNotFoundException e) { e.printStackTrace(); } Element properties = targetDriver.element("properties");//获取到目标驱动的properties节点 List<Element> allProperties = properties.elements("property");//获取到目标驱动的所有property节点,即:要设置的系统属性 //遍历并设置所有的系统属性 for (Element property : allProperties) { String propertyName = property.element("name").getText(); String path = property.element("value").getText(); String propertyValue = pathFormat(path); System.setProperty(propertyName,propertyValue); } Element capabilities = targetDriver.element("capabilities");//获取到目标驱动的capabilities if (capabilities != null){ List<Element> allCapabilities = capabilities.elements("capability");//获取到目标驱动的所有capability节点,即:要设置IE浏览器的能力 for (Element capability : allCapabilities) { String capabilieyToSet = capability.element("name").getText(); DesiredCapabilities desiredCapabilities = new DesiredCapabilities(); desiredCapabilities.setCapability(capabilieyToSet,true); } } Element optionsElement = targetDriver.element("options");//获取到目标驱动下的options ChromeOptions chromeOptions =null; if (optionsElement != null){ List<Element> allOptions = optionsElement.elements("option"); for (Element option : allOptions) { String binaryPath = option.element("name").getText(); System.out.println("chrome的binary地址为"+binaryPath); chromeOptions = new ChromeOptions(); chromeOptions.setBinary(binaryPath); } System.out.println("创建的是Chrome的驱动哦"); return new ChromeDriver(chromeOptions); } WebDriver driver = null; try { driver = (WebDriver) driverClass.newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return driver; } /** * 格式化driver文件的位置 * * @param path * 传入的浏览器路径设置值 * 当带有占位符resourcespath时,解析为"src/test/resources/" * 没有占位符时,原样返回 * @return * 返回格式化后的driver路径 */ public static String pathFormat(String path){ if (path.contains("{resourcespath}")){ path = "src/test/resources/"+path.substring(15); System.out.println("格式化之后的路径为"+path); return path; } return path; } }
3.BaseTester
package com.claire.jing.base; import com.claire.jing.annocations.TargetPage; import com.claire.jing.annocations.TargetTestData; import com.claire.jing.elementLocator.ElementInfo; import com.claire.jing.utils.ElementLocatorUtil; import com.claire.jing.utils.ExcelDataproviderUtil; import com.claire.jing.utils.GetDriverUtil; import com.claire.jing.utils.MysqlDataproviderUtil; import org.apache.log4j.Logger; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedCondition; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.Select; import org.openqa.selenium.support.ui.WebDriverWait; import org.testng.Assert; import org.testng.ITestNGMethod; import org.testng.ITestResult; import org.testng.Reporter; import org.testng.annotations.AfterSuite; import org.testng.annotations.BeforeSuite; import org.testng.annotations.DataProvider; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Iterator; import java.util.List; import java.util.Set; public class BaseTester { public static WebDriver driver; public static Logger logger = Logger.getLogger(BaseTester.class); /*-----------------------------------测试前后准备工作--------------------------------------------------------*/ @BeforeSuite public void beforeSuit() { driver = GetDriverUtil.getDriver();//测试开始之前,启动浏览器 } @AfterSuite public void afterSuit() { driver.quit();//测试结束之后,退出浏览器驱动,并关闭所有相关浏览器窗口 } /*----------------------------------数据提供者----------------------------------------------------*/ @DataProvider public Iterator<Object[]> testData(Method method) { TargetTestData annotation = method.getAnnotation(TargetTestData.class); String simpleName = this.getClass().getSimpleName(); String substring = simpleName.substring(0, simpleName.indexOf("_")); // System.out.println(substring); if (annotation == null) { annotation = (TargetTestData) Proxy.newProxyInstance(BaseTester.class.getClassLoader(), new Class[]{TargetTestData.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("sourceType".equals(method.getName())) return "excel";//默认去excel读取数据 if ("sourcePath".equals(method.getName())) return "/testData/" + substring + ".xlsx";//如果没有写读取地址,则默认到类名前缀的文件中去找 if ("sourceSheetOrSql".equals(method.getName())) return "Sheet1"; if ("mysqlPrefix".equals(method.getName())) return ".test"; return null; } }); } String sourceType = annotation.sourceType(); if ("excel".equals(sourceType)) { return new ExcelDataproviderUtil(annotation.sourcePath(), annotation.sourceSheetOrSql()); } if ("mysql".equals(sourceType)) { return new MysqlDataproviderUtil(annotation.sourceSheetOrSql(), annotation.mysqlPrefix()); } return null; } /*-----------------------------------------智能等待定位元素--------------------------------------------------------*/ /** * 智能定位元素 * * @param path 要读取的配置文件地址 * @param pageName 页面名称 * @param desc 元素描述 * @param timeOut 超时时间,单位为S * @return 返回定位到的元素 */ public WebElement intelligentWaitgetWebElement(String path, String pageName, String desc, int timeOut) { //System.out.println("path----->"+path+"pagename----->"+pageName+timeOut); ElementInfo locator = ElementLocatorUtil.getLocator(path, pageName, desc);//获取到定位器 //定位器内容 String byStr = locator.getBy(); String value = locator.getValue(); //通过反射,获取到By类的字节码文件(By类的引用) Class<By> byClass = By.class; Method declaredMethod = null;//根据方法名获取到方法By.id(),方法名就是id try { declaredMethod = byClass.getDeclaredMethod(byStr, String.class); By by = (By) declaredMethod.invoke(null, value);//调用null对象(即By类的静态方法)declaredMethod,参数为value WebDriverWait wait = new WebDriverWait(driver, timeOut);//创建等待对对象 WebElement until = wait.until(new ExpectedCondition<WebElement>() {//等待--直到内部类返回一个element public WebElement apply(WebDriver input) { WebElement element = input.findElement(by); return element; } }); return until; } catch (Exception e) { e.printStackTrace(); } return null; } /** * 当没有传入path,pageName时,通过注解去获取path和pageName, * 然后再去调用重载方法IntelligentWaitgetWebElement(String path, String pageName, String desc, int timeOut) * 智能等待时间默认为3秒 * * @param desc * @return */ public WebElement intelligentWaitgetWebElement(String desc) { GetPathAndPagename getPathAndPagename = new GetPathAndPagename().invoke(); String path = getPathAndPagename.getPath(); String pageName = getPathAndPagename.getPageName(); return intelligentWaitgetWebElement(path, pageName, desc, 3); } /** * 返回元素列表 * * @param path 要读取的配置文件地址 * @param pageName 页面名称 * @param desc 元素描述 * @param timeOut 超时时间,单位为S * @return 返回定位到的元素列表 */ public List<WebElement> intelligentWaitgetWebElements(String path, String pageName, String desc, int timeOut) { //System.out.println("path----->"+path+"pagename----->"+pageName+timeOut); ElementInfo locator = ElementLocatorUtil.getLocator(path, pageName, desc);//获取到定位器 //定位器内容 String byStr = locator.getBy(); String value = locator.getValue(); //通过反射,获取到By类的字节码文件(By类的引用) Class<By> byClass = By.class; Method declaredMethod = null;//根据方法名获取到方法By.id(),方法名就是id try { declaredMethod = byClass.getDeclaredMethod(byStr, String.class); By by = (By) declaredMethod.invoke(null, value);//调用null对象(即By类的静态方法)declaredMethod,参数为value WebDriverWait wait = new WebDriverWait(driver, timeOut);//创建等待对对象 List<WebElement> until = wait.until(new ExpectedCondition<List<WebElement>>() {//等待--直到内部类返回一个element public List<WebElement> apply(WebDriver input) { List<WebElement> elements = input.findElements(by); return elements; } }); return until; } catch (Exception e) { e.printStackTrace(); } return null; } /** * 返回元素列表 * 当没有传入path,pageName时,通过注解去获取path和pageName, * 然后再去调用重载方法IntelligentWaitgetWebElements(String path, String pageName, String desc, int timeOut) * 智能等待时间默认为3秒 * * @param desc * @return */ public List<WebElement> intelligentWaitgetWebElements(String desc) { GetPathAndPagename getPathAndPagename = new GetPathAndPagename().invoke(); String path = getPathAndPagename.getPath(); String pageName = getPathAndPagename.getPageName(); return intelligentWaitgetWebElements(path, pageName, desc, 3); } /** * 内部类,用于获取到当前运行方法上的TargetPage注解 */ private class GetPathAndPagename { private String path; private String pageName; public String getPath() { return path; } public String getPageName() { return pageName; } public GetPathAndPagename invoke() { ITestNGMethod method = Reporter.getCurrentTestResult().getMethod(); Method method1 = method.getMethod(); String name = method.getMethodName(); // System.out.println("当前运行方法为" +method1.getName()); //this.getClass().getDeclaredMethod(name,) TargetPage annotation = method1.getAnnotation(TargetPage.class); if (annotation == null) { annotation = (TargetPage) Proxy.newProxyInstance(BaseTester.class.getClassLoader(), new Class[]{TargetPage.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("path".equals(method.getName())) return "/frontPage/frontPage.xml"; if ("pageName".equals(method.getName())) return name; return null; } }); } path = annotation.path(); pageName = annotation.pageName(); return this; } } /*------------------------------------普通常用方法封装-------------------------------------------*/ /** * 打开浏览器页面 */ public void go() { GetPathAndPagename getPathAndPagename = new GetPathAndPagename().invoke(); String path = getPathAndPagename.getPath(); String pageName = getPathAndPagename.getPageName(); String url = ElementLocatorUtil.getUrl(path, pageName); driver.get(url); } /** * 打开浏览器指定页面 * * @param path 读取的配置文件地址 * @param pageName 页面名称 */ public void go(String path, String pageName) { String url = ElementLocatorUtil.getUrl(path, pageName); driver.get(url); } /** * 打开浏览器指定页面 * * @param pageName 页面名称 */ public void go(String pageName) { GetPathAndPagename getPathAndPagename = new GetPathAndPagename().invoke(); String path = getPathAndPagename.getPath(); String url = ElementLocatorUtil.getUrl(path, pageName); driver.get(url); } /** * 打开指定url * @param url */ public void goByUrl(String url){ driver.get(url); } /** * 向指定元素输入内容 * * @param desc 元素关键字 * @param inputKeys 输入的内容 */ public void inputKeys(String desc, String inputKeys) { intelligentWaitgetWebElement(desc).sendKeys(inputKeys); } /** * 元素点击 * * @param desc 元素关键字 */ public void click(String desc) { intelligentWaitgetWebElement(desc).click(); } /** * 点击指定元素 * @param element */ public void click(WebElement element){ element.click(); } public void intelligentClick(WebElement element,int timeOut){ WebDriverWait wait = new WebDriverWait(driver,timeOut); wait.until(ExpectedConditions.elementToBeClickable(element)).click(); } /** * 获取到元素文本 * * @param desc * @return */ public String getText(String desc) { return intelligentWaitgetWebElement(desc).getText(); } /** * 获取元素文本 * @param path * @param pagename * @param desc * @return */ public String getText(String path, String pagename, String desc) { return intelligentWaitgetWebElement(path, pagename, desc, 3).getText(); } /** * 获取到指定元素的指定属性值 * @param element * 元素 * @param attrName * 属性名 * @return * 属性值 */ public String getAttrVal(WebElement element,String attrName){ return element.getAttribute(attrName); } /** * 获得当前窗口title * @return */ public String getTitle(){ return driver.getTitle(); } /** * 获得当前窗口url * @return */ public String getCurrentUrl(){ return driver.getCurrentUrl(); } /** * 返回之前页面 */ public void back(){ driver.navigate().back(); } /** * 获得当前所有窗口句柄 * @return */ public Set<String> getWindowHandles(){ return driver.getWindowHandles(); } /** * 获得当前窗口句柄 * @return */ public String getWindowHandle(){ return driver.getWindowHandle(); } /** * 进入指定句柄的窗口 * @param windowhandle */ public void goInNewWindow(String windowhandle){ driver.switchTo().window(windowhandle); } public void selectBytext(String desc,String text){ if (text != ""){ WebElement webElement = intelligentWaitgetWebElement(desc); Select select =new Select(webElement); select.selectByVisibleText(text); } } /*----------------------------------------断言--------------------------------------------------*/ /** * 断言预期字符串与实际字符串相等 * * @param expected * @param actural */ public void assertTextEqual(String expected, String actural) { Assert.assertEquals(expected, actural); } /** * 断言实际文本包含预期文本 * * @param expected * @param actural */ public void assertTextContain(String expected, String actural) { try { Assert.assertTrue(actural.contains(expected)); } catch (Exception e) { e.printStackTrace(); logger.error("断言失败!"); } } /** * 断言非空 * * @param actural */ public void assertNotNull(String actural) { Assert.assertNotNull(actural); } public void assertTrue(Boolean flag){ Assert.assertTrue(flag); } }
4.用于封装页面信息的2个类
package com.claire.jing.elementLocator; /** * 该类模拟页面元素定位器,任何元素通过By.by(value)的方式都可以获得,如By.id() */ public class ElementInfo { private String by; private String value; private String desc; public ElementInfo() { } public ElementInfo(String by, String value, String desc) { this.by = by; this.value = value; this.desc = desc; } public String getBy() { return by; } public void setBy(String by) { this.by = by; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } @Override public String toString() { return "ElementInfo{" + "by=‘" + by + ‘‘‘ + ", value=‘" + value + ‘‘‘ + ", desc=‘" + desc + ‘‘‘ + ‘}‘; } }
package com.claire.jing.elementLocator; import java.util.Arrays; import java.util.Map; public class PageInfo { private String pageName; private String pageUrl; private Map<String,ElementInfo> elementInfoMap; public PageInfo() { } public PageInfo(String pageName, String pageUrl, Map<String, ElementInfo> elementInfoMap) { this.pageName = pageName; this.pageUrl = pageUrl; this.elementInfoMap = elementInfoMap; } public String getPageName() { return pageName; } public void setPageName(String pageName) { this.pageName = pageName; } public String getPageUrl() { return pageUrl; } public void setPageUrl(String pageUrl) { this.pageUrl = pageUrl; } public Map<String, ElementInfo> getElementInfoMap() { return elementInfoMap; } public void setElementInfoMap(Map<String, ElementInfo> elementInfoMap) { this.elementInfoMap = elementInfoMap; } @Override public String toString() { return "PageInfo{" + "pageName=‘" + pageName + ‘‘‘ + ", pageUrl=‘" + pageUrl + ‘‘‘ + ", elementInfoMap=" + elementInfoMap + ‘}‘; } }
5.页面信息配置文件以及文件解析的util(实现关键字驱动---将页面元素定位信息以及页面Url写到配置文件中,并读取映射到指定pojo上。对位提供获取url和获取定位器的方法)
<?xml version="1.0" encoding="UTF-8"?> <!--登录页--> <pages> <page pageName="Login" url="http://118.184.217.39/Organizer/Login"> <locator by="id" value="UserInfo" desc="输入用户名"/> <locator by="id" value="Password" desc="输入密码"/> <locator by="id" value="button" desc="登录按钮"/> <locator by="cssSelector" value="span[data-valmsg-for=‘UserInfo‘]" desc="用户名错误提示"/> <locator by="cssSelector" value="span[data-valmsg-for=‘Password‘]" desc="密码错误提示"/> </page> <!--首页--> <page pageName="HomePage" url="http://118.184.217.39/"> <locator by="cssSelector" value="a[href=‘/Organizer/Index‘]" desc="首页欢迎您文本"/> <locator by="xpath" value="//div[@id=‘banner_list‘]/a" desc="首页banners"/> <locator by="xpath" value="//td[@height=‘28‘]/a" desc="首页所有竞赛链接"/> </page> <!--竞赛搜索页--> <page pageName="CompetitionSearch" url="http://118.184.217.39/Competition/Index"> <locator by="id" value="ProvinceCode" desc="选择省"/> <locator by="id" value="CityCode" desc="选择市"/> <locator by="id" value="AreaCode" desc="选择区"/> <locator by="id" value="search_text" desc="竞赛搜索输入框"/> <locator by="id" value="search_btn" desc="竞赛搜索按钮"/> <locator by="id" value="loadMoreBtn" desc="点击加载更多"/> <locator by="cssSelector" value="div[class=‘fl search_comp_list‘]" desc="页面所有竞赛"/> <locator by="id" value="search_btn" desc="竞赛搜索按钮"/> </page> <!--报名页--> <page pageName="ApplyCompetition" url="http://118.184.217.39/Project/Index/"> <locator by="cssSelector" value="[href=‘/Organizer/Login‘]" desc="登录按钮"/> <locator by="xpath" value="//*[@id=‘EnrollType‘]/label/input" desc="角色类型"/> <locator by="cssSelector" value="input[name=‘GroupType‘]" desc="选择组别"/> <locator by="id" value="teamName" desc="团队全称"/> <locator by="id" value="leaderName" desc="领队姓名"/> <locator by="id" value="leaderPhone" desc="领队电话"/> <locator by="id" value="coachName" desc="教练姓名"/> <locator by="id" value="coachPhone" desc="教练电话"/> <locator by="id" value="coachName2" desc="教练姓名2"/> <locator by="id" value="coachPhone2" desc="教练电话2"/> <locator by="id" value="coachName3" desc="教练姓名3"/> <locator by="id" value="coachPhone3" desc="教练电话3"/> <locator by="id" value="coachName4" desc="教练姓名4"/> <locator by="id" value="coachPhone4" desc="教练电话4"/> <locator by="id" value="parentName" desc="家长姓名"/> <locator by="id" value="parentPhone" desc="家长电话"/> <locator by="id" value="email" desc="电子邮箱"/> <locator by="id" value="companyName" desc="单位名称"/> <locator by="id" value="taxPayerNumber" desc="税号"/> <locator by="id" value="row_name" desc="参赛人姓名"/> <locator by="id" value="row_sex" desc="参赛人性别"/> <locator by="id" value="row_idType" desc="参赛人证件类型"/> <locator by="id" value="row_idno" desc="参赛人证件号码"/> <locator by="id" value="row_birthday" desc="参赛人生日"/> <locator by="id" value="row_add" desc="添加参赛人按钮"/> <locator by="id" value="submit" desc="确定报名按钮"/> <locator by="id" value="search_btn" desc="竞赛搜索按钮"/> </page> </pages>
package com.claire.jing.utils; import com.claire.jing.elementLocator.ElementInfo; import com.claire.jing.elementLocator.PageInfo; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import java.util.HashMap; import java.util.List; import java.util.Map; public class ElementLocatorUtil { /** * example * @param args */ public static void main(String[] args) { String pageUrl = ElementLocatorUtil.getUrl("/frontPage/frontPage.xml", "Login"); System.out.println(pageUrl); ElementInfo locator = ElementLocatorUtil.getLocator("/frontPage/frontPage.xml", "Login", "输入用户名"); System.out.println(locator); } private static String path; private static Map<String, Map<String, PageInfo>> sureInitMap = new HashMap<String, Map<String, PageInfo>>(); /** * 构造函数,为了能将path传递进来 * @param path */ public ElementLocatorUtil(String path) { this.path = path; } /** * 确保同一份文件只被解析一遍 */ private static void sureInit() { if (sureInitMap.get(path) == null) { readXml(); } } /** * 解析xml文件 * 思想是:将xml解析到JavaBean中 * Map<String, PageInfo> -------根据pageName 获取到对应的 pageInfo对象 * Map<String, ElementInfo>------根据元素描述 获取到对应的 ElementInfo对象(即:获取到页面所有元素的定位信息) */ private static void readXml() { SAXReader reader = new SAXReader(); Document document = null; try { document = reader.read(ElementLocatorUtil.class.getResourceAsStream(path)); } catch (DocumentException e) { e.printStackTrace(); } Element rootElement = document.getRootElement();//获取到根节点pages List<Element> allPages = rootElement.elements("page");//获取到所有的page子节点 Map<String, PageInfo> pageInfoMap = new HashMap<String, PageInfo>();//为了可以通过pageName获取到页面所有信息,创建该map PageInfo pageInfo = null;//创建一个PageInfo对象 for (Element page : allPages) { String pageName = page.attributeValue("pageName");//获取到pageName String url = page.attributeValue("url");//获取到url List<Element> allLocator = page.elements("locator");//获取到page节点下的所有locator子节点 Map<String, ElementInfo> elementInfoMap = new HashMap<String, ElementInfo>();//为了可以通过元素的描述--获取到对应元素,创建该map //List<ElementInfo> elementInfo = null;//创建一个ElementInfo对象 for (Element locator : allLocator) { String by = locator.attributeValue("by");//获取到locator的by属性值 String value = locator.attributeValue("value");//获取到locator的value属性值 String desc = locator.attributeValue("desc");//获取到locator的desc属性值 elementInfoMap.put(desc, new ElementInfo(by, value, desc));//通过元素描述,可以直接获得ElementInfo } pageInfo = new PageInfo(pageName, url, elementInfoMap); pageInfoMap.put(pageName, pageInfo); } sureInitMap.put(path, pageInfoMap); } /** * 获取到测试页面的url * @param path * 要读取的配置文件路径(如:在resources下的路径为/frontPage(配置文件上一层包名)/frontPage.xml(配置文件名) * @param pageName * 页面名称 * @return * 返回页面url */ public static String getUrl(String path, String pageName) { new ElementLocatorUtil(path); sureInit(); Map<String, PageInfo> map = sureInitMap.get(path); return map.get(pageName).getPageUrl(); } /** * 获取到测试页面指定元素的定位器(定位器即JavaBean,元素所有定位信息封装在JavaBean中) * @param path * 要读取的配置文件路径(如:在resources下的路径为/frontPage(配置文件上一层包名)/frontPage.xml(配置文件名) * @param pageName * 页面名称 * @param desc * 元素描述 * @return * 返回定位器 */ public static ElementInfo getLocator(String path, String pageName, String desc) { new ElementLocatorUtil(path); sureInit(); Map<String, PageInfo> map = sureInitMap.get(path); PageInfo pageInfo = map.get(pageName); ElementInfo elementInfo = pageInfo.getElementInfoMap().get(desc); return elementInfo; } }
6.数据提供者(关于数据驱动--将测试数据与代码完全分离)
读取Excel中的测试数据:
package com.claire.jing.utils; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.ss.usermodel.*; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class ExcelDataproviderUtil implements Iterator<Object[]> { public static void main(String[] args) { ExcelDataproviderUtil excelDataproviderUtil = new ExcelDataproviderUtil("/testData/login.xlsx", "Sheet1"); while (excelDataproviderUtil.hasNext()){ Object[] next = excelDataproviderUtil.next(); System.out.println(Arrays.deepToString(next)); } } private String path; private String sheetName; String[] clomnName; Iterator<Row> rowIterator; Map<String, Workbook> workbookMap = new HashMap<>(); public ExcelDataproviderUtil(String path, String sheetName) { this.path = path; this.sheetName = sheetName; } private void sureInit() { if (workbookMap.get(path) == null) initReadExcel(); } public void initReadExcel() { InputStream inp = ExcelDataproviderUtil.class.getResourceAsStream(path); Workbook workbook = null; try { workbook = WorkbookFactory.create(inp); } catch (IOException e) { e.printStackTrace(); } catch (InvalidFormatException e) { e.printStackTrace(); } workbookMap.put(path, workbook); Sheet sheet = workbook.getSheet(this.sheetName); Row firstRow = sheet.getRow(0); short lastCellNum = firstRow.getLastCellNum(); clomnName = new String[lastCellNum]; //初始化列名数组 for (int i = 0; i < clomnName.length; i++) { clomnName[i] = firstRow.getCell(i).getStringCellValue(); } rowIterator = sheet.iterator(); rowIterator.next();//直接将行迭代器移动到数据行上面 } boolean flag = false;//默认假设没有下一个 @Override public boolean hasNext() { sureInit(); if (flag) return true; //判断,当有下一个的时候,将flag设置为true if (rowIterator.hasNext()) { flag = true; return true; } else { return false; } } @Override public Object[] next() { sureInit(); flag = false; //读取当前行数据 Row currentRow = rowIterator.next(); Map<String,String> currentRowData = new HashMap<>(); for (int i = 0; i < clomnName.length; i++) { Cell cell = currentRow.getCell(i, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK); cell.setCellType(CellType.STRING); currentRowData.put(clomnName[i],cell.getStringCellValue()); } return new Object[]{currentRowData}; } @Override public void remove() { throw new UnsupportedOperationException("不支持删除操作!"); } }
jdbc帮助类:
package com.claire.jing.utils.jdbc; import com.claire.jing.utils.PropertiesUtil; import java.sql.*; public class JdbcUtil { private static final String JDBC_DRIVER = "jdbc.driver"; private static final String JDBC_URL="jdbc.url"; private static final String JDBC_USERNAME="jdbc.username"; private static final String JDBC_PWD="jdbc.password"; private static final String PATH = "/mysql.properties"; //保证driver只加载一次 static { try { Class.forName(PropertiesUtil.getProVal(PATH,JDBC_DRIVER)); } catch (ClassNotFoundException e) { e.printStackTrace(); } } /** * 获取数据库链接 * @return */ public static Connection getConn(String prefix){ String url = PropertiesUtil.getProVal(PATH, prefix+JDBC_URL); String username = PropertiesUtil.getProVal(PATH, prefix+JDBC_USERNAME); String pwd = PropertiesUtil.getProVal(PATH, prefix+JDBC_PWD); try { Connection connection = DriverManager.getConnection(url, username, pwd); return connection; } catch (SQLException e) { e.printStackTrace(); } return null; } public static void close(Connection conn, ResultSet resultSet, PreparedStatement statement){ if (conn !=null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } if (resultSet!=null) try { resultSet.close(); } catch (SQLException e) { e.printStackTrace(); } if (statement!=null) try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } }
数据库读取测试数据:
package com.claire.jing.utils; import com.claire.jing.utils.jdbc.JdbcUtil; import java.sql.*; import java.util.*; public class MysqlDataproviderUtil implements Iterator<Object[]> { public static void main(String[] args) { MysqlDataproviderUtil mysqlDataproviderUtil = new MysqlDataproviderUtil("SELECT * FROM `Organizers`", "test."); for (int i = 0; i < 3; i++) { if (mysqlDataproviderUtil.hasNext()){ Object[] next = mysqlDataproviderUtil.next(); System.out.println(Arrays.toString(next));} } } private String sql; private String prefix; Connection conn; PreparedStatement statement; private ResultSet resultSet; String[] columnName; Map<String, ResultSet> sureInitMap = new HashMap<>(); /** * 构造函数 * @param sql * @param prefix */ public MysqlDataproviderUtil(String sql,String prefix) { this.sql = sql; this.prefix = prefix; } /** * 确保一个sql不去执行多遍 */ private void sureInit() { if (sureInitMap.get(sql) == null) doSelect(); } /** * 执行查询 */ private void doSelect() { //System.out.println("我的执行次数"); conn = JdbcUtil.getConn(prefix); try { statement = conn.prepareStatement(sql); resultSet = statement.executeQuery(); sureInitMap.put(sql, resultSet); } catch (SQLException e) { e.printStackTrace(); } ResultSetMetaData metaData = null; int columnCount = 0; try { metaData = resultSet.getMetaData(); columnCount = metaData.getColumnCount(); } catch (SQLException e) { e.printStackTrace(); } columnName = new String[columnCount]; for (int i = 0; i < columnName.length; i++) { try { columnName[i] = metaData.getColumnName(i + 1); } catch (SQLException e) { e.printStackTrace(); } } // System.out.println("执行结束,获得了resultSet"); } Boolean flag = false;//消除next带来的副作用 @Override public boolean hasNext() { sureInit(); if (flag) { return true; } boolean temp = false; try { temp = resultSet.next(); } catch (SQLException e) { e.printStackTrace(); } if (temp) { flag = true; return true; } else { close();//当没有下一行数据时,自动关闭数据库各种连接。如果在next=true时,就不再取数据了,建议手动调用close方法,去关闭数据库连接。 return false; } } @Override public Object[] next() { sureInit(); flag = false; Map<String, String> testData = new HashMap<>(); for (int i = 0; i < columnName.length; i++) {//遍历,并获取到当前行的数据 try { String setString = resultSet.getString(i + 1); testData.put(columnName[i], setString); } catch (SQLException e) { e.printStackTrace(); } } return new Object[]{testData}; } @Override public void remove() { throw new UnsupportedOperationException("不支持删除操作"); } /** * 对外提供一个关闭数据库的方法,可以手动关闭。 */ public void close(){ JdbcUtil.close(conn,resultSet,statement); } }
7.为了实现失败截图的监听器,在上一篇文章中已经介绍,这里不再赘述。
package com.claire.jing.MyListener; import com.claire.jing.base.BaseTester; import com.claire.jing.utils.FailTestScreenShotUtil; import io.qameta.allure.Attachment; import org.openqa.selenium.OutputType; import org.openqa.selenium.TakesScreenshot; import org.testng.ITestResult; import org.testng.TestListenerAdapter; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; public class TestFailListener extends TestListenerAdapter { @Override public void onTestFailure(ITestResult result) { takePhoto(); } @Attachment(value = "screen shot",type = "image/png") public byte[] takePhoto(){ byte[] screenshotAs = ((TakesScreenshot)BaseTester.driver).getScreenshotAs(OutputType.BYTES); return screenshotAs; } }
8.自定义注解,方便在测试用例执行时,指定测试数据以及要测试页面
package com.claire.jing.annocations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /**
*要测试页面的配置文件路径以及页面名称
*/ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface TargetPage { public String path() default ""; public String pageName() default "";
}
package com.claire.jing.annocations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;
/**
*sourceType指定测试数据源的类型,当前支持Excel和mysql数据库
*sourcePath指定测试数据的配置文件路径
*sourceSheetOrSql 当测试数据从Excel读取时,指定sheet名。如果为mysql读取时,指定sql语句
*mysqlPrefix 当测试数据从mysql读取时,指定数据库前缀
*/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface TargetTestData { public String sourceType() default "excel"; public String sourcePath() default ""; public String sourceSheetOrSql() default ""; public String mysqlPrefix() default "test."; }
9.测试脚本(只粘贴一个竞赛搜索页面的测试)
package com.claire.jing.testCases.competitionsList; import com.claire.jing.MyListener.TestFailListener; import com.claire.jing.annocations.TargetPage; import com.claire.jing.annocations.TargetTestData; import com.claire.jing.base.BaseTester; import io.qameta.allure.Step; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.Select; import org.testng.annotations.Listeners; import org.testng.annotations.Test; import java.util.List; import java.util.Map; @Listeners(TestFailListener.class) public class CompetitionsList_testCases extends BaseTester { private static final String PAGE_PATH = "/frontPage/frontPage.xml"; private static final String PAGE_NAME = "CompetitionSearch"; private static final String SOURCE_PATH = "/testData/competitionSearch.xlsx"; private static final String SOURCE_SHEET="competitionSearch"; @Test(dataProvider = "testData",description = "竞赛搜索测试") @Step("输入搜索关键字:{0},对应搜索条数预期结果为:{1}") @TargetTestData(sourcePath = SOURCE_PATH,sourceSheetOrSql = SOURCE_SHEET) @TargetPage(path = PAGE_PATH,pageName = PAGE_NAME) public void competitionSearchTest(Map<String,String> testData) throws Exception{ go(PAGE_NAME);//打开竞赛搜索页面 Thread.sleep(2000); selectBytext("选择省",testData.get("选择省"));//选择省 selectBytext("选择市",testData.get("选择市"));//选择市 selectBytext("选择区",testData.get("选择区"));//选择区 inputKeys("竞赛搜索输入框",testData.get("搜索关键字"));//输入搜索关键字 click("竞赛搜索按钮"); int expectCounts =Integer.valueOf(testData.get("预期搜索到的竞赛个数")); logger.info("预期竞赛个数为"+expectCounts); List<WebElement> competitionsList = intelligentWaitgetWebElements("页面所有竞赛"); int index =10; while (competitionsList.size() - index ==0){ click(intelligentWaitgetWebElement("点击加载更多"));//点击加载更多 Thread.sleep(1000); competitionsList = intelligentWaitgetWebElements("页面所有竞赛"); index += 10; } logger.info("实际搜索到的竞赛有"+competitionsList.size()+"个"); assertTrue(expectCounts==competitionsList.size()); } }
下面是对应的测试数据:
后续Jenkins的配置已经在之前博客中整理过,有兴趣可以学习下哦。
以上是关于自动化测试--实现一套完全解耦的测试框架的主要内容,如果未能解决你的问题,请参考以下文章
Airtest+python+selenium 一套轻量级web自动化测试框架