├── .gitignore ├── README.md ├── autotest-app ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── qianmi │ └── autotest │ └── app │ ├── AppTestApplication.java │ ├── common │ ├── AppAutotestProperties.java │ ├── AppPageTest.java │ └── AppResourceLoader.java │ ├── page │ ├── AppBasePage.java │ └── AppPageFacade.java │ └── testng │ └── AppTestRetryAnalyzer.java ├── autotest-appium ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── qianmi │ └── autotest │ └── appium │ ├── common │ ├── AppiumAutotestProperties.java │ ├── AppiumResourceLoader.java │ └── AppiumWait.java │ ├── data │ ├── DataInitialization.java │ └── DataProvider.java │ ├── page │ └── AppiumBasePage.java │ └── testng │ └── ScreenShotListener.java ├── autotest-base ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── qianmi │ └── autotest │ └── base │ ├── AbstractTestApplication.java │ ├── common │ ├── AutotestException.java │ ├── AutotestProperties.java │ ├── AutotestUtils.java │ ├── BasePageTest.java │ ├── BeanFactory.java │ ├── Logoutable.java │ ├── Module.java │ └── Scene.java │ ├── page │ ├── AppLoginPage.java │ └── PageObject.java │ └── testng │ ├── BaseTestRetryAnalyzer.java │ ├── DefaultReporter.java │ ├── QmDingNotifier.java │ └── TestRetryListener.java ├── autotest-demo ├── autotest-demo-app │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── com │ │ │ └── qianmi │ │ │ └── autotest │ │ │ └── demo │ │ │ └── app │ │ │ └── jd │ │ │ ├── page │ │ │ ├── HomePage.java │ │ │ ├── NavigatePage.java │ │ │ ├── ProductPage.java │ │ │ ├── SearchPage.java │ │ │ ├── SearchResultPage.java │ │ │ └── ShoppingCartPage.java │ │ │ └── test │ │ │ └── JdAppTest.java │ │ └── resources │ │ ├── application.properties │ │ └── data.properties ├── autotest-demo-html5 │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── com │ │ │ └── qianmi │ │ │ └── autotest │ │ │ └── demo │ │ │ └── html5 │ │ │ └── jd │ │ │ ├── page │ │ │ ├── HomePage.java │ │ │ ├── NavigatePage.java │ │ │ ├── ProductPage.java │ │ │ ├── SearchPage.java │ │ │ ├── SearchResultPage.java │ │ │ └── ShoppingCartPage.java │ │ │ └── test │ │ │ └── JdHtml5Test.java │ │ └── resources │ │ ├── application.properties │ │ └── data.properties ├── autotest-demo-web │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── com │ │ │ └── qianmi │ │ │ └── autotest │ │ │ └── demo │ │ │ └── web │ │ │ └── baidu │ │ │ ├── page │ │ │ ├── HomePage.java │ │ │ └── SearchResultPage.java │ │ │ └── test │ │ │ └── BaiduWebTest.java │ │ └── resources │ │ ├── application.properties │ │ └── data.properties └── pom.xml ├── autotest-html5 ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── qianmi │ └── autotest │ └── html5 │ ├── Html5TestApplication.java │ ├── common │ ├── Html5PageTest.java │ └── Html5ResourceLoader.java │ ├── page │ ├── Html5Page.java │ └── Html5PageFacade.java │ └── testng │ └── Html5TestRetryAnalyzer.java ├── autotest-web ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── qianmi │ └── autotest │ └── web │ ├── WebTestApplication.java │ ├── common │ ├── WebAutotestProperties.java │ ├── WebPageTest.java │ └── WebResourceLoader.java │ ├── data │ ├── DataInitialization.java │ └── DataProvider.java │ ├── page │ ├── WebBasePage.java │ └── WebPageFacade.java │ └── testng │ ├── ScreenShotListener.java │ └── WebTestRetryAnalyzer.java └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Maven template 3 | target/ 4 | pom.xml.tag 5 | pom.xml.releaseBackup 6 | pom.xml.versionsBackup 7 | pom.xml.next 8 | release.properties 9 | dependency-reduced-pom.xml 10 | buildNumber.properties 11 | .mvn/timing.properties 12 | ### Java template 13 | *.class 14 | 15 | # Mobile Tools for Java (J2ME) 16 | .mtj.tmp/ 17 | 18 | # Package Files # 19 | *.jar 20 | *.war 21 | *.ear 22 | 23 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 24 | hs_err_pid* 25 | .idea/ 26 | *.iml 27 | 28 | ### JetBrains template 29 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 30 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 31 | 32 | # User-specific stuff: 33 | .idea/workspace.xml 34 | .idea/tasks.xml 35 | .idea/dictionaries 36 | .idea/vcs.xml 37 | .idea/jsLibraryMappings.xml 38 | 39 | # Sensitive or high-churn files: 40 | .idea/dataSources.ids 41 | .idea/dataSources.xml 42 | .idea/dataSources.local.xml 43 | .idea/sqlDataSources.xml 44 | .idea/dynamic.xml 45 | .idea/uiDesigner.xml 46 | 47 | # Gradle: 48 | .idea/gradle.xml 49 | .idea/libraries 50 | 51 | # Mongo Explorer plugin: 52 | .idea/mongoSettings.xml 53 | 54 | ## File-based project format: 55 | *.iws 56 | 57 | ## Plugin-specific files: 58 | 59 | # IntelliJ 60 | /out/ 61 | 62 | # mpeltonen/sbt-idea plugin 63 | .idea_modules/ 64 | 65 | # JIRA plugin 66 | atlassian-ide-plugin.xml 67 | 68 | # Crashlytics plugin (for Android Studio and IntelliJ) 69 | com_crashlytics_export_strings.xml 70 | crashlytics.properties 71 | crashlytics-build.properties 72 | fabric.properties 73 | ### Maven template 74 | target/ 75 | pom.xml.tag 76 | pom.xml.releaseBackup 77 | pom.xml.versionsBackup 78 | pom.xml.next 79 | release.properties 80 | dependency-reduced-pom.xml 81 | buildNumber.properties 82 | .mvn/timing.properties 83 | ### Java template 84 | *.class 85 | 86 | # Mobile Tools for Java (J2ME) 87 | .mtj.tmp/ 88 | 89 | # Package Files # 90 | *.jar 91 | *.war 92 | *.ear 93 | 94 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 95 | hs_err_pid* 96 | 97 | 98 | autotest-framework/ 99 | autotest-d2p/ 100 | autotest-d2p-app/ 101 | test-output/ 102 | logs/ 103 | screenshot/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 前端自动化测试框架(UI Autotest Framework) 2 | 3 | 框架提供统一的接口、设计原语和开发模式,支持 APP、微信、HTML5、Web 网页。自动化测试用例开发人员只需要学习一次,就可以编写前端自动化测试用例,对前端产品进行自动化测试。 4 | 5 | ## 前端自动化测试框架包含如下模块: APP 自动化测试框架、HTML5 网页自动化测试框架、Web网页自动化测试框架。 6 | 7 | * APP 自动化测试框架主要用于移动端APP自动化测试项目,目标程序运行在移动设备上。 8 | 9 | * HTML5 网页自动化测试框架主要用于移动端H5网页(比如微信程序),目标网页通过移动设备上的 Chrome 或者 Safari 浏览器运行。 10 | 11 | * Web 网页自动化测试主要用于 PC 端网页,目前支持 Chrome 、Safari 、Firefox 、IE 、Edge 浏览器。 12 | 13 | ## 特性 14 | 15 | Autotest Framework 有如下特性: 16 | 17 | * 采用Java语言,基于 SpringBoot 框架。 18 | 19 | * 基于 Page Object 设计模式,将 UI 界面抽象为 Page Object,可以减少重复代码和降低维护成本。 20 | 21 | * 基于 TesgNG 测试框架构建测试用例,支持钉钉消息通知、失败截屏、HTTP 报告、并发执行等特性。 22 | 23 | * 统一管理和维护 Adb 连接、Appium server,对上层测试程序屏蔽实现细节,降低测试人员编写用例难度。 24 | 25 | * 封装和抽象配置和数据仓库,直接注入到测试用例中,无需额外获取。 26 | 27 | ## 架构 28 | 29 | ### APP 测试框架的逻辑视图 30 | 31 | ![APP 测试框架逻辑视图](https://s2.ax1x.com/2019/09/11/nwC234.jpg) 32 | 33 | 测试程序主要分为三层: 34 | 35 | * APP 自动化测试程序层,包含 Page Object 对象和测试用例 36 | 37 | * APP Framework 层,主要提供统一的系统封装 38 | 39 | * Appium Server Manger 层,提供 Adb 连接、Appium Server、Apk 的管理和维护 40 | 41 | ### APP 测试框架模块视图 42 | 43 | ![APP 测试框架模块视图](https://s2.ax1x.com/2019/09/11/nwCWv9.jpg) 44 | 45 | ## 开发指南 46 | 47 | ### 1. 创建测试项目 48 | 49 | 以 APP 自动化测试为例:只需要创建一个自动化测试项目,并且依赖 APP 自动化测试框架 autotest-app 即可。 50 | 51 | ```xml 52 | 53 | 54 | com.qianmi 55 | autotest-app 56 | 2.0.0-SNAPSHOT 57 | 58 | 59 | ``` 60 | 61 | 再配置一个 SpringBoot 的 Maven 打包插件,mainClass 属性配置为对应框架的启动类。 62 | 63 | * APP 的启动类为:**`com.qianmi.autotest.app.AppTestApplication`** 64 | * HTML5 的启动类为:**`com.qianmi.autotest.html5.Html5TestApplication`** 65 | * Web 的启动类为:**`com.qianmi.autotest.web.WebTestApplication`** 66 | 67 | ```xml 68 | 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-maven-plugin 73 | 74 | 75 | 76 | repackage 77 | 78 | 79 | 80 | 81 | com.qianmi.autotest.app.AppTestApplication 82 | 83 | 84 | 85 | 86 | ``` 87 | 88 | ### 2.配置文件 89 | 90 | #### 项目配置文件 91 | 92 | 项目配置文件位于:`src/main/resources/application.properties` 93 | 94 | #### 测试数据 95 | 96 | 测试数据文件位于:`src/main/resources/data.properties`。输入数据以 input 开头,输出数据以 output 开头,支持根据手机指定特定的输入输出数据。 97 | 98 | ```properties 99 | input.userName=p031801 100 | input.password=000000 101 | input.productName1=TestingGoods 102 | input.productPrice1=1 103 | input.productName2=可亲 香浓燕麦片 袋装 400g 104 | input.productPrice2=11 105 | input.productName3=漫步者(EDIFIER) H185 手机耳机input.productPrice3=5 106 | input.productName4=卡乐比 水果果仁谷物麦片 袋装 800g 107 | input.productPrice4=2 108 | input.cateName = 食品、饮料 109 | 110 | device.htcM8.input.userName=p031801 111 | device.htcM8.input.password=000000 112 | device.huawei8.input.userName=p031802 113 | device.huawei8.input.password=000000 114 | device.meizu.input.userName=p031803 115 | device.meizu.input.password=000000 116 | device.mi4.input.userName=p031804 117 | device.mi4.input.password=000000 118 | device.samsung.input.userName=p031805 119 | device.samsung.input.password=000000 120 | device.hwp7.input.userName=p031806 121 | device.hwp7.input.password=000000 122 | ``` 123 | 124 | 特定机型的参数优先级高于全局配置。比如有两个配置项: 125 | 126 | ```properties 127 | input.userName=p031801 128 | device.huawei8.input.userName=p031802 129 | ``` 130 | 131 | 针对 huawei8 手机,其 userName 配置为 p031802 ,如果不指定第二个配置项,那么其 userName 配置为 p031801 。 132 | 133 | ### 3. 代码开发 134 | 135 | 项目代码包含两个包:page 和 test 。 136 | 137 | #### 3.1 Page Object 编写 138 | 139 | Page Object 对象是指 UI 界面上用于与用户进行交互的对象,一般指某个页面。对于一个 Page Object 对象,它有两方面的特征: 140 | 141 | * 元素 ( WebElement ) 142 | 143 | * 功能 ( Service ) 144 | 145 | 元素就是界面上的标签、输入框、按钮等控件。而 Page Object 通常也都是实现一定的功能的,这些功能就是 Service 。比如登录页面有用户名输入框、密码输入框、登录按钮,点击登录按钮要么登录成功跳转到首页,要么提示登录失败。用户名输入框、密码输入框、登录按钮就是登录页面元素,登录操作就是登录页面对外提供的 Service 。 146 | 147 | 我们对 Page Object 统一定义如下: 148 | 149 | 1. Page Object 类的命名格式为:XxxPage,类上请加上 `@Component`,标明这个类由 Spring 容器托管。类注释请标明该类是什么页面,主要功能描述,方便后续人员维护。 150 | 2. Page 对象类必须继承对应框架中的 BasePage 类,元素使用注解进行定义。 151 | 3. public 方法对应页面提供的功能,方法必须返回 Page 对象,要么是自身,要么是另一个 Page 对象。如果操作失败,请抛出 AutoTestException 异常。 152 | 4. 页面跳转请调用父类的 gotoPage 方法。 153 | 154 | #### 3.2 测试用例编写 155 | 156 | 测试用例类就是调用 Page Object 类完成某个待测功能。 157 | 158 | 1. 测试用例类的命名为 XxxTest 159 | 2. 测试用例类必须继承对应框架的 PageTest 类 160 | 3. 测试用户中的每个方法对应一个一个测试功能,方法上面加上 `@Test` 注解,注解的 priority 属性对于测试功能的执行优先级,priority 越小越先执行,priority 相同按照定义的先后执行 161 | 162 | ## 执行 163 | 164 | 编码完成后,使用 Maven 打包,执行 `java -jar` 命令执行即可,文件会输出测试报告,失败的话会截屏,并且支持消息通知、重试等功能。 165 | 166 | 如果不使用 Appium Server Mng 而在本地执行的话,需要安装 Appium , 安装教程见:[http://appium.io/docs/en/about-appium/getting-started](http://appium.io/docs/en/about-appium/getting-started)。 167 | 168 | ### 执行 Demo 169 | 170 | [autotest-demo](/autotest-demo) 目录下是三个模块 ,分别是 APP 、 HTML5 、 Web 自动化测试示例。 171 | 172 | 以运行autotest-demo-web为例: 173 | 174 | ```shell 175 | $ cd {project_home} 176 | $ git clone git@github.com:jingpeicomp/autotest-framework.git 177 | $ cd autotest-framework 178 | $ mvn clean package -Dmaven.test.skip=true 179 | $ cd autotest-demo/autotest-demo-web/target 180 | $ chmod a+x autotest-demo-web-2.0.0-SNAPSHOT.jar 181 | $ java -jar autotest-demo-web-2.0.0-SNAPSHOT.jar 182 | ``` 183 | 184 | 执行完成后,报告位于 `test-output/custom-test-report.html` (可以自定义路径),报告的格式如下: 185 | 186 | ![Web 执行报告](https://s2.ax1x.com/2019/09/11/nwC4D1.jpg) 187 | 188 | --- 189 | 190 | * [APP demo](/autotest-demo/autotest-demo-app) 执行过程录屏如下: 191 | 192 | 193 | ![APP demo 执行过程](https://s2.ax1x.com/2019/09/11/nwC5Hx.gif) 194 | 195 | --- 196 | 197 | * [HTML5 demo](/autotest-demo/autotest-demo-html5) 执行过程录屏如下: 198 | 199 | ![HTML5 demo 执行过程](https://s2.ax1x.com/2019/09/11/nwC74O.gif) 200 | -------------------------------------------------------------------------------- /autotest-app/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | autotest-parent 6 | com.qianmi 7 | 2.0.0-SNAPSHOT 8 | ../pom.xml 9 | 10 | 4.0.0 11 | 12 | autotest-app 13 | ${project.artifactId} 14 | 移动端APP自动化测试框架 15 | 16 | 17 | 18 | com.qianmi 19 | autotest-appium 20 | ${project.version} 21 | 22 | 23 | org.projectlombok 24 | lombok 25 | 26 | 27 | -------------------------------------------------------------------------------- /autotest-app/src/main/java/com/qianmi/autotest/app/AppTestApplication.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.app; 2 | 3 | import com.qianmi.autotest.app.testng.AppTestRetryAnalyzer; 4 | import com.qianmi.autotest.appium.testng.ScreenShotListener; 5 | import com.qianmi.autotest.base.AbstractTestApplication; 6 | import com.qianmi.autotest.base.testng.DefaultReporter; 7 | import com.qianmi.autotest.base.testng.QmDingNotifier; 8 | import com.qianmi.autotest.base.testng.TestRetryListener; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | import org.testng.ITestNGListener; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * APP自动化测试主程序入口 17 | * Created by liuzhaoming on 16/9/23. 18 | */ 19 | @SpringBootApplication(scanBasePackages = {"com.qianmi.autotest", "${autotest.scanPackage:}"}) 20 | public class AppTestApplication extends AbstractTestApplication { 21 | 22 | public static void main(String[] args) { 23 | AppTestApplication appApplication = new AppTestApplication(); 24 | appApplication.runTest(args); 25 | } 26 | 27 | /** 28 | * 初始化TestNG监听器 29 | * 30 | * @return List 监听器 31 | */ 32 | @Override 33 | protected List getListeners() { 34 | List listeners = new ArrayList<>(); 35 | listeners.add(new DefaultReporter()); 36 | 37 | if (Boolean.valueOf(System.getProperty("screenshot"))) { 38 | listeners.add(new ScreenShotListener()); 39 | } 40 | 41 | if (Boolean.valueOf(System.getProperty("testRetry"))) { 42 | listeners.add(new TestRetryListener(AppTestRetryAnalyzer.class)); 43 | } 44 | 45 | if (Boolean.valueOf(System.getProperty("dingNotice"))) { 46 | listeners.add(new QmDingNotifier()); 47 | } 48 | return listeners; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /autotest-app/src/main/java/com/qianmi/autotest/app/common/AppAutotestProperties.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.app.common; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | /** 7 | * 配置文件,主要是处理框架的配置信息 8 | * Created by liuzhaoming on 16/9/27. 9 | */ 10 | @Data 11 | @ToString(callSuper = true) 12 | public class AppAutotestProperties { 13 | 14 | /** 15 | * 默认的APP文件 16 | */ 17 | private String defaultAppFile; 18 | 19 | 20 | /** 21 | * 获取当前APP安装文件 22 | * 23 | * @return APP安装文件 24 | */ 25 | public String getActiveAppFile() { 26 | return System.getProperty("appfile", defaultAppFile); 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /autotest-app/src/main/java/com/qianmi/autotest/app/common/AppPageTest.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.app.common; 2 | 3 | import com.qianmi.autotest.app.AppTestApplication; 4 | import com.qianmi.autotest.app.page.AppPageFacade; 5 | import com.qianmi.autotest.appium.data.DataProvider; 6 | import com.qianmi.autotest.base.common.AutotestUtils; 7 | import com.qianmi.autotest.base.common.BasePageTest; 8 | import com.qianmi.autotest.base.common.BeanFactory; 9 | import com.qianmi.autotest.base.page.AppLoginPage; 10 | import io.appium.java_client.AppiumDriver; 11 | import io.appium.java_client.TouchAction; 12 | import io.appium.java_client.pagefactory.AppiumFieldDecorator; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.apache.commons.lang3.StringUtils; 15 | import org.openqa.selenium.support.PageFactory; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.boot.test.context.SpringBootTest; 18 | import org.testng.annotations.AfterMethod; 19 | import org.testng.annotations.BeforeMethod; 20 | 21 | import javax.annotation.Resource; 22 | import java.lang.reflect.Method; 23 | import java.time.Duration; 24 | 25 | /** 26 | * Test基类 27 | * Created by liuzhaoming on 16/9/23. 28 | */ 29 | @SuppressWarnings({"unused", "WeakerAccess"}) 30 | @Slf4j 31 | @SpringBootTest(classes = AppTestApplication.class) 32 | public class AppPageTest extends BasePageTest { 33 | 34 | @Autowired 35 | protected AppPageFacade pageFacade; 36 | 37 | @Autowired 38 | protected AppiumDriver appiumDriver; 39 | 40 | @Autowired 41 | protected TouchAction touchAction; 42 | 43 | @Resource 44 | protected DataProvider inputData; 45 | 46 | @Resource 47 | protected DataProvider outputData; 48 | 49 | @Autowired 50 | private AppResourceLoader resourceContainer; 51 | 52 | /** 53 | * 注销用户 54 | */ 55 | @AfterMethod 56 | public void logout() { 57 | try { 58 | pageFacade.logout(); 59 | } catch (Exception e) { 60 | log.warn("Logout has error", e); 61 | } 62 | } 63 | 64 | @BeforeMethod 65 | public void login(Method method) { 66 | AppLoginPage loginPage = BeanFactory.getBeanByType(AppLoginPage.class); 67 | String sceneName = AutotestUtils.getSceneName(method); 68 | String userName = inputData.getProperty("userName", sceneName); 69 | String password = inputData.getProperty("password", sceneName); 70 | try { 71 | if (null != loginPage && StringUtils.isNoneBlank(userName)) { 72 | PageFactory.initElements(new AppiumFieldDecorator(appiumDriver, Duration.ofSeconds(1)), loginPage); 73 | loginPage.login(userName, password); 74 | } 75 | } catch (Exception e) { 76 | log.warn("Login has error", e); 77 | resourceContainer.restartApp(); 78 | AutotestUtils.sleep(2000); 79 | try { 80 | if (StringUtils.isNoneBlank(userName)) { 81 | loginPage.login(userName, password); 82 | } 83 | } catch (Exception ignored) { 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /autotest-app/src/main/java/com/qianmi/autotest/app/common/AppResourceLoader.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.app.common; 2 | 3 | import com.qianmi.autotest.appium.common.AppiumAutotestProperties; 4 | import com.qianmi.autotest.appium.common.AppiumResourceLoader; 5 | import com.qianmi.autotest.base.common.AutotestException; 6 | import io.appium.java_client.AppiumDriver; 7 | import io.appium.java_client.android.AndroidDriver; 8 | import io.appium.java_client.ios.IOSDriver; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.openqa.selenium.remote.DesiredCapabilities; 12 | import org.springframework.boot.context.properties.ConfigurationProperties; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.core.io.FileSystemResource; 15 | import org.springframework.http.HttpEntity; 16 | import org.springframework.http.HttpMethod; 17 | import org.springframework.http.MediaType; 18 | import org.springframework.http.ResponseEntity; 19 | import org.springframework.stereotype.Component; 20 | import org.springframework.util.LinkedMultiValueMap; 21 | import org.springframework.util.MultiValueMap; 22 | import org.springframework.web.util.UriComponentsBuilder; 23 | 24 | import java.net.URI; 25 | import java.net.URL; 26 | import java.util.Map; 27 | import java.util.Properties; 28 | 29 | /** 30 | * 资源管理 31 | * Created by liuzhaoming on 16/9/29. 32 | */ 33 | @Component 34 | @Slf4j 35 | public class AppResourceLoader extends AppiumResourceLoader { 36 | /** 37 | * 重启Appium Server 38 | */ 39 | public void restartApp() { 40 | try { 41 | AppiumDriver appiumDriver = getAppiumDriver(); 42 | 43 | if (null == appiumDriver) { 44 | log.error("Restart app fail because appium server is null"); 45 | return; 46 | } 47 | 48 | appiumDriver.closeApp(); 49 | appiumDriver.launchApp(); 50 | } catch (Exception e) { 51 | log.error("Cannot restart app", e); 52 | } 53 | } 54 | 55 | 56 | @Bean 57 | public AppiumDriver appiumDriver(AppAutotestProperties appProperties, AppiumAutotestProperties appiumProperties) { 58 | String appiumServerUrl = initAppiumServer(appiumProperties); 59 | String appiumServerAppFile = getActiveAppFile(appProperties, appiumProperties); 60 | Properties driverConfig = appiumProperties.getDriverConfig(); 61 | DesiredCapabilities capabilities = new DesiredCapabilities(); 62 | for (String name : driverConfig.stringPropertyNames()) { 63 | setCapabilities(name, driverConfig.getProperty(name), capabilities); 64 | } 65 | 66 | if (StringUtils.isNotBlank(appiumServerAppFile)) { 67 | setCapabilities("app", appiumServerAppFile, capabilities); 68 | } 69 | 70 | try { 71 | AppiumDriver appiumDriver; 72 | if (driverConfig.getProperty("platformName", "Android").equalsIgnoreCase("ios")) { 73 | appiumDriver = new IOSDriver(new URL(appiumServerUrl), capabilities); 74 | } else { 75 | appiumDriver = new AndroidDriver(new URL(appiumServerUrl), capabilities); 76 | } 77 | return appiumDriver; 78 | } catch (Exception e) { 79 | log.error("Create appium driver fail {}", appiumServerUrl, e); 80 | throw new AutotestException("Create appium driver fail " + appiumServerUrl); 81 | } 82 | } 83 | 84 | @Bean 85 | @ConfigurationProperties("autotest") 86 | public AppAutotestProperties appAutotestProperties() { 87 | return new AppAutotestProperties(); 88 | } 89 | 90 | /** 91 | * 获取当前生效的APP安装文件路径 92 | * 93 | * @param appProperties app配置参数 94 | * @param appiumProperties appium配置参数 95 | * @return 当前生效的APP安装文件路径 96 | */ 97 | private String getActiveAppFile(AppAutotestProperties appProperties, AppiumAutotestProperties appiumProperties) { 98 | String appFile = appProperties.getActiveAppFile(); 99 | if (appiumProperties.isAppiumServerMngEnable()) { 100 | String appiumServerAppFile = queryAppFileInAppiumServer(appFile, appiumProperties); 101 | if (StringUtils.isNotBlank(appiumServerAppFile)) { 102 | return appiumServerAppFile; 103 | } 104 | 105 | return uploadAppFile(appFile, appiumProperties); 106 | } 107 | 108 | return appFile; 109 | } 110 | 111 | /** 112 | * 获取appium server上app安装文件路径 113 | * 114 | * @param appFile 本地apk文件地址 115 | * @param appiumProperties appium配置参数 116 | * @return appium server上app安装文件路径 117 | */ 118 | private String queryAppFileInAppiumServer(String appFile, AppiumAutotestProperties appiumProperties) { 119 | try { 120 | String url = String.format("%s/appium/appfile", chooseAppiumServerMngUrl(appiumProperties)); 121 | URI finalUrl = UriComponentsBuilder.fromUriString(url).queryParam("appfileName", appFile).build().toUri(); 122 | Map responseBody = restTemplate.getForObject(finalUrl, Map.class); 123 | if (responseBody.containsKey("exist") && (boolean) responseBody.get("exist")) { 124 | return String.valueOf(responseBody.get("appfile")); 125 | } 126 | } catch (Exception e) { 127 | log.error("Fail to get appium server app file {}", appFile, e); 128 | } 129 | 130 | return ""; 131 | } 132 | 133 | /** 134 | * 上次程序安装文件到appium server服务器 135 | * 136 | * @param appFile 本地apk文件地址 137 | * @param appiumProperties appium配置参数 138 | * @return appium server服务器安装文件路径 139 | */ 140 | private String uploadAppFile(String appFile, AppiumAutotestProperties appiumProperties) { 141 | try { 142 | String url = String.format("%s/appium/appfile", chooseAppiumServerMngUrl(appiumProperties)); 143 | FileSystemResource resource = new FileSystemResource(appFile); 144 | MultiValueMap parts = new LinkedMultiValueMap<>(); 145 | parts.add("Content-Type", MediaType.MULTIPART_FORM_DATA); 146 | parts.add("appfile", resource); 147 | 148 | ResponseEntity responseBody = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(parts), Map.class); 149 | if (responseBody.getStatusCode().is2xxSuccessful()) { 150 | return String.valueOf(responseBody.getBody().get("appfile")); 151 | } 152 | } catch (Exception e) { 153 | log.error("Fail to upload app file", e); 154 | } 155 | throw new AutotestException("Fail to upload app file"); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /autotest-app/src/main/java/com/qianmi/autotest/app/page/AppBasePage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.app.page; 2 | 3 | import com.qianmi.autotest.appium.page.AppiumBasePage; 4 | 5 | /** 6 | * APP测试Page基类 7 | * Created by liuzhaoming on 2016/12/6. 8 | */ 9 | @SuppressWarnings("unused") 10 | public abstract class AppBasePage extends AppiumBasePage { 11 | 12 | /** 13 | * This Method for swipe Left 14 | */ 15 | protected void swipeLeft() { 16 | swipeLeft(autotestProperties.getSwipeTimeInMills()); 17 | } 18 | 19 | /** 20 | * This Method for swipe up 21 | */ 22 | protected void swipeUp() { 23 | swipeUp(autotestProperties.getSwipeTimeInMills()); 24 | } 25 | 26 | 27 | /** 28 | * This Method for swipe down 29 | */ 30 | protected void swipeToDown() { 31 | swipeDown(autotestProperties.getSwipeTimeInMills()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /autotest-app/src/main/java/com/qianmi/autotest/app/page/AppPageFacade.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.app.page; 2 | 3 | import com.qianmi.autotest.base.common.BeanFactory; 4 | import com.qianmi.autotest.base.common.Logoutable; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.stereotype.Component; 8 | 9 | /** 10 | * APP页面门户 11 | * Created by liuzhaoming on 16/9/26. 12 | */ 13 | @Component 14 | public class AppPageFacade extends AppBasePage { 15 | private static final Logger LOGGER = LoggerFactory.getLogger(AppPageFacade.class); 16 | 17 | /** 18 | * 退出当前登录用户 19 | */ 20 | public void logout() { 21 | Logoutable logoutable = BeanFactory.getBeanByType(Logoutable.class); 22 | if (null != logoutable) { 23 | logoutable.logout(); 24 | } else { 25 | LOGGER.warn("Logout cannot find Logoutable bean"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /autotest-app/src/main/java/com/qianmi/autotest/app/testng/AppTestRetryAnalyzer.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.app.testng; 2 | 3 | import com.qianmi.autotest.app.common.AppPageTest; 4 | import com.qianmi.autotest.app.common.AppResourceLoader; 5 | import com.qianmi.autotest.base.common.BeanFactory; 6 | import com.qianmi.autotest.base.testng.BaseTestRetryAnalyzer; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.testng.ITestResult; 9 | 10 | /** 11 | * APP 测试失败重试监听器 12 | * Created by liuzhaoming on 2016/12/5. 13 | */ 14 | @Slf4j 15 | public class AppTestRetryAnalyzer extends BaseTestRetryAnalyzer { 16 | /** 17 | * 重启APP 18 | */ 19 | protected void restart(ITestResult result) { 20 | try { 21 | AppResourceLoader resourceContainer = BeanFactory.getBean(AppResourceLoader.class); 22 | resourceContainer.restartApp(); 23 | AppPageTest pageTest = (AppPageTest) result.getInstance(); 24 | pageTest.login(result.getMethod().getConstructorOrMethod().getMethod()); 25 | } catch (Exception e) { 26 | log.error("TestRetryAnalyzer restart app fail {}", result.getMethod().getMethodName(), e); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /autotest-appium/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | autotest-parent 6 | com.qianmi 7 | 2.0.0-SNAPSHOT 8 | ../pom.xml 9 | 10 | 4.0.0 11 | 12 | autotest-appium 13 | ${project.artifactId} 14 | 使用Appium driver的基础包 15 | 16 | 17 | 18 | io.appium 19 | java-client 20 | 21 | 22 | org.projectlombok 23 | lombok 24 | 25 | 26 | com.qianmi 27 | autotest-base 28 | ${project.version} 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /autotest-appium/src/main/java/com/qianmi/autotest/appium/common/AppiumAutotestProperties.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.appium.common; 2 | 3 | import com.qianmi.autotest.base.common.AutotestProperties; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.ToString; 7 | import org.apache.commons.collections4.MapUtils; 8 | 9 | import java.util.Properties; 10 | 11 | /** 12 | * Appium 程序配置参数 13 | * Created by liuzhaoming on 2016/12/6. 14 | */ 15 | @Data 16 | @EqualsAndHashCode(callSuper = true) 17 | @ToString(callSuper = true) 18 | public class AppiumAutotestProperties extends AutotestProperties { 19 | /** 20 | * driver配置属性 21 | */ 22 | protected Properties driver; 23 | 24 | /** 25 | * device配置属性 26 | */ 27 | protected Properties device; 28 | 29 | /** 30 | * 默认device名称 31 | */ 32 | protected String defaultDevice; 33 | 34 | /** 35 | * 是否连接Appium server Manager 36 | */ 37 | protected boolean appiumServerMngEnable = false; 38 | 39 | /** 40 | * Appium server Manager地址,可以配置多个,中间用','分隔 41 | */ 42 | protected String appiumServerMngUrl; 43 | 44 | /** 45 | * appium server地址,只有在不连接Appium server Manager情况下生效 46 | */ 47 | protected String appiumServerUrl; 48 | 49 | /** 50 | * 设备配置信息 51 | */ 52 | protected Properties deviceConfig; 53 | 54 | /** 55 | * appium driver配置信息 56 | */ 57 | protected Properties driverConfig; 58 | 59 | /** 60 | * 获取当前生效的设备名称 61 | * 62 | * @return 设备名称 63 | */ 64 | public String getActiveDeviceName() { 65 | return System.getProperty("deviceName", defaultDevice); 66 | } 67 | 68 | /** 69 | * 获取设备配置信息 70 | * 71 | * @return 设备配置信息 72 | */ 73 | public Properties getDeviceConfig() { 74 | if (MapUtils.isEmpty(deviceConfig)) { 75 | synchronized (this) { 76 | if (MapUtils.isEmpty(deviceConfig)) { 77 | String deviceName = getActiveDeviceName(); 78 | Properties totalDeviceProperties = new Properties(); 79 | String deviceNamePrefix = deviceName + "."; 80 | String driverNamePrefix = deviceName + ".driver."; 81 | device.stringPropertyNames().stream() 82 | .filter(propertyName -> propertyName.startsWith(deviceNamePrefix) && !propertyName.startsWith(driverNamePrefix)) 83 | .forEach(propertyName -> 84 | totalDeviceProperties.setProperty(propertyName.substring(deviceNamePrefix.length()), 85 | device.getProperty(propertyName)) 86 | ); 87 | deviceConfig = totalDeviceProperties; 88 | } 89 | } 90 | } 91 | 92 | return deviceConfig; 93 | } 94 | 95 | /** 96 | * 获取appium driver配置信息 97 | * 98 | * @return appium driver配置信息 99 | */ 100 | public Properties getDriverConfig() { 101 | if (MapUtils.isEmpty(driverConfig)) { 102 | synchronized (this) { 103 | if (MapUtils.isEmpty(driverConfig)) { 104 | String deviceName = getActiveDeviceName(); 105 | Properties totalDriverProperties = new Properties(driver); 106 | String driverNamePrefix = deviceName + ".driver."; 107 | if (MapUtils.isNotEmpty(device)) { 108 | device.stringPropertyNames().stream() 109 | .filter(propertyName -> propertyName.startsWith(driverNamePrefix)) 110 | .forEach(propertyName -> 111 | totalDriverProperties.setProperty(propertyName.substring(driverNamePrefix.length()), 112 | device.getProperty(propertyName)) 113 | ); 114 | } 115 | driverConfig = totalDriverProperties; 116 | } 117 | } 118 | } 119 | 120 | return driverConfig; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /autotest-appium/src/main/java/com/qianmi/autotest/appium/common/AppiumResourceLoader.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.appium.common; 2 | 3 | import com.qianmi.autotest.base.common.AutotestException; 4 | import com.qianmi.autotest.base.common.BeanFactory; 5 | import io.appium.java_client.AppiumDriver; 6 | import io.appium.java_client.TouchAction; 7 | import io.appium.java_client.android.AndroidDriver; 8 | import io.appium.java_client.ios.IOSDriver; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.openqa.selenium.remote.DesiredCapabilities; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.context.properties.ConfigurationProperties; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.http.converter.StringHttpMessageConverter; 16 | import org.springframework.web.client.RestTemplate; 17 | import org.springframework.web.util.UriComponentsBuilder; 18 | 19 | import javax.annotation.PostConstruct; 20 | import javax.annotation.PreDestroy; 21 | import javax.annotation.Resource; 22 | import java.net.URI; 23 | import java.nio.charset.Charset; 24 | import java.util.Collections; 25 | import java.util.HashMap; 26 | import java.util.Map; 27 | import java.util.Properties; 28 | 29 | /** 30 | * Appium资源加载器 31 | * Created by liuzhaoming on 2016/12/6. 32 | */ 33 | @Slf4j 34 | @SuppressWarnings("WeakerAccess") 35 | public class AppiumResourceLoader { 36 | @Resource 37 | protected RestTemplate restTemplate; 38 | 39 | @Autowired 40 | private AppiumAutotestProperties appiumAutotestProperties; 41 | 42 | private String appiumServerMngUrl; 43 | 44 | @Bean 45 | @ConfigurationProperties("autotest") 46 | public AppiumAutotestProperties appiumAutotestProperties() { 47 | return new AppiumAutotestProperties(); 48 | } 49 | 50 | /** 51 | * 系统关闭的时候关闭Appium连接 52 | */ 53 | @PreDestroy 54 | public void stopAppiumServer() { 55 | log.info("Begin to stop appium server"); 56 | if (!appiumAutotestProperties.isAppiumServerMngEnable()) { 57 | return; 58 | } 59 | 60 | Properties deviceConfig = appiumAutotestProperties.getDeviceConfig(); 61 | String deviceIp = deviceConfig.getProperty("ip"); 62 | String devicePort = deviceConfig.getProperty("port"); 63 | String url = String.format("%s/appium/servers", appiumServerMngUrl); 64 | UriComponentsBuilder urlBuilder = UriComponentsBuilder.fromUriString(url).queryParam("deviceIp", deviceIp); 65 | if (StringUtils.isNotBlank(devicePort)) { 66 | urlBuilder.queryParam("devicePort", devicePort); 67 | } 68 | 69 | URI deleteUrl = urlBuilder.build().toUri(); 70 | 71 | try { 72 | restTemplate.delete(deleteUrl); 73 | } catch (Exception e) { 74 | log.error("Fail to stop appium server {} {}", deviceIp, devicePort, e); 75 | } 76 | } 77 | 78 | @Bean 79 | public RestTemplate restTemplate() { 80 | RestTemplate restTemplate = new RestTemplate(); 81 | restTemplate.getMessageConverters() 82 | .add(0, new StringHttpMessageConverter(Charset.forName("UTF-8"))); 83 | 84 | return restTemplate; 85 | } 86 | 87 | @Bean 88 | public TouchAction touchAction(AppiumDriver driver) { 89 | return new TouchAction(driver); 90 | } 91 | 92 | @Bean 93 | public AppiumWait appiumWait() { 94 | return new AppiumWait(); 95 | } 96 | 97 | @PostConstruct 98 | public void printStartLog() { 99 | log.warn("~~~~~||~~~~~ Start test by deviceName:{}", appiumAutotestProperties.getActiveDeviceName()); 100 | } 101 | 102 | /** 103 | * 获取Appium 驱动 104 | * 105 | * @return Appium驱动 106 | */ 107 | protected AppiumDriver getAppiumDriver() { 108 | AppiumDriver appiumDriver = BeanFactory.getBeanByType(AndroidDriver.class); 109 | if (null == appiumDriver) { 110 | appiumDriver = BeanFactory.getBeanByType(IOSDriver.class); 111 | } 112 | 113 | return appiumDriver; 114 | } 115 | 116 | /** 117 | * 远程启动一个Appium Server 118 | * 119 | * @param appiumAutotestProperties appium配置参数 120 | * @param deviceIp 手机地址 121 | * @param devicePort 手机通讯端口 122 | * @return Appium Server URL 123 | */ 124 | private String startAppiumServer(AppiumAutotestProperties appiumAutotestProperties, String deviceIp, String devicePort) { 125 | String url = String.format("%s/appium/servers", chooseAppiumServerMngUrl(appiumAutotestProperties)); 126 | Map requestBody = new HashMap<>(); 127 | requestBody.put("deviceIp", deviceIp); 128 | if (StringUtils.isNotBlank(devicePort)) { 129 | requestBody.put("devicePort", devicePort); 130 | } 131 | 132 | try { 133 | Map response = restTemplate.postForObject(url, requestBody, Map.class); 134 | Object message = response.get("message"); 135 | if (null != message) { 136 | String detail = null; 137 | if (message instanceof String) { 138 | detail = (String) message; 139 | 140 | } else if (message instanceof Map) { 141 | detail = (String) (((Map) message).get("detail")); 142 | } 143 | throw new AutotestException(String.format("Cannot start appium server deviceIp=%s devicePort=%s " + 144 | "detail=%s", deviceIp, devicePort, detail)); 145 | } 146 | 147 | int appiumServerPort = ((Double) response.get("server_port")).intValue(); 148 | String appiumServerIp = (String) response.get("server_ip"); 149 | if (appiumServerPort == 0 || StringUtils.isBlank(appiumServerIp)) { 150 | log.error("Cannot start appium server {} {}", deviceIp, devicePort); 151 | throw new AutotestException(String.format("Cannot start appium server deviceIp=%s devicePort=%s", 152 | deviceIp, devicePort)); 153 | } 154 | 155 | return String.format("http://%s:%s/wd/hub", appiumServerIp, appiumServerPort); 156 | } catch (Exception e) { 157 | log.error("Fail to start appium server {} {}", deviceIp, devicePort, e); 158 | throw new AutotestException(String.format("Fail to start appium server deviceIp=%s devicePort=%s", 159 | deviceIp, devicePort)); 160 | } 161 | } 162 | 163 | /** 164 | * 获取Appium Server Manger服务器上正在运行的Appium server信息 165 | * 166 | * @param mngUrl Appium Server Manger服务器 167 | * @return 正在运行的Appium server信息 168 | */ 169 | @SuppressWarnings("unchecked") 170 | private Map getAppiumServerInfo(String mngUrl) { 171 | String url = String.format("%s/appium/servers", mngUrl); 172 | try { 173 | return restTemplate.getForObject(url, Map.class); 174 | } catch (Exception e) { 175 | log.error("Fail to get appium server info from {}", mngUrl, e); 176 | return Collections.emptyMap(); 177 | } 178 | } 179 | 180 | /** 181 | * 设置配置属性 182 | * 183 | * @param name 属性名称 184 | * @param value 属性值 185 | * @param capabilities 配置容器 186 | */ 187 | protected void setCapabilities(String name, String value, DesiredCapabilities capabilities) { 188 | if (null == value) { 189 | return; 190 | } 191 | 192 | capabilities.setCapability(name, value); 193 | } 194 | 195 | /** 196 | * 选择一个连接数最小的appium mng server 197 | * 198 | * @param appiumAutotestProperties appium配置参数 199 | * @return appium mng server url 200 | */ 201 | protected String chooseAppiumServerMngUrl(AppiumAutotestProperties appiumAutotestProperties) { 202 | if (null == appiumServerMngUrl) { 203 | synchronized (this) { 204 | if (null == appiumServerMngUrl) { 205 | if (!appiumAutotestProperties.isAppiumServerMngEnable()) { 206 | appiumServerMngUrl = ""; 207 | } 208 | 209 | Properties deviceConfig = appiumAutotestProperties.getDeviceConfig(); 210 | String deviceIp = deviceConfig.getProperty("ip"); 211 | String devicePort = deviceConfig.getProperty("port", "5555"); 212 | String duid = String.format("%s:%s", deviceIp, devicePort); 213 | 214 | String[] allMngUrls = appiumAutotestProperties.getAppiumServerMngUrl().split(","); 215 | int minCount = 1000; 216 | String curMngUrl = ""; 217 | for (String mngUrl : allMngUrls) { 218 | Map appiumServerInfo = getAppiumServerInfo(mngUrl); 219 | for (String appiumServerKey : appiumServerInfo.keySet()) { 220 | if (appiumServerKey.equalsIgnoreCase(duid)) { 221 | return mngUrl; 222 | } 223 | } 224 | 225 | int curSize = appiumServerInfo.size(); 226 | if (minCount > curSize) { 227 | minCount = curSize; 228 | curMngUrl = mngUrl; 229 | } 230 | } 231 | 232 | appiumServerMngUrl = curMngUrl; 233 | } 234 | } 235 | } 236 | 237 | return appiumServerMngUrl; 238 | } 239 | 240 | /** 241 | * 初始化appium server 242 | * 243 | * @param appiumAutotestProperties appium配置参数 244 | * @return appium server url 245 | */ 246 | protected String initAppiumServer(AppiumAutotestProperties appiumAutotestProperties) { 247 | if (appiumAutotestProperties.isAppiumServerMngEnable()) { 248 | Properties deviceConfig = appiumAutotestProperties.getDeviceConfig(); 249 | String deviceIp = deviceConfig.getProperty("ip"); 250 | String devicePort = deviceConfig.getProperty("port"); 251 | 252 | return startAppiumServer(appiumAutotestProperties, deviceIp, devicePort); 253 | } 254 | 255 | return appiumAutotestProperties.getAppiumServerUrl(); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /autotest-appium/src/main/java/com/qianmi/autotest/appium/common/AppiumWait.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.appium.common; 2 | 3 | import com.qianmi.autotest.base.common.AutotestUtils; 4 | import io.appium.java_client.AppiumDriver; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.openqa.selenium.By; 7 | import org.openqa.selenium.WebElement; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | 10 | /** 11 | * appium控件等待控制 12 | * Created by liuzhaoming on 2016/10/19. 13 | */ 14 | @Slf4j 15 | public class AppiumWait { 16 | @Autowired 17 | private AppiumDriver appiumDriver; 18 | 19 | @Autowired 20 | private AppiumAutotestProperties autotestProperties; 21 | 22 | /** 23 | * 等待界面元素显示 24 | * 25 | * @param webElement webElement 26 | * @return webElement 27 | */ 28 | @SuppressWarnings("unused") 29 | public WebElement wait(WebElement webElement) { 30 | if (log.isDebugEnabled()) { 31 | log.info("Begin wait WebElement {}", AutotestUtils.getWebElementDesc(webElement)); 32 | } 33 | 34 | int maxCount = autotestProperties.getElementLoadTimeInMills() / autotestProperties.getRefreshIntervalInMills(); 35 | for (int i = 0; i < maxCount; i++) { 36 | if (log.isDebugEnabled()) { 37 | log.info("Find WebElement {} for {} try", AutotestUtils.getWebElementDesc(webElement), i); 38 | } 39 | if (exist(webElement)) { 40 | if (log.isDebugEnabled()) { 41 | log.info("Finish wait WebElement {}", AutotestUtils.getWebElementDesc(webElement)); 42 | } 43 | return webElement; 44 | } 45 | 46 | sleep(); 47 | } 48 | 49 | log.info("Cannot find WebElement {} within {} try", AutotestUtils.getWebElementDesc(webElement), maxCount); 50 | return webElement; 51 | } 52 | 53 | /** 54 | * 根据ID查询界面元素 55 | * 56 | * @param id 界面元素ID 57 | * @return webElement 58 | */ 59 | public WebElement wait(String id) { 60 | log.info("Begin wait WebElement by id {}", id); 61 | int maxCount = autotestProperties.getElementLoadTimeInMills() / autotestProperties.getRefreshIntervalInMills(); 62 | for (int i = 0; i < maxCount; i++) { 63 | log.info("Find WebElement by id {} for {} try", id, i); 64 | WebElement webElement = findById(id); 65 | if (null != webElement) { 66 | log.info("Finish wait WebElement by id {}", id); 67 | return webElement; 68 | } 69 | 70 | sleep(); 71 | } 72 | 73 | log.info("Cannot find WebElement by id {} within {} try", id, maxCount); 74 | return null; 75 | } 76 | 77 | /** 78 | * 判断元素是否存在 79 | * 80 | * @param webElement 界面元素 81 | * @return boolean 82 | */ 83 | private boolean exist(WebElement webElement) { 84 | long startTime = System.currentTimeMillis(); 85 | try { 86 | webElement.isEnabled(); 87 | return true; 88 | } catch (Exception e) { 89 | log.info("Exist WebElement spend time {} ms", System.currentTimeMillis() - startTime); 90 | return false; 91 | } 92 | } 93 | 94 | /** 95 | * 根据id查找元素 96 | * 97 | * @param id id 98 | * @return WebElement 99 | */ 100 | private WebElement findById(String id) { 101 | try { 102 | return appiumDriver.findElement(By.id(id)); 103 | } catch (Exception e) { 104 | return null; 105 | } 106 | } 107 | 108 | /** 109 | * 当前线程sleep等待 110 | */ 111 | private void sleep() { 112 | try { 113 | Thread.sleep(autotestProperties.getRefreshIntervalInMills()); 114 | } catch (Exception ignored) { 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /autotest-appium/src/main/java/com/qianmi/autotest/appium/data/DataInitialization.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.appium.data; 2 | 3 | import com.qianmi.autotest.appium.common.AppiumAutotestProperties; 4 | import lombok.Data; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.PropertySource; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.util.Properties; 12 | 13 | /** 14 | * 测试数据 15 | * Created by liuzhaoming on 16/9/27. 16 | */ 17 | @Data 18 | @Component 19 | @ConfigurationProperties 20 | @PropertySource("classpath:data.properties") 21 | public class DataInitialization { 22 | private Properties input; 23 | 24 | private Properties output; 25 | 26 | private Properties device; 27 | 28 | @Autowired 29 | private AppiumAutotestProperties appiumAutotestProperties; 30 | 31 | @Bean 32 | public DataProvider inputData() { 33 | Properties totalInputProperties = new Properties(); 34 | if (null != input) { 35 | totalInputProperties.putAll(input); 36 | } 37 | 38 | if (null != device) { 39 | String deviceNamePrefix = appiumAutotestProperties.getActiveDeviceName() + ".input."; 40 | device.stringPropertyNames().stream() 41 | .filter(propertyName -> propertyName.startsWith(deviceNamePrefix)) 42 | .forEach(propertyName -> 43 | totalInputProperties.setProperty(propertyName.substring(deviceNamePrefix.length()), 44 | device.getProperty(propertyName)) 45 | ); 46 | } 47 | 48 | return new DataProvider(totalInputProperties); 49 | } 50 | 51 | @Bean 52 | public DataProvider outputData() { 53 | Properties totalOutputProperties = new Properties(); 54 | if (null != output) { 55 | totalOutputProperties.putAll(output); 56 | } 57 | 58 | if (null != device) { 59 | String deviceNamePrefix = appiumAutotestProperties.getActiveDeviceName() + ".output."; 60 | device.stringPropertyNames().stream() 61 | .filter(propertyName -> propertyName.startsWith(deviceNamePrefix)) 62 | .forEach(propertyName -> 63 | totalOutputProperties.setProperty(propertyName.substring(deviceNamePrefix.length()), 64 | device.getProperty(propertyName)) 65 | ); 66 | } 67 | return new DataProvider(totalOutputProperties); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /autotest-appium/src/main/java/com/qianmi/autotest/appium/data/DataProvider.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.appium.data; 2 | 3 | import java.io.Serializable; 4 | import java.util.Properties; 5 | 6 | /** 7 | * 测试数据封装类 8 | * Created by liuzhaoming on 2016/11/14. 9 | */ 10 | public class DataProvider implements Serializable { 11 | private Properties originData; 12 | 13 | public DataProvider(Properties originData) { 14 | this.originData = originData; 15 | } 16 | 17 | public String getProperty(String name) { 18 | return originData.getProperty(name); 19 | } 20 | 21 | public String getProperty(String name, String sceneName) { 22 | String propertyName = String.format("scene.%s.%s", sceneName, name); 23 | if (originData.containsKey(propertyName)) { 24 | return originData.getProperty(propertyName); 25 | } 26 | 27 | return originData.getProperty(name); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /autotest-appium/src/main/java/com/qianmi/autotest/appium/page/AppiumBasePage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.appium.page; 2 | 3 | import com.qianmi.autotest.appium.common.AppiumAutotestProperties; 4 | import com.qianmi.autotest.appium.common.AppiumWait; 5 | import com.qianmi.autotest.base.common.BeanFactory; 6 | import com.qianmi.autotest.base.page.PageObject; 7 | import io.appium.java_client.AppiumDriver; 8 | import io.appium.java_client.TouchAction; 9 | import io.appium.java_client.android.AndroidDriver; 10 | import io.appium.java_client.ios.IOSDriver; 11 | import io.appium.java_client.pagefactory.AppiumFieldDecorator; 12 | import io.appium.java_client.touch.WaitOptions; 13 | import io.appium.java_client.touch.offset.PointOption; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.openqa.selenium.NoSuchElementException; 16 | import org.openqa.selenium.WebDriver; 17 | import org.openqa.selenium.WebElement; 18 | import org.openqa.selenium.support.PageFactory; 19 | import org.openqa.selenium.support.ui.ExpectedCondition; 20 | import org.openqa.selenium.support.ui.WebDriverWait; 21 | import org.springframework.beans.factory.annotation.Autowired; 22 | 23 | import javax.annotation.Nullable; 24 | import java.time.Duration; 25 | 26 | /** 27 | * APP测试基类 28 | * Created by liuzhaoming on 16/9/23. 29 | */ 30 | @Slf4j 31 | @SuppressWarnings({"WeakerAccess", "unused"}) 32 | public abstract class AppiumBasePage implements PageObject { 33 | @Autowired 34 | protected AppiumDriver driver; 35 | 36 | @Autowired 37 | protected TouchAction touchAction; 38 | 39 | @Autowired 40 | protected AppiumAutotestProperties autotestProperties; 41 | 42 | @Autowired 43 | private AppiumWait appiumWait; 44 | 45 | /** 46 | * 页面跳转 47 | * 48 | * @param tClass Page Class 49 | * @param 泛型 50 | * @return Page页面 51 | */ 52 | public T gotoPage(Class tClass) { 53 | log.info("Begin goto page " + tClass.getName()); 54 | T page = BeanFactory.getBean(tClass); 55 | PageFactory.initElements(new AppiumFieldDecorator(driver, Duration.ofSeconds(getPageInitTime())), page); 56 | page.afterConstruct(); 57 | return page; 58 | } 59 | 60 | /** 61 | * 初始化页面 62 | * 63 | * @param page page 64 | * @param 泛型 65 | * @return 页面 66 | */ 67 | protected T initPage(T page) { 68 | PageFactory.initElements(new AppiumFieldDecorator(driver, Duration.ofSeconds(getPageInitTime())), page); 69 | page.afterConstruct(); 70 | return page; 71 | } 72 | 73 | /** 74 | * 线程阻塞,供页面渲染 75 | */ 76 | protected void sleep() { 77 | sleepInMillTime(autotestProperties.getPageLoadTimeInMills()); 78 | } 79 | 80 | /** 81 | * Page init time 82 | * 83 | * @return 默认值为1 84 | */ 85 | protected int getPageInitTime() { 86 | return 1; 87 | } 88 | 89 | /** 90 | * Page元素构造完成后需要执行的操作 91 | */ 92 | protected void afterConstruct() { 93 | 94 | } 95 | 96 | /** 97 | * 等待Page加载某个元素或者查询条件完成 98 | * 99 | * @param id 元素id 100 | * @return WebElement 101 | */ 102 | protected WebElement wait(String id) { 103 | return appiumWait.wait(id); 104 | } 105 | 106 | /** 107 | * 等待Page加载某个元素 108 | * 109 | * @param webElement 页面元素 110 | * @return WebElement 111 | */ 112 | protected WebElement wait(WebElement webElement) { 113 | return wait(webElement, autotestProperties.getElementLoadTimeInMills()); 114 | } 115 | 116 | /** 117 | * 等待Page加载某个元素 118 | * 119 | * @param webElement 页面元素 120 | * @param timeOutInMills 最大等待时间,毫秒值 121 | * @return WebElement 122 | */ 123 | protected WebElement wait(WebElement webElement, int timeOutInMills) { 124 | new WebDriverWait(driver, autotestProperties.getElementLoadTimeInMills() / 1000, autotestProperties.getRefreshIntervalInMills()) 125 | .until(new ExpectedCondition() { 126 | @Nullable 127 | @Override 128 | public WebElement apply(@Nullable WebDriver driver) { 129 | if (isExist(webElement)) { 130 | return webElement; 131 | } else { 132 | return null; 133 | } 134 | } 135 | }); 136 | 137 | return webElement; 138 | } 139 | 140 | /** 141 | * 判断Page 元素是否存在 142 | * 143 | * @param webElement Page元素 144 | * @return boolean 145 | */ 146 | protected boolean isExist(WebElement webElement) { 147 | if (null == webElement) { 148 | return false; 149 | } 150 | 151 | try { 152 | webElement.isDisplayed(); 153 | return true; 154 | } catch (NoSuchElementException e) { 155 | return false; 156 | } 157 | } 158 | 159 | 160 | /** 161 | * 判断Page 元素是否存在 162 | * 163 | * @param webElement Page元素 164 | * @param timeOutInMills 超时时间,单位为毫秒 165 | * @return boolean 166 | */ 167 | protected boolean isExist(WebElement webElement, int timeOutInMills) { 168 | if (null == webElement) { 169 | return false; 170 | } 171 | 172 | try { 173 | new WebDriverWait(driver, timeOutInMills / 1000, autotestProperties.getRefreshIntervalInMills()) 174 | .until(new ExpectedCondition() { 175 | @Nullable 176 | @Override 177 | public WebElement apply(@Nullable WebDriver driver) { 178 | if (isExist(webElement)) { 179 | return webElement; 180 | } else { 181 | return null; 182 | } 183 | } 184 | }); 185 | 186 | return true; 187 | } catch (Exception e) { 188 | return false; 189 | } 190 | } 191 | 192 | /** 193 | * 使用TouchAction滚动屏幕 194 | * 195 | * @param startX 起始点X坐标 196 | * @param startY 起始点Y坐标 197 | * @param finishX 结束点X坐标 198 | * @param finishY 结束点Y坐标 199 | * @param duringInMills 滚动时长(ms) 200 | */ 201 | protected void swipe(int startX, int startY, int finishX, int finishY, int duringInMills) { 202 | PointOption startPoint = PointOption.point(startX, startY); 203 | PointOption finishPoint = PointOption.point(finishX, finishY); 204 | swipe(startPoint, finishPoint, duringInMills); 205 | } 206 | 207 | /** 208 | * 使用TouchAction滚动屏幕 209 | * 210 | * @param startPoint 起始点 211 | * @param finishPoint 结束点 212 | * @param duringInMills 滚动时长(ms) 213 | */ 214 | protected void swipe(PointOption startPoint, PointOption finishPoint, int duringInMills) { 215 | touchAction.press(startPoint) 216 | .waitAction(WaitOptions.waitOptions(Duration.ofMillis(duringInMills))) 217 | .moveTo(finishPoint) 218 | .release() 219 | .perform(); 220 | } 221 | 222 | /** 223 | * This Method for swipe up 224 | * 225 | * @param duringInMills 滑动速度 等待多少毫秒 226 | */ 227 | protected void swipeUp(int duringInMills) { 228 | int width = driver.manage().window().getSize().width; 229 | int height = driver.manage().window().getSize().height; 230 | PointOption startPoint = PointOption.point(width / 2, height * 3 / 4); 231 | PointOption finishPoint = PointOption.point(width / 2, height / 4); 232 | swipe(startPoint, finishPoint, duringInMills); 233 | } 234 | 235 | /** 236 | * This Method for swipe down 237 | * 238 | * @param duringInMills 滑动速度 等待多少毫秒 239 | */ 240 | protected void swipeDown(int duringInMills) { 241 | int width = driver.manage().window().getSize().width; 242 | int height = driver.manage().window().getSize().height; 243 | PointOption startPoint = PointOption.point(width / 2, height / 4); 244 | PointOption finishPoint = PointOption.point(width / 2, height * 3 / 4); 245 | swipe(startPoint, finishPoint, duringInMills); 246 | } 247 | 248 | /** 249 | * This Method for swipe Left 250 | * 251 | * @param duringInMills 滑动速度 等待多少毫秒 252 | */ 253 | protected void swipeLeft(int duringInMills) { 254 | int width = driver.manage().window().getSize().width; 255 | int height = driver.manage().window().getSize().height; 256 | PointOption startPoint = PointOption.point(width * 3 / 4, height / 2); 257 | PointOption finishPoint = PointOption.point(width / 4, height / 2); 258 | swipe(startPoint, finishPoint, duringInMills); 259 | } 260 | 261 | /** 262 | * This Method for swipe right 263 | * 264 | * @param duringInMills 滑动速度 等待多少毫秒 265 | */ 266 | protected void swipeRight(int duringInMills) { 267 | int width = driver.manage().window().getSize().width; 268 | int height = driver.manage().window().getSize().height; 269 | PointOption startPoint = PointOption.point(width / 4, height / 2); 270 | PointOption finishPoint = PointOption.point(width * 3 / 4, height / 2); 271 | swipe(startPoint, finishPoint, duringInMills); 272 | } 273 | 274 | /** 275 | * 是否是Android系统 276 | * 277 | * @return boolean 278 | */ 279 | protected boolean isIosPlatform() { 280 | return driver instanceof IOSDriver; 281 | } 282 | 283 | /** 284 | * 是否是IOS系统 285 | * 286 | * @return boolean 287 | */ 288 | protected boolean isAndroidPlatform() { 289 | return driver instanceof AndroidDriver; 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /autotest-appium/src/main/java/com/qianmi/autotest/appium/testng/ScreenShotListener.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.appium.testng; 2 | 3 | import com.qianmi.autotest.base.common.BeanFactory; 4 | import io.appium.java_client.AppiumDriver; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.apache.commons.io.FileUtils; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.openqa.selenium.OutputType; 9 | import org.testng.ITestContext; 10 | import org.testng.ITestListener; 11 | import org.testng.ITestResult; 12 | import org.testng.Reporter; 13 | 14 | import java.io.File; 15 | import java.text.SimpleDateFormat; 16 | import java.util.Date; 17 | 18 | /** 19 | * 失败用例截屏 20 | * Created by liuzhaoming on 16/10/10. 21 | */ 22 | @Slf4j 23 | public class ScreenShotListener implements ITestListener { 24 | /** 25 | * Invoked each time before a test will be invoked. 26 | * The ITestResult is only partially filled with the references to 27 | * class, method, start millis and status. 28 | * 29 | * @param result the partially filled ITestResult 30 | * @see ITestResult#STARTED 31 | */ 32 | @Override 33 | public void onTestStart(ITestResult result) { 34 | 35 | } 36 | 37 | /** 38 | * Invoked each time a test succeeds. 39 | * 40 | * @param result ITestResult containing information about the run test 41 | * @see ITestResult#SUCCESS 42 | */ 43 | @Override 44 | public void onTestSuccess(ITestResult result) { 45 | } 46 | 47 | /** 48 | * Invoked each time a test fails. 49 | * 50 | * @param result ITestResult containing information about the run test 51 | * @see ITestResult#FAILURE 52 | */ 53 | @Override 54 | public void onTestFailure(ITestResult result) { 55 | log.info("onTestFailure is called"); 56 | saveScreenShot(result); 57 | } 58 | 59 | /** 60 | * Invoked each time a test is skipped. 61 | * 62 | * @param result ITestResult containing information about the run test 63 | * @see ITestResult#SKIP 64 | */ 65 | @Override 66 | public void onTestSkipped(ITestResult result) { 67 | } 68 | 69 | /** 70 | * Invoked each time a method fails but has been annotated with 71 | * successPercentage and this failure still keeps it within the 72 | * success percentage requested. 73 | * 74 | * @param result ITestResult containing information about the run test 75 | * @see ITestResult#SUCCESS_PERCENTAGE_FAILURE 76 | */ 77 | @Override 78 | public void onTestFailedButWithinSuccessPercentage(ITestResult result) { 79 | 80 | } 81 | 82 | /** 83 | * Invoked after the test class is instantiated and before 84 | * any configuration method is called. 85 | * 86 | * @param context context 87 | */ 88 | @Override 89 | public void onStart(ITestContext context) { 90 | 91 | } 92 | 93 | /** 94 | * Invoked after all the tests have run and all their 95 | * Configuration methods have been called. 96 | * 97 | * @param context context 98 | */ 99 | @Override 100 | public void onFinish(ITestContext context) { 101 | 102 | } 103 | 104 | private void saveScreenShot(ITestResult result) { 105 | SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); 106 | String mDateTime = formatter.format(new Date()); 107 | String fileName = mDateTime + "_" + result.getName(); 108 | String filePath; 109 | try { 110 | AppiumDriver appiumDriver = BeanFactory.getBean(AppiumDriver.class); 111 | if (null == appiumDriver) { 112 | return; 113 | } 114 | File screenshot = appiumDriver.getScreenshotAs(OutputType.FILE); 115 | filePath = new File(result.getTestContext().getOutputDirectory()).getParentFile().getAbsolutePath() + 116 | "/screenshot/" + fileName + ".jpg"; 117 | File destFile = new File(filePath); 118 | FileUtils.copyFile(screenshot, destFile); 119 | 120 | } catch (Exception e) { 121 | log.error("Fail to screenshot {}", fileName, e); 122 | return; 123 | } 124 | 125 | if (StringUtils.isNoneBlank(filePath)) { 126 | Reporter.setCurrentTestResult(result); 127 | Reporter.log("Not passed test " + result.getName() + " screenshot is : "); 128 | //把截图写入到Html报告中方便查看 129 | Reporter.log(""); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /autotest-base/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | autotest-parent 6 | com.qianmi 7 | 2.0.0-SNAPSHOT 8 | ../pom.xml 9 | 10 | 4.0.0 11 | 12 | autotest-base 13 | ${project.artifactId} 14 | 自动化测试基础包 15 | 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter-test 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-configuration-processor 24 | 25 | 26 | org.springframework 27 | spring-web 28 | 29 | 30 | org.projectlombok 31 | lombok 32 | 33 | 34 | org.testng 35 | testng 36 | 37 | 38 | org.seleniumhq.selenium 39 | selenium-java 40 | 41 | 42 | org.reflections 43 | reflections 44 | 45 | 46 | org.apache.commons 47 | commons-lang3 48 | 49 | 50 | org.apache.commons 51 | commons-collections4 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /autotest-base/src/main/java/com/qianmi/autotest/base/AbstractTestApplication.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.base; 2 | 3 | import com.qianmi.autotest.base.common.AutotestUtils; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.apache.commons.collections4.CollectionUtils; 6 | import org.apache.commons.lang3.StringUtils; 7 | import org.openqa.selenium.WebDriver; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.testng.ITestNGListener; 10 | import org.testng.TestNG; 11 | 12 | import javax.annotation.PreDestroy; 13 | import java.util.ArrayList; 14 | import java.util.Arrays; 15 | import java.util.Collections; 16 | import java.util.List; 17 | import java.util.stream.Collectors; 18 | 19 | /** 20 | * Appium 应用程序基类 21 | * Created by liuzhaoming on 2016/12/6. 22 | */ 23 | @Slf4j 24 | public abstract class AbstractTestApplication { 25 | 26 | @Autowired 27 | private WebDriver driver; 28 | 29 | /** 30 | * Run测试用例 31 | * 32 | * @param args 参数 33 | */ 34 | protected void runTest(String[] args) { 35 | log.info("Run args is {}", Arrays.toString(args)); 36 | AutotestUtils.initSystemProperties(args); 37 | 38 | TestNG testng = TestNG.privateMain(getTestNGArgs(), null); 39 | System.exit(testng.getStatus()); 40 | } 41 | 42 | /** 43 | * 获取TestNG启动参数 44 | * 45 | * @return testng 启动参数 46 | */ 47 | protected String[] getTestNGArgs() { 48 | String methodNames = String.join(",", AutotestUtils.getTestMethods()); 49 | List argList = new ArrayList<>(); 50 | Collections.addAll(argList, "-methods", methodNames, "-usedefaultlisteners", "false"); 51 | 52 | List listeners = getListeners(); 53 | if (CollectionUtils.isNotEmpty(listeners)) { 54 | String listenerStr = listeners.stream() 55 | .map(listener -> listener.getClass().getName()) 56 | .collect(Collectors.joining(",")); 57 | Collections.addAll(argList, "-listener", listenerStr); 58 | } 59 | 60 | String outputDir = System.getProperty("testOutputDirectory"); 61 | if (StringUtils.isNotBlank(outputDir)) { 62 | Collections.addAll(argList, "-d", outputDir); 63 | } 64 | 65 | return argList.toArray(new String[0]); 66 | } 67 | 68 | /** 69 | * 子类可以重新此方法 70 | * 71 | * @return List 监听器 72 | */ 73 | protected abstract List getListeners(); 74 | 75 | /** 76 | * 系统关闭时关闭driver 77 | */ 78 | @PreDestroy 79 | public void closeDriver() { 80 | if (null != driver) { 81 | driver.quit(); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /autotest-base/src/main/java/com/qianmi/autotest/base/common/AutotestException.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.base.common; 2 | 3 | import lombok.*; 4 | 5 | /** 6 | * 异常基类 7 | * Created by liuzhaoming on 16/9/26. 8 | */ 9 | @Data 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | @RequiredArgsConstructor 13 | @EqualsAndHashCode(callSuper = true) 14 | public class AutotestException extends RuntimeException { 15 | /** 16 | * 异常码 17 | */ 18 | private int code; 19 | 20 | /** 21 | * 异常信息 22 | */ 23 | @NonNull 24 | private String message; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /autotest-base/src/main/java/com/qianmi/autotest/base/common/AutotestProperties.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.base.common; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * 自动化测试参数 9 | * Created by liuzhaoming on 2019/1/7. 10 | */ 11 | @Data 12 | public class AutotestProperties implements Serializable { 13 | /** 14 | * wait元素的总时间,单位为毫秒,超过此时间就认为超时 15 | */ 16 | private int elementLoadTimeInMills = 10000; 17 | 18 | /** 19 | * wait元素的轮询间隔,单位为毫秒,即wait某个元素时,每隔多少时间取查看一次界面是否存在此元素 20 | */ 21 | private int refreshIntervalInMills = 100; 22 | 23 | /** 24 | * 新页面加载后等待时间,单位为毫秒 25 | */ 26 | private int pageLoadTimeInMills = 5000; 27 | 28 | /** 29 | * 页面左右滑动时间,单位为毫秒 30 | */ 31 | private int swipeTimeInMills = 1000; 32 | 33 | /** 34 | * 测试程序中需要Spring扫描的包路径 35 | */ 36 | private String scanPackage; 37 | } 38 | -------------------------------------------------------------------------------- /autotest-base/src/main/java/com/qianmi/autotest/base/common/AutotestUtils.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.base.common; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.apache.commons.lang3.math.NumberUtils; 5 | import org.openqa.selenium.WebElement; 6 | import org.reflections.Reflections; 7 | import org.reflections.scanners.MethodAnnotationsScanner; 8 | import org.testng.annotations.Test; 9 | 10 | import java.lang.reflect.Method; 11 | import java.lang.reflect.Modifier; 12 | import java.math.BigDecimal; 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.Set; 16 | import java.util.stream.Collectors; 17 | import java.util.stream.Stream; 18 | 19 | /** 20 | * 通用工具类 21 | * Created by liuzhaoming on 16/9/27. 22 | */ 23 | @SuppressWarnings({"WeakerAccess", "unused", "unchecked"}) 24 | public final class AutotestUtils { 25 | private AutotestUtils() { 26 | } 27 | 28 | /** 29 | * 获取价格 30 | * 31 | * @param priceStr 价格字符串,格式为'¥30' 32 | * @return 价格 数值类型 33 | */ 34 | public static BigDecimal getPrice(String priceStr) { 35 | if (StringUtils.isBlank(priceStr)) { 36 | return BigDecimal.ZERO; 37 | } 38 | 39 | String formatStr = StringUtils.strip(priceStr, "¥ "); 40 | return BigDecimal.valueOf(NumberUtils.toDouble(formatStr, 0d)); 41 | } 42 | 43 | /** 44 | * 获取价格 45 | * 46 | * @param priceStr 价格字符串,格式为'¥30' 47 | * @return 价格 数值类型字符串 48 | */ 49 | public static String getPriceStr(String priceStr) { 50 | if (StringUtils.isBlank(priceStr)) { 51 | return ""; 52 | } 53 | 54 | return StringUtils.strip(priceStr, "¥ "); 55 | } 56 | 57 | /** 58 | * 比较两个商品价格字符串 59 | * 60 | * @param priceStr1 价格字符串,格式为'¥30' 61 | * @param priceStr2 价格字符串,格式为'¥30' 62 | * @return boolean 63 | */ 64 | public static boolean comparePrice(String priceStr1, String priceStr2) { 65 | BigDecimal price1 = getPrice(priceStr1); 66 | BigDecimal price2 = getPrice(priceStr2); 67 | 68 | return price1.equals(price2); 69 | } 70 | 71 | /** 72 | * 获取 WebElement的描述信息 73 | * 74 | * @param webElement webElement 75 | * @return 字符串描述信息 76 | */ 77 | public static String getWebElementDesc(WebElement webElement) { 78 | StringBuilder sb = new StringBuilder("WebElement["); 79 | try { 80 | sb.append("class="); 81 | sb.append(webElement.getClass().toString()); 82 | sb.append(" ,tag="); 83 | sb.append(webElement.getTagName()); 84 | sb.append(" ,text="); 85 | sb.append(webElement.getText()); 86 | sb.append(" ,location="); 87 | sb.append(webElement.getLocation()); 88 | } catch (Exception ignored) { 89 | } 90 | 91 | sb.append("]"); 92 | 93 | return sb.toString(); 94 | } 95 | 96 | /** 97 | * 线程sleep 98 | * 99 | * @param timeInMills 毫秒值 100 | */ 101 | public static void sleep(long timeInMills) { 102 | try { 103 | Thread.sleep(timeInMills); 104 | } catch (InterruptedException e) { 105 | e.printStackTrace(); 106 | } 107 | } 108 | 109 | /** 110 | * 获取测试方法的测试场景 111 | * 112 | * @param method 测试方法 113 | * @return 测试场景 114 | */ 115 | public static Scene getScene(Method method) { 116 | Scene scene = method.getAnnotation(Scene.class); 117 | if (null != scene) { 118 | return scene; 119 | } 120 | 121 | Class methodClass = method.getDeclaringClass(); 122 | return (Scene) methodClass.getAnnotation(Scene.class); 123 | } 124 | 125 | /** 126 | * 获取方法测试场景名称 127 | * 128 | * @param method 测试方法 129 | * @return 测试场景 130 | */ 131 | public static String getSceneName(Method method) { 132 | Scene scene = getScene(method); 133 | if (null != scene) { 134 | return scene.value(); 135 | } 136 | 137 | return ""; 138 | } 139 | 140 | /** 141 | * 获取测试方法的测试模块 142 | * 143 | * @param method 测试方法 144 | * @return 测试模块 145 | */ 146 | public static Module getModule(Method method) { 147 | Module module = method.getAnnotation(Module.class); 148 | if (null != module) { 149 | return module; 150 | } 151 | 152 | Class methodClass = method.getDeclaringClass(); 153 | return (Module) methodClass.getAnnotation(Module.class); 154 | } 155 | 156 | /** 157 | * 获取方法测试模块名称 158 | * 159 | * @param method 测试方法 160 | * @return 测试模块 161 | */ 162 | public static String getModuleName(Method method) { 163 | Module module = getModule(method); 164 | if (null != module) { 165 | return module.value(); 166 | } 167 | 168 | return ""; 169 | } 170 | 171 | /** 172 | * 获取框架中所有要执行的Test类 173 | * 174 | * @return Test类名 175 | */ 176 | public static String[] getAllTestClasses() { 177 | Reflections reflections = new Reflections("com.qianmi.autotest"); 178 | Set> subTestClassSet = reflections.getSubTypesOf 179 | (BasePageTest.class); 180 | return subTestClassSet.stream() 181 | .filter(classT -> !Modifier.isAbstract(classT.getModifiers())) 182 | .map(Class::getName) 183 | .toArray(String[]::new); 184 | } 185 | 186 | /** 187 | * 获取方法名 188 | * 189 | * @return 所有的方法名 190 | */ 191 | public static List getTestMethods() { 192 | Reflections reflections = new Reflections("com.qianmi.autotest", new MethodAnnotationsScanner()); 193 | Set methods = reflections.getMethodsAnnotatedWith(Test.class); 194 | Stream methodStream = methods.stream(); 195 | methodStream = filterScene(methodStream); 196 | methodStream = filterModule(methodStream); 197 | Stream methodNames = methodStream.map(AutotestUtils::getMethodName); 198 | 199 | return filterMethod(methodNames).collect(Collectors.toList()); 200 | } 201 | 202 | /** 203 | * 过滤启动参数中的场景配置项 204 | * 205 | * @param inputStream 输入流 206 | * @return 输出流 207 | */ 208 | private static Stream filterScene(Stream inputStream) { 209 | String sceneStr = System.getProperty("runScenes"); 210 | if (StringUtils.isBlank(sceneStr)) { 211 | return inputStream; 212 | } 213 | 214 | String[] scenes = sceneStr.split(","); 215 | return inputStream.filter(method -> { 216 | String sceneName = getSceneName(method); 217 | int index = Arrays.binarySearch(scenes, sceneName); 218 | return index > -1; 219 | }); 220 | } 221 | 222 | /** 223 | * 过滤启动参数中的模块配置项 224 | * 225 | * @param inputStream 输入流 226 | * @return 输出流 227 | */ 228 | private static Stream filterModule(Stream inputStream) { 229 | String moduleStr = System.getProperty("runModules"); 230 | if (StringUtils.isBlank(moduleStr)) { 231 | return inputStream; 232 | } 233 | 234 | String[] modules = moduleStr.split(","); 235 | return inputStream.filter(method -> { 236 | String moduleName = getModuleName(method); 237 | int index = Arrays.binarySearch(modules, moduleName); 238 | return index > -1; 239 | }); 240 | } 241 | 242 | /** 243 | * 过滤启动参数中的方法配置项 244 | * 245 | * @param inputStream 输入流 246 | * @return 输出流 247 | */ 248 | private static Stream filterMethod(Stream inputStream) { 249 | String methodStr = System.getProperty("runMethods"); 250 | if (StringUtils.isBlank(methodStr)) { 251 | return inputStream; 252 | } 253 | 254 | String[] methods = methodStr.split(","); 255 | return inputStream.filter(methodName -> Arrays.binarySearch(methods, methodName) > -1); 256 | } 257 | 258 | /** 259 | * 获取方法全名 260 | * 261 | * @param method 方法 262 | * @return 方法名 263 | */ 264 | private static String getMethodName(Method method) { 265 | return method.getDeclaringClass().getName() + "." + method.getName(); 266 | } 267 | 268 | /** 269 | * 获取启动参数中的Devices配置参数 270 | */ 271 | public static void initSystemProperties(String[] args) { 272 | Arrays.asList(args).forEach(arg -> { 273 | String[] templates = arg.split("="); 274 | if (templates.length > 1) { 275 | System.setProperty(templates[0], templates[1]); 276 | } else { 277 | System.setProperty(templates[0], ""); 278 | } 279 | }); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /autotest-base/src/main/java/com/qianmi/autotest/base/common/BasePageTest.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.base.common; 2 | 3 | import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; 4 | import org.testng.annotations.AfterMethod; 5 | import org.testng.annotations.BeforeMethod; 6 | 7 | import java.lang.reflect.Method; 8 | 9 | /** 10 | * Page测试类基类 11 | * Created by liuzhaoming on 2016/12/5. 12 | */ 13 | public abstract class BasePageTest extends AbstractTestNGSpringContextTests { 14 | @BeforeMethod 15 | public void beforeLog(Method method) { 16 | String methodName = method.getName(); 17 | logger.info("**************** " + methodName + " started"); 18 | } 19 | 20 | @AfterMethod 21 | public void afterLog(Method method) { 22 | String methodName = method.getName(); 23 | logger.info("**************** " + methodName + " finished"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /autotest-base/src/main/java/com/qianmi/autotest/base/common/BeanFactory.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.base.common; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.springframework.beans.BeansException; 5 | import org.springframework.context.ApplicationContext; 6 | import org.springframework.context.ApplicationContextAware; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.Collection; 10 | import java.util.Map; 11 | 12 | /** 13 | * Spring Bean 工厂类 14 | * Created by liuzhaoming on 16/9/22. 15 | */ 16 | @Component 17 | public class BeanFactory implements ApplicationContextAware { 18 | private static ApplicationContext applicationContext; 19 | 20 | /** 21 | * 根据Bean名称获取实例 22 | * 23 | * @return bean实例 24 | * @throws BeansException BeansException 25 | */ 26 | @SuppressWarnings("unchecked") 27 | public static T getBean(String name) throws BeansException { 28 | return (T) applicationContext.getBean(name); 29 | } 30 | 31 | /** 32 | * 根据类型获取实例 33 | * 34 | * @param type 类型 35 | * @return bean实例 36 | * @throws BeansException BeansException 37 | */ 38 | public static T getBean(Class type) throws BeansException { 39 | String beanName = StringUtils.uncapitalize(type.getSimpleName()); 40 | T bean = applicationContext.getBean(beanName, type); 41 | if (null != bean) { 42 | return bean; 43 | } 44 | return applicationContext.getBean(type); 45 | } 46 | 47 | /** 48 | * 根据类型获取Bean,可能存在多个事例,默认取第一个 49 | * 50 | * @param type 类型 51 | * @param 泛型 52 | * @return bean实例 53 | * @throws BeansException BeansException 54 | */ 55 | public static T getBeanByType(Class type) throws BeansException { 56 | Map beanMap = applicationContext.getBeansOfType(type); 57 | if (beanMap.values().iterator().hasNext()) { 58 | return beanMap.values().iterator().next(); 59 | } 60 | 61 | return null; 62 | } 63 | 64 | /** 65 | * 根据类型获取Bean列表 66 | * 67 | * @param type 类型 68 | * @param 泛型 69 | * @return bean实例集合 70 | * @throws BeansException BeansException 71 | */ 72 | public static Collection getBeansByType(Class type) throws BeansException { 73 | Map beanMap = applicationContext.getBeansOfType(type); 74 | 75 | return beanMap.values(); 76 | } 77 | 78 | @Override 79 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 80 | BeanFactory.applicationContext = applicationContext; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /autotest-base/src/main/java/com/qianmi/autotest/base/common/Logoutable.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.base.common; 2 | 3 | /** 4 | * 退出当前用户 5 | * Created by liuzhaoming on 16/9/27. 6 | */ 7 | public interface Logoutable { 8 | /** 9 | * 退出当前用户 10 | */ 11 | void logout(); 12 | } 13 | -------------------------------------------------------------------------------- /autotest-base/src/main/java/com/qianmi/autotest/base/common/Module.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.base.common; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * 测试模块, 比如订单、购物车等,模块不划分输入输出数据 10 | * Created by liuzhaoming on 2016/12/6. 11 | */ 12 | @Target({ElementType.TYPE, ElementType.METHOD}) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface Module { 15 | String value(); 16 | } 17 | -------------------------------------------------------------------------------- /autotest-base/src/main/java/com/qianmi/autotest/base/common/Scene.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.base.common; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * 测试场景 10 | * 测试场景可以区分输入输出数据 11 | * Created by liuzhaoming on 2016/11/14. 12 | */ 13 | @Target({ElementType.TYPE, ElementType.METHOD}) 14 | @Retention(RetentionPolicy.RUNTIME) 15 | public @interface Scene { 16 | String value(); 17 | } 18 | -------------------------------------------------------------------------------- /autotest-base/src/main/java/com/qianmi/autotest/base/page/AppLoginPage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.base.page; 2 | 3 | /** 4 | * APP登录页面 5 | * Created by liuzhaoming on 16/9/26. 6 | */ 7 | public interface AppLoginPage { 8 | /** 9 | * 登录并跳转到首页 10 | * 11 | * @param username 用户名 12 | * @param password 密码 13 | */ 14 | void login(String username, String password); 15 | } 16 | -------------------------------------------------------------------------------- /autotest-base/src/main/java/com/qianmi/autotest/base/page/PageObject.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.base.page; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | /** 7 | * Page Object 对象 8 | * Created by liuzhaoming on 2016/12/5. 9 | */ 10 | public interface PageObject { 11 | Logger LOGGER = LoggerFactory.getLogger(PageObject.class); 12 | 13 | /** 14 | * 线程阻塞,供页面渲染 15 | */ 16 | default void sleep(int secTime) { 17 | if (secTime > 0) { 18 | try { 19 | Thread.sleep(secTime * 1000); 20 | } catch (InterruptedException e) { 21 | LOGGER.error("Page sleep fail", e); 22 | } 23 | } 24 | } 25 | 26 | /** 27 | * 线程阻塞,供页面渲染 28 | * 29 | * @param millTime 毫秒时间 30 | */ 31 | default void sleepInMillTime(int millTime) { 32 | if (millTime > 0) { 33 | try { 34 | Thread.sleep(millTime); 35 | } catch (InterruptedException e) { 36 | LOGGER.error("Page sleep fail", e); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /autotest-base/src/main/java/com/qianmi/autotest/base/testng/BaseTestRetryAnalyzer.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.base.testng; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.apache.commons.lang3.math.NumberUtils; 5 | import org.testng.IRetryAnalyzer; 6 | import org.testng.ITestResult; 7 | import org.testng.Reporter; 8 | 9 | /** 10 | * 执行用例失败重试 11 | * Created by liuzhaoming on 16/10/10. 12 | */ 13 | @Slf4j 14 | public abstract class BaseTestRetryAnalyzer implements IRetryAnalyzer { 15 | private int retryCount = 1; 16 | private static int maxRetryCount = NumberUtils.toInt(System.getProperty("maxRetryCount"), 1); 17 | 18 | /** 19 | * Returns true if the test method has to be retried, false otherwise. 20 | * 21 | * @param result The result of the test method that just ran. 22 | * @return true if the test method has to be retried, false otherwise. 23 | */ 24 | @Override 25 | public boolean retry(ITestResult result) { 26 | restart(result); 27 | if (retryCount <= maxRetryCount) { 28 | log.info("Retry for {} on class {} for {} times", result.getName(), result.getTestClass().getName(), 29 | retryCount); 30 | Reporter.setCurrentTestResult(result); 31 | Reporter.log("RunCount=" + (retryCount + 1)); 32 | retryCount++; 33 | 34 | return true; 35 | } 36 | 37 | return false; 38 | } 39 | 40 | /** 41 | * 重启APP 42 | */ 43 | protected abstract void restart(ITestResult result); 44 | } 45 | -------------------------------------------------------------------------------- /autotest-base/src/main/java/com/qianmi/autotest/base/testng/QmDingNotifier.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.base.testng; 2 | 3 | import com.qianmi.autotest.base.common.BeanFactory; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.springframework.web.client.RestTemplate; 7 | import org.testng.ISuite; 8 | import org.testng.ISuiteListener; 9 | import org.testng.ISuiteResult; 10 | 11 | import java.net.InetAddress; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | /** 16 | * 钉钉通知执行结果 17 | * Created by liuzhaoming on 16/10/13. 18 | */ 19 | @Slf4j 20 | public class QmDingNotifier implements ISuiteListener { 21 | 22 | private String testOutputDirectory = System.getProperty("testOutputDirectory"); 23 | 24 | private String deviceName = System.getProperty("deviceName"); 25 | 26 | private String operatorId = System.getProperty("operatorId"); 27 | 28 | private String ip; 29 | 30 | public QmDingNotifier() { 31 | ip = getLocalHostIP(); 32 | } 33 | 34 | /** 35 | * This method is invoked before the SuiteRunner starts. 36 | * 37 | * @param suite suite 38 | */ 39 | @Override 40 | public void onStart(ISuite suite) { 41 | 42 | } 43 | 44 | /** 45 | * This method is invoked after the SuiteRunner has run all 46 | * the test suites. 47 | * 48 | * @param suite suite 49 | */ 50 | @Override 51 | public void onFinish(ISuite suite) { 52 | if (needSendMessage()) { 53 | int skipCount = 0; 54 | int failCount = 0; 55 | for (ISuiteResult suiteResult : suite.getResults().values()) { 56 | skipCount += suiteResult.getTestContext().getSkippedTests().size(); 57 | failCount += suiteResult.getTestContext().getFailedTests().size(); 58 | } 59 | 60 | StringBuilder message = new StringBuilder(); 61 | if (skipCount > 0 || failCount > 0) {//部分用例执行失败 62 | message.append(deviceName); 63 | message.append("自动化测试失败, 测试报告: "); 64 | message.append(getReportUrl()); 65 | } else { 66 | message.append(deviceName); 67 | message.append("自动化测试通过, 测试报告: "); 68 | message.append(getReportUrl()); 69 | } 70 | 71 | sendDingMsg(message.toString()); 72 | } 73 | } 74 | 75 | /** 76 | * 判断是否需要发送订单消息 77 | * 78 | * @return boolean 79 | */ 80 | private boolean needSendMessage() { 81 | return StringUtils.isNotBlank(testOutputDirectory) && StringUtils.isNotBlank(deviceName) && StringUtils 82 | .isNotBlank(ip); 83 | } 84 | 85 | /** 86 | * 获取本地IP地址 87 | * 88 | * @return 本地IP地址 89 | */ 90 | private String getLocalHostIP() { 91 | String ip; 92 | try { 93 | InetAddress addr = InetAddress.getLocalHost(); 94 | ip = addr.getHostAddress(); 95 | } catch (Exception ex) { 96 | ip = ""; 97 | } 98 | 99 | return ip; 100 | } 101 | 102 | /** 103 | * 获取测试报告的URL 104 | * 105 | * @return 测试报告URL 106 | */ 107 | private String getReportUrl() { 108 | String[] temp = testOutputDirectory.split("/"); 109 | String path = temp[temp.length - 1]; 110 | return String.format("http://%s:8080/%s/%s-test-report.html", ip, path, deviceName); 111 | } 112 | 113 | /** 114 | * 发送钉钉通知 115 | * 116 | * @param message 通知内容 117 | */ 118 | @SuppressWarnings("unchecked") 119 | private void sendDingMsg(String message) { 120 | RestTemplate restTemplate = BeanFactory.getBean(RestTemplate.class); 121 | if (null == restTemplate) { 122 | log.error("Cannot find RestTemplate instance"); 123 | return; 124 | } 125 | 126 | if (StringUtils.isBlank(operatorId)) { 127 | log.error("Cannot find staffId"); 128 | return; 129 | } 130 | 131 | try { 132 | String url = "http://ding.com/dingtalk/sendnotice?staffno={staffno}&content={content}"; 133 | restTemplate.getForObject(url, Map.class, new HashMap() { 134 | { 135 | put("staffno", operatorId); 136 | put("content", message); 137 | } 138 | }); 139 | 140 | } catch (Exception e) { 141 | log.error("Fail to send ding message {}", message, e); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /autotest-base/src/main/java/com/qianmi/autotest/base/testng/TestRetryListener.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.base.testng; 2 | 3 | import org.testng.IAnnotationTransformer; 4 | import org.testng.IRetryAnalyzer; 5 | import org.testng.annotations.ITestAnnotation; 6 | 7 | import java.lang.reflect.Constructor; 8 | import java.lang.reflect.Method; 9 | 10 | /** 11 | * 失败重试监听器 12 | * Created by liuzhaoming on 16/10/10. 13 | */ 14 | public class TestRetryListener implements IAnnotationTransformer { 15 | private Class analyzerClass; 16 | 17 | public TestRetryListener(Class analyzerClass) { 18 | this.analyzerClass = analyzerClass; 19 | } 20 | 21 | /** 22 | * This method will be invoked by TestNG to give you a chance 23 | * to modify a TestNG annotation read from your test classes. 24 | * You can change the values you need by calling any of the 25 | * setters on the ITest interface. 26 | *

27 | * Note that only one of the three parameters testClass, 28 | * testConstructor and testMethod will be non-null. 29 | * 30 | * @param annotation The annotation that was read from your 31 | * test class. 32 | * @param testClass If the annotation was found on a class, this 33 | * parameter represents this class (null otherwise). 34 | * @param testConstructor If the annotation was found on a constructor, 35 | * this parameter represents this constructor (null otherwise). 36 | * @param testMethod If the annotation was found on a method, 37 | */ 38 | @Override 39 | public void transform(ITestAnnotation annotation, Class testClass, Constructor testConstructor, Method testMethod) { 40 | IRetryAnalyzer retry = annotation.getRetryAnalyzer(); 41 | if (retry == null) { 42 | annotation.setRetryAnalyzer(analyzerClass); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-app/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | autotest-demo 7 | com.qianmi 8 | 2.0.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | autotest-demo-app 13 | ${project.artifactId} 14 | APP自动化测试Demo 15 | 16 | 17 | 18 | com.qianmi 19 | autotest-app 20 | ${project.version} 21 | 22 | 23 | 24 | 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-maven-plugin 29 | 30 | 31 | 32 | repackage 33 | 34 | 35 | 36 | 37 | com.qianmi.autotest.app.AppTestApplication 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-app/src/main/java/com/qianmi/autotest/demo/app/jd/page/HomePage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.app.jd.page; 2 | 3 | import io.appium.java_client.pagefactory.AndroidFindBy; 4 | import io.appium.java_client.pagefactory.iOSFindBy; 5 | import org.openqa.selenium.WebElement; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * 京东APP首页 10 | * Created by liuzhaoming on 2018/12/25. 11 | */ 12 | @Component 13 | public class HomePage extends NavigatePage { 14 | 15 | @AndroidFindBy(id = "com.jingdong.app.mall:id/a5f") 16 | @iOSFindBy(accessibility = "JDMainPage_input_gray") 17 | private WebElement searchButton; 18 | 19 | /** 20 | * 去搜索页 21 | * 22 | * @return 搜索页 23 | */ 24 | public SearchPage gotoSearchPage() { 25 | wait(searchButton).click(); 26 | return gotoPage(SearchPage.class); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-app/src/main/java/com/qianmi/autotest/demo/app/jd/page/NavigatePage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.app.jd.page; 2 | 3 | import com.qianmi.autotest.app.page.AppBasePage; 4 | import io.appium.java_client.pagefactory.AndroidFindBy; 5 | import io.appium.java_client.pagefactory.iOSFindBy; 6 | import org.openqa.selenium.WebElement; 7 | 8 | /** 9 | * 带有下方导航条的页面 10 | * Created by liuzhaoming on 2018/12/26. 11 | */ 12 | public class NavigatePage extends AppBasePage { 13 | 14 | @AndroidFindBy(accessibility = "首页") 15 | @iOSFindBy(accessibility = "首页") 16 | private WebElement homeButton; 17 | 18 | @AndroidFindBy(accessibility = "分类") 19 | @iOSFindBy(accessibility = "分类") 20 | private WebElement categoryButton; 21 | 22 | @AndroidFindBy(accessibility = "发现") 23 | @iOSFindBy(accessibility = "发现") 24 | private WebElement discoveryButton; 25 | 26 | @AndroidFindBy(accessibility = "购物车") 27 | @iOSFindBy(accessibility = "购物车") 28 | private WebElement shoppingCartButton; 29 | 30 | @AndroidFindBy(accessibility = "我的") 31 | @iOSFindBy(accessibility = "我的") 32 | private WebElement mineButton; 33 | 34 | /** 35 | * 前往购物车页面 36 | * 37 | * @return 购物车页面 38 | */ 39 | public ShoppingCartPage gotoCartPage() { 40 | wait(shoppingCartButton).click(); 41 | return gotoPage(ShoppingCartPage.class); 42 | } 43 | 44 | /** 45 | * 前往首页 46 | * 47 | * @return 首页 48 | */ 49 | public HomePage gotoHomePage() { 50 | wait(homeButton).click(); 51 | return gotoPage(HomePage.class); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-app/src/main/java/com/qianmi/autotest/demo/app/jd/page/ProductPage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.app.jd.page; 2 | 3 | import com.qianmi.autotest.app.page.AppBasePage; 4 | import io.appium.java_client.pagefactory.AndroidFindBy; 5 | import io.appium.java_client.pagefactory.iOSFindBy; 6 | import org.openqa.selenium.WebElement; 7 | import org.springframework.stereotype.Component; 8 | 9 | /** 10 | * 商品页 11 | * Created by liuzhaoming on 2018/12/26. 12 | */ 13 | @Component 14 | public class ProductPage extends AppBasePage { 15 | 16 | @AndroidFindBy(id = "com.jd.lib.productdetail:id/pd_invite_friend") 17 | @iOSFindBy(accessibility = "加入购物车") 18 | private WebElement addCartButton; 19 | 20 | @AndroidFindBy(id = "com.jd.lib.productdetail:id/title_back") 21 | @iOSFindBy(accessibility = "返回") 22 | private WebElement backButton; 23 | 24 | @AndroidFindBy(id = "com.jd.lib.productdetail:id/detail_style_add_2_car") 25 | @iOSFindBy(accessibility = "确定") 26 | private WebElement okButton; 27 | 28 | /** 29 | * 将商品添加到购物车 30 | * 31 | * @return 当前页 32 | */ 33 | public ProductPage addCart() { 34 | wait(addCartButton).click(); 35 | if (isExist(okButton, autotestProperties.getElementLoadTimeInMills())) { 36 | okButton.click(); 37 | } 38 | return this; 39 | } 40 | 41 | /** 42 | * 返回搜索列表页 43 | * 44 | * @return 搜索列表页 45 | */ 46 | public SearchResultPage backToSearchResultPage() { 47 | wait(backButton).click(); 48 | return gotoPage(SearchResultPage.class); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-app/src/main/java/com/qianmi/autotest/demo/app/jd/page/SearchPage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.app.jd.page; 2 | 3 | import com.qianmi.autotest.app.page.AppBasePage; 4 | import io.appium.java_client.pagefactory.AndroidFindBy; 5 | import io.appium.java_client.pagefactory.iOSFindBy; 6 | import org.openqa.selenium.Keys; 7 | import org.openqa.selenium.WebElement; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * 商品搜索内容输入页 12 | * Created by liuzhaoming on 2018/12/25. 13 | */ 14 | @Component 15 | public class SearchPage extends AppBasePage { 16 | 17 | @AndroidFindBy(id = "com.jd.lib.search:id/search_text") 18 | @iOSFindBy(accessibility = "搜索框") 19 | private WebElement searchField; 20 | 21 | @AndroidFindBy(id = "com.jingdong.app.mall:id/ax2") 22 | private WebElement searchButton; 23 | 24 | /** 25 | * 搜索商品 26 | * 27 | * @param keyword 关键词 28 | * @return 搜索结果页 29 | */ 30 | public SearchResultPage search(String keyword) { 31 | wait(searchField).sendKeys(keyword); 32 | if (isAndroidPlatform()) { 33 | wait(searchButton).click(); 34 | } else { 35 | wait(searchField).sendKeys(Keys.ENTER); 36 | } 37 | return gotoPage(SearchResultPage.class); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-app/src/main/java/com/qianmi/autotest/demo/app/jd/page/SearchResultPage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.app.jd.page; 2 | 3 | import com.qianmi.autotest.app.page.AppBasePage; 4 | import io.appium.java_client.pagefactory.AndroidFindBy; 5 | import io.appium.java_client.pagefactory.iOSFindBy; 6 | import org.openqa.selenium.WebElement; 7 | import org.springframework.stereotype.Component; 8 | 9 | /** 10 | * 商品搜索结果列表页 11 | * Created by liuzhaoming on 2018/12/25. 12 | */ 13 | @Component 14 | public class SearchResultPage extends AppBasePage { 15 | 16 | @AndroidFindBy(id = "com.jd.lib.search:id/product_list_item") 17 | @iOSFindBy(accessibility = "com.jd.lib.search:id/product_list_item") 18 | private WebElement productItem; 19 | 20 | @AndroidFindBy(accessibility = "返回") 21 | @iOSFindBy(accessibility = "返回") 22 | private WebElement backButton; 23 | 24 | /** 25 | * 浏览列表首个商品详情 26 | * 27 | * @return 商品详情页 28 | */ 29 | public ProductPage viewFirstProduct() { 30 | wait(productItem).click(); 31 | return gotoPage(ProductPage.class); 32 | } 33 | 34 | /** 35 | * 返回APP首页 36 | * 37 | * @return APP首页 38 | */ 39 | public HomePage backToHomePage() { 40 | wait(backButton).click(); 41 | return gotoPage(HomePage.class); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-app/src/main/java/com/qianmi/autotest/demo/app/jd/page/ShoppingCartPage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.app.jd.page; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | /** 6 | * 购物车页面 7 | * Created by liuzhaoming on 2018/12/26. 8 | */ 9 | @Component 10 | public class ShoppingCartPage extends NavigatePage { 11 | } 12 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-app/src/main/java/com/qianmi/autotest/demo/app/jd/test/JdAppTest.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.app.jd.test; 2 | 3 | import com.qianmi.autotest.app.common.AppPageTest; 4 | import com.qianmi.autotest.demo.app.jd.page.HomePage; 5 | import org.testng.annotations.Test; 6 | 7 | /** 8 | * 京东商城APP测试用例 9 | * Created by liuzhaoming on 2018/12/25. 10 | */ 11 | public class JdAppTest extends AppPageTest { 12 | /** 13 | * 测试未登录情况下的商品搜索和浏览 14 | * 页面跳转流程: 15 | * 1. 进入首页,点击搜索框 16 | * 2. 进入搜索页,输入关键词并输入确定 17 | * 3. 搜索结果列表找到第一条商品并查看商品详情 18 | * 4. 点击返回按钮回到搜索结果页 19 | * 5. 点击首页回到首页 20 | */ 21 | @Test(priority = 1) 22 | public void testSearchWhenLogout() { 23 | String mate9Name = inputData.getProperty("searchProductName"); 24 | 25 | pageFacade.gotoPage(HomePage.class) 26 | .gotoSearchPage() 27 | .search(mate9Name) 28 | .viewFirstProduct() 29 | .backToSearchResultPage() 30 | .backToHomePage(); 31 | } 32 | 33 | /** 34 | * 测试未登录将商品添加到购物车 35 | * * 页面跳转流程: 36 | * 1. 进入首页,点击搜索框 37 | * 2. 进入搜索页,输入关键词并输入确定 38 | * 3. 搜索结果列表找到第一条商品并添加到购物车 39 | * 4. 进入购物车页面 40 | */ 41 | @Test(priority = 2) 42 | public void testAddCart() { 43 | String mate9Name = inputData.getProperty("searchProductName"); 44 | 45 | pageFacade.gotoPage(HomePage.class) 46 | .gotoSearchPage() 47 | .search(mate9Name) 48 | .viewFirstProduct() 49 | .addCart() 50 | .backToSearchResultPage() 51 | .backToHomePage() 52 | .gotoCartPage(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-app/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | #\u65E5\u5FD7\u7EA7\u522B\uFF0C\u5982\u679C\u662F\u5F00\u53D1\u9636\u6BB5\uFF0C\u53EF\u4EE5\u5C06\u65E5\u5FD7\u7EA7\u522B\u6539\u4E3Adebug\uFF0C\u65B9\u4FBF\u5B9A\u4F4D\u95EE\u9898 2 | logging.level.org.springframework=info 3 | logging.level.com.qianmi.autotest=info 4 | logging.level.org.reflections=info 5 | # \u6D4B\u8BD5\u7A0B\u5E8F\u9700\u8981Spring\u626B\u63CF\u7684\u5305\u8DEF\u5F84 6 | autotest.scanPackage= 7 | #\u9875\u9762\u76F8\u5173\u65F6\u95F4\u76F8\u5173\u914D\u7F6E 8 | # wait\u5143\u7D20\u7684\u603B\u65F6\u95F4,\u5355\u4F4D\u4E3A\u6BEB\u79D2,\u8D85\u8FC7\u6B64\u65F6\u95F4\u5C31\u8BA4\u4E3A\u8D85\u65F6 9 | autotest.elementLoadTimeInMills=10000 10 | # wait\u5143\u7D20\u7684\u8F6E\u8BE2\u95F4\u9694,\u5355\u4F4D\u4E3A\u6BEB\u79D2,\u5373wait\u67D0\u4E2A\u5143\u7D20\u65F6,\u6BCF\u9694\u591A\u5C11\u65F6\u95F4\u53D6\u67E5\u770B\u4E00\u6B21\u754C\u9762\u662F\u5426\u5B58\u5728\u6B64\u5143\u7D20 11 | autotest.refreshIntervalInMills=100 12 | # \u65B0\u9875\u9762\u52A0\u8F7D\u540E\u7B49\u5F85\u65F6\u95F4,\u5355\u4F4D\u4E3A\u6BEB\u79D2 13 | autotest.pageLoadTimeInMills=5000 14 | # \u9875\u9762\u5DE6\u53F3\u6ED1\u52A8\u65F6\u95F4,\u5355\u4F4D\u4E3A\u6BEB\u79D2 15 | autotest.swipeTimeInMills=1000 16 | #appium server\u76F8\u5173\u914D\u7F6E 17 | #\u662F\u5426\u8FDE\u63A5Appium server Manager 18 | autotest.appiumServerMngEnable=false 19 | #appium server\u5730\u5740,\u53EA\u6709\u5728\u4E0D\u8FDE\u63A5Appium server Manager\u60C5\u51B5\u4E0B\u751F\u6548 20 | autotest.appiumServerUrl=http://127.0.0.1:4723/wd/hub 21 | #Appium server Manager\u5730\u5740,\u53EF\u4EE5\u914D\u7F6E\u591A\u4E2A,\u4E2D\u95F4\u7528','\u5206\u9694 22 | autotest.appiumServerMngUrl=http://172.21.4.169:5000,http://172.21.4.170:5000,http://172.19.65.167:5000,http://172.19.65.174:5000 23 | #driver \u76F8\u5173\u914D\u7F6E 24 | #\u7A0B\u5E8FAPK\u8DEF\u5F84,\u53EA\u6709\u5728\u4E0D\u8FDE\u63A5Appium server Manager\u60C5\u51B5\u4E0B\u751F\u6548 25 | #autotest.driver.app= 26 | autotest.driver.newCommandTimeout=120 27 | ##\u5B89\u5353\u7684app\u5305\u540D 28 | autotest.driver.appPackage=com.jingdong.app.mall 29 | ## app\u4E3B\u7A0B\u5E8F\u754C\u9762\u7C7B 30 | autotest.driver.appActivity=com.jingdong.app.mall.main.MainActivity 31 | ## \u4F7F\u80FDunicode\u952E\u76D8 32 | autotest.driver.unicodeKeyboard=true 33 | ## \u91CD\u65B0\u8BBE\u7F6E\u952E\u76D8 34 | autotest.driver.resetKeyboard=true 35 | #\u7EC8\u7AEF\u914D\u7F6E 36 | #\u9ED8\u8BA4\u7EC8\u7AEF,\u53EA\u6709\u5728\u4E0D\u6307\u5B9A\u7EC8\u7AEF\u7684\u60C5\u51B5\u4E0B\u751F\u6548 37 | autotest.defaultDevice=oppor15 38 | #\u7EC8\u7AEFIP\u5730\u5740 39 | autotest.device.oppor15.ip=172.19.8.166 40 | #\u7EC8\u7AEF\u7248\u672C\u53F7 41 | autotest.device.oppor15.driver.platformVersion=8.1.0 42 | #\u7EC8\u7AEF\u5E73\u53F0\u540D\u79F0 43 | autotest.device.oppor15.driver.platformName=Android 44 | #\u7EC8\u7AEF\u8BBE\u5907\u540D\u79F0,\u76EE\u524D\u65E0\u7528 45 | autotest.device.oppor15.driver.deviceName=I7BAJ7IRSKSS9PU4 46 | autotest.device.oppor15.driver.automationName=UiAutomator2 47 | autotest.device.iphone6p.driver.platformVersion=12.1 48 | autotest.device.iphone6p.driver.platformName=iOS 49 | autotest.device.iphone6p.driver.deviceName= 50 | autotest.device.iphone6p.driver.udid= 51 | autotest.device.iphone6p.driver.automationName=XCUITest 52 | autotest.device.iphone6p.driver.bundleId=com.360buy.jdmobile 53 | autotest.device.iphone6p.driver.xcodeSigningId=iPhone Developer 54 | autotest.device.iphone6p.driver.xcodeOrgId= -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-app/src/main/resources/data.properties: -------------------------------------------------------------------------------- 1 | input.searchProductName=\u534E\u4E3A Mate20Pro -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-html5/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | autotest-demo 7 | com.qianmi 8 | 2.0.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | autotest-demo-html5 13 | 14 | 15 | 16 | com.qianmi 17 | autotest-html5 18 | ${project.version} 19 | 20 | 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-maven-plugin 27 | 28 | 29 | 30 | repackage 31 | 32 | 33 | 34 | 35 | com.qianmi.autotest.html5.Html5TestApplication 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-html5/src/main/java/com/qianmi/autotest/demo/html5/jd/page/HomePage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.html5.jd.page; 2 | 3 | import org.openqa.selenium.WebElement; 4 | import org.openqa.selenium.support.FindBy; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * 京东H5首页 9 | * Created by liuzhaoming on 2019/1/21. 10 | */ 11 | @Component 12 | public class HomePage extends NavigatePage { 13 | @FindBy(id = "msKeyWord") 14 | private WebElement searchButton; 15 | 16 | /** 17 | * 去搜索页 18 | * 19 | * @return 搜索页 20 | */ 21 | public SearchPage gotoSearchPage() { 22 | wait(searchButton).click(); 23 | return gotoPage(SearchPage.class); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-html5/src/main/java/com/qianmi/autotest/demo/html5/jd/page/NavigatePage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.html5.jd.page; 2 | 3 | import com.qianmi.autotest.html5.page.Html5Page; 4 | import org.openqa.selenium.WebElement; 5 | import org.openqa.selenium.support.FindBy; 6 | 7 | /** 8 | * 带有下方导航条的页面 9 | * Created by liuzhaoming on 2019/1/21. 10 | */ 11 | public class NavigatePage extends Html5Page { 12 | @FindBy(id = "mCommonHome") 13 | private WebElement homePageButton; 14 | 15 | @FindBy(id = "分类") 16 | private WebElement categoryPageButton; 17 | 18 | @FindBy(id = "发现") 19 | private WebElement discoveryPageButton; 20 | 21 | @FindBy(id = "购物车") 22 | private WebElement cartPageButton; 23 | 24 | @FindBy(id = "我的") 25 | private WebElement minePageButton; 26 | 27 | /** 28 | * 前往购物车页面 29 | * 30 | * @return 购物车页面 31 | */ 32 | public ShoppingCartPage gotoCartPage() { 33 | wait(minePageButton).click(); 34 | 35 | return gotoPage(ShoppingCartPage.class); 36 | } 37 | 38 | /** 39 | * 前往首页 40 | * 41 | * @return 首页 42 | */ 43 | public HomePage gotoHomePage() { 44 | wait(homePageButton).click(); 45 | 46 | return gotoPage(HomePage.class); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-html5/src/main/java/com/qianmi/autotest/demo/html5/jd/page/ProductPage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.html5.jd.page; 2 | 3 | import com.qianmi.autotest.html5.page.Html5Page; 4 | import org.openqa.selenium.WebElement; 5 | import org.openqa.selenium.support.FindBy; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * 商品详情页 10 | * Created by liuzhaoming on 2019/1/21. 11 | */ 12 | @Component 13 | public class ProductPage extends Html5Page { 14 | @FindBy(id = "addCart2") 15 | private WebElement addCartButton; 16 | 17 | @FindBy(id = "gotoCart") 18 | private WebElement cartButton; 19 | 20 | @FindBy(id = "m_common_header_goback") 21 | private WebElement backButton; 22 | 23 | @FindBy(id = "popupConfirm") 24 | private WebElement okButton; 25 | 26 | /** 27 | * 将商品添加到购物车 28 | * 29 | * @return 当前页 30 | */ 31 | public ProductPage addCart() { 32 | wait(addCartButton).click(); 33 | if (isExist(okButton, autotestProperties.getElementLoadTimeInMills())) { 34 | okButton.click(); 35 | } 36 | return this; 37 | } 38 | 39 | /** 40 | * 跳转到购物车页面 41 | * 42 | * @return 购物车页面 43 | */ 44 | public ShoppingCartPage gotoCartPage() { 45 | wait(cartButton).click(); 46 | //无实际意义,主要是演示时让观众看得清楚,实际场景可删除 47 | sleep(3); 48 | return gotoPage(ShoppingCartPage.class); 49 | } 50 | 51 | /** 52 | * 返回搜索列表页 53 | * 54 | * @return 搜索列表页 55 | */ 56 | public SearchResultPage backToSearchResultPage() { 57 | wait(backButton).click(); 58 | return gotoPage(SearchResultPage.class); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-html5/src/main/java/com/qianmi/autotest/demo/html5/jd/page/SearchPage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.html5.jd.page; 2 | 3 | import com.qianmi.autotest.html5.page.Html5Page; 4 | import org.openqa.selenium.Keys; 5 | import org.openqa.selenium.WebElement; 6 | import org.openqa.selenium.support.FindBy; 7 | import org.springframework.stereotype.Component; 8 | 9 | /** 10 | * 搜索页 11 | * Created by liuzhaoming on 2019/1/21. 12 | */ 13 | @Component 14 | public class SearchPage extends Html5Page { 15 | @FindBy(id = "msKeyWord") 16 | private WebElement searchField; 17 | 18 | /** 19 | * 搜索商品 20 | * 21 | * @param keyword 关键词 22 | * @return 搜索结果页 23 | */ 24 | public SearchResultPage search(String keyword) { 25 | wait(searchField).sendKeys(keyword); 26 | wait(searchField).sendKeys(Keys.ENTER); 27 | 28 | return gotoPage(SearchResultPage.class); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-html5/src/main/java/com/qianmi/autotest/demo/html5/jd/page/SearchResultPage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.html5.jd.page; 2 | 3 | import com.qianmi.autotest.html5.page.Html5Page; 4 | import org.openqa.selenium.WebElement; 5 | import org.openqa.selenium.support.FindBy; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * 搜索结果页面 10 | * Created by liuzhaoming on 2019/1/21. 11 | */ 12 | @Component 13 | public class SearchResultPage extends Html5Page { 14 | @FindBy(id = "itemList") 15 | private WebElement firstProductItem; 16 | 17 | @FindBy(id = "msCancelBtn") 18 | private WebElement backButton; 19 | 20 | /** 21 | * 浏览列表首个商品详情 22 | * 23 | * @return 商品详情页 24 | */ 25 | public ProductPage viewFirstProduct() { 26 | wait(firstProductItem).click(); 27 | return gotoPage(ProductPage.class); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-html5/src/main/java/com/qianmi/autotest/demo/html5/jd/page/ShoppingCartPage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.html5.jd.page; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | /** 6 | * 购物车页面 7 | * Created by liuzhaoming on 2019/1/21. 8 | */ 9 | @Component 10 | public class ShoppingCartPage extends NavigatePage { 11 | } 12 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-html5/src/main/java/com/qianmi/autotest/demo/html5/jd/test/JdHtml5Test.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.html5.jd.test; 2 | 3 | import com.qianmi.autotest.demo.html5.jd.page.HomePage; 4 | import com.qianmi.autotest.html5.common.Html5PageTest; 5 | import org.testng.annotations.Test; 6 | 7 | /** 8 | * 京东商城Html5测试用例 9 | * Created by liuzhaoming on 2018/12/25. 10 | */ 11 | public class JdHtml5Test extends Html5PageTest { 12 | /** 13 | * 测试未登录情况下的商品搜索和浏览 14 | * 页面跳转流程: 15 | * 1. 进入首页,点击搜索框 16 | * 2. 进入搜索页,输入关键词并输入确定 17 | * 3. 搜索结果列表找到第一条商品并查看商品详情 18 | * 4. 点击返回按钮回到搜索结果页 19 | * 5. 点击首页回到首页 20 | */ 21 | @Test(priority = 1) 22 | public void testSearchWhenLogout() { 23 | String mate9Name = inputData.getProperty("searchProductName"); 24 | 25 | pageFacade.gotoPage(HomePage.class) 26 | .gotoSearchPage() 27 | .search(mate9Name) 28 | .viewFirstProduct() 29 | .backToSearchResultPage(); 30 | } 31 | 32 | /** 33 | * 测试未登录将商品添加到购物车 34 | * * 页面跳转流程: 35 | * 1. 进入首页,点击搜索框 36 | * 2. 进入搜索页,输入关键词并输入确定 37 | * 3. 搜索结果列表找到第一条商品并添加到购物车 38 | * 4. 进入购物车页面 39 | */ 40 | @Test(priority = 2) 41 | public void testAddCart() { 42 | String mate9Name = inputData.getProperty("searchProductName"); 43 | 44 | pageFacade.gotoPage(HomePage.class) 45 | .gotoSearchPage() 46 | .search(mate9Name) 47 | .viewFirstProduct() 48 | .addCart() 49 | .gotoCartPage(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-html5/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | #\u65E5\u5FD7\u7EA7\u522B\uFF0C\u5982\u679C\u662F\u5F00\u53D1\u9636\u6BB5\uFF0C\u53EF\u4EE5\u5C06\u65E5\u5FD7\u7EA7\u522B\u6539\u4E3Adebug\uFF0C\u65B9\u4FBF\u5B9A\u4F4D\u95EE\u9898 2 | logging.level.org.springframework=info 3 | logging.level.com.qianmi.autotest=info 4 | logging.level.org.reflections=info 5 | # \u6D4B\u8BD5\u7A0B\u5E8F\u9700\u8981Spring\u626B\u63CF\u7684\u5305\u8DEF\u5F84 6 | autotest.scanPackage= 7 | #\u9875\u9762\u76F8\u5173\u65F6\u95F4\u76F8\u5173\u914D\u7F6E 8 | # wait\u5143\u7D20\u7684\u603B\u65F6\u95F4,\u5355\u4F4D\u4E3A\u6BEB\u79D2,\u8D85\u8FC7\u6B64\u65F6\u95F4\u5C31\u8BA4\u4E3A\u8D85\u65F6 9 | autotest.elementLoadTimeInMills=10000 10 | # wait\u5143\u7D20\u7684\u8F6E\u8BE2\u95F4\u9694,\u5355\u4F4D\u4E3A\u6BEB\u79D2,\u5373wait\u67D0\u4E2A\u5143\u7D20\u65F6,\u6BCF\u9694\u591A\u5C11\u65F6\u95F4\u53D6\u67E5\u770B\u4E00\u6B21\u754C\u9762\u662F\u5426\u5B58\u5728\u6B64\u5143\u7D20 11 | autotest.refreshIntervalInMills=100 12 | # \u65B0\u9875\u9762\u52A0\u8F7D\u540E\u7B49\u5F85\u65F6\u95F4,\u5355\u4F4D\u4E3A\u6BEB\u79D2 13 | autotest.pageLoadTimeInMills=5000 14 | # \u9875\u9762\u5DE6\u53F3\u6ED1\u52A8\u65F6\u95F4,\u5355\u4F4D\u4E3A\u6BEB\u79D2 15 | autotest.swipeTimeInMills=1000 16 | #appium server\u76F8\u5173\u914D\u7F6E 17 | #\u662F\u5426\u8FDE\u63A5Appium server Manager 18 | autotest.appiumServerMngEnable=false 19 | #appium server\u5730\u5740,\u53EA\u6709\u5728\u4E0D\u8FDE\u63A5Appium server Manager\u60C5\u51B5\u4E0B\u751F\u6548 20 | autotest.appiumServerUrl=http://127.0.0.1:4723/wd/hub 21 | #Appium server Manager\u5730\u5740,\u53EF\u4EE5\u914D\u7F6E\u591A\u4E2A,\u4E2D\u95F4\u7528','\u5206\u9694 22 | autotest.appiumServerMngUrl=http://172.21.4.169:5000,http://172.21.4.170:5000,http://172.19.65.167:5000,http://172.19.65.174:5000 23 | #driver \u76F8\u5173\u914D\u7F6E 24 | #\u7A0B\u5E8FAPK\u8DEF\u5F84,\u53EA\u6709\u5728\u4E0D\u8FDE\u63A5Appium server Manager\u60C5\u51B5\u4E0B\u751F\u6548 25 | #autotest.driver.app= 26 | autotest.driver.newCommandTimeout=120 27 | ## \u4F7F\u80FDunicode\u952E\u76D8 28 | autotest.driver.unicodeKeyboard=true 29 | ## \u91CD\u65B0\u8BBE\u7F6E\u952E\u76D8 30 | autotest.driver.resetKeyboard=true 31 | #\u7EC8\u7AEF\u914D\u7F6E 32 | #\u9ED8\u8BA4\u7EC8\u7AEF,\u53EA\u6709\u5728\u4E0D\u6307\u5B9A\u7EC8\u7AEF\u7684\u60C5\u51B5\u4E0B\u751F\u6548 33 | autotest.defaultDevice=oppor15 34 | #\u7EC8\u7AEFIP\u5730\u5740 35 | autotest.device.oppor15.ip=172.19.8.166 36 | #\u7EC8\u7AEF\u7248\u672C\u53F7 37 | autotest.device.oppor15.driver.platformVersion=8.1.0 38 | #\u7EC8\u7AEF\u5E73\u53F0\u540D\u79F0 39 | autotest.device.oppor15.driver.platformName=Android 40 | autotest.device.htcM8.driver.browserName=Chrome 41 | #\u7EC8\u7AEF\u8BBE\u5907\u540D\u79F0 42 | autotest.device.oppor15.driver.deviceName=I7BAJ7IRSKSS9PU4 43 | autotest.device.oppor15.driver.automationName=UiAutomator 44 | autotest.device.iphone6p.driver.platformVersion=12.1 45 | autotest.device.iphone6p.driver.platformName=iOS 46 | autotest.device.iphone6p.driver.deviceName= 47 | autotest.device.iphone6p.driver.udid= 48 | autotest.device.iphone6p.driver.automationName=XCUITest 49 | autotest.device.iphone6p.driver.bundleId=com.360buy.jdmobile 50 | autotest.device.iphone6p.driver.xcodeSigningId=iPhone Developer 51 | autotest.device.iphone6p.driver.xcodeOrgId= 52 | autotest.device.iphone6p.driver.browserName=Safari 53 | autotest.device.iphone6p.driver.safariAllowPopups=true 54 | autotest.device.iphone6p.driver.nativeWebTap=true 55 | autotest.device.iphone6p.driver.safariInitialUrl=http://m.jd.com/ -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-html5/src/main/resources/data.properties: -------------------------------------------------------------------------------- 1 | input.startUrl=http://m.jd.com/ 2 | input.searchProductName=\u534E\u4E3A Mate20Pro -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-web/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | autotest-demo 7 | com.qianmi 8 | 2.0.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | autotest-demo-web 13 | 14 | 15 | 16 | com.qianmi 17 | autotest-web 18 | ${project.version} 19 | 20 | 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-maven-plugin 27 | 28 | 29 | 30 | repackage 31 | 32 | 33 | 34 | 35 | com.qianmi.autotest.web.WebTestApplication 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-web/src/main/java/com/qianmi/autotest/demo/web/baidu/page/HomePage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.web.baidu.page; 2 | 3 | import com.qianmi.autotest.web.page.WebBasePage; 4 | import org.openqa.selenium.WebElement; 5 | import org.openqa.selenium.support.FindBy; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * Baidu首页 10 | * Created by liuzhaoming on 2019/1/22. 11 | */ 12 | @Component 13 | public class HomePage extends WebBasePage { 14 | @FindBy(id = "kw") 15 | private WebElement inputField; 16 | 17 | @FindBy(id = "su") 18 | private WebElement submitButton; 19 | 20 | public SearchResultPage search(String keyword) { 21 | wait(inputField).sendKeys(keyword); 22 | wait(submitButton).click(); 23 | 24 | return gotoPage(SearchResultPage.class); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-web/src/main/java/com/qianmi/autotest/demo/web/baidu/page/SearchResultPage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.web.baidu.page; 2 | 3 | import com.qianmi.autotest.web.page.WebBasePage; 4 | import org.openqa.selenium.WebElement; 5 | import org.openqa.selenium.support.FindBy; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * Baidu搜索结果页面 10 | * Created by liuzhaoming on 2019/1/22. 11 | */ 12 | @Component 13 | public class SearchResultPage extends WebBasePage { 14 | @FindBy(xpath = "//div[@id=\"content_left\"]/div[@id=\"1\"]") 15 | private WebElement firstItem; 16 | 17 | public boolean checkResult(String keyword) { 18 | return wait(firstItem).getText().contains(keyword); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-web/src/main/java/com/qianmi/autotest/demo/web/baidu/test/BaiduWebTest.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.demo.web.baidu.test; 2 | 3 | import com.qianmi.autotest.demo.web.baidu.page.HomePage; 4 | import com.qianmi.autotest.demo.web.baidu.page.SearchResultPage; 5 | import com.qianmi.autotest.web.common.WebPageTest; 6 | import org.testng.Assert; 7 | import org.testng.annotations.Test; 8 | 9 | /** 10 | * 百度Web页面测试 11 | * Created by liuzhaoming on 2019/1/22. 12 | */ 13 | public class BaiduWebTest extends WebPageTest { 14 | @Test 15 | public void searchBaidu() { 16 | String keyword = "百度"; 17 | SearchResultPage searchResultPage = pageFacade.gotoPage(HomePage.class).search(keyword); 18 | Assert.assertTrue(searchResultPage.checkResult(keyword)); 19 | } 20 | 21 | @Test(priority = 1) 22 | public void searchYouzhan() { 23 | String keyword = "有赞"; 24 | SearchResultPage searchResultPage = pageFacade.gotoPage(HomePage.class).search(keyword); 25 | Assert.assertTrue(searchResultPage.checkResult(keyword)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-web/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | #\u65E5\u5FD7\u7EA7\u522B\uFF0C\u5982\u679C\u662F\u5F00\u53D1\u9636\u6BB5\uFF0C\u53EF\u4EE5\u5C06\u65E5\u5FD7\u7EA7\u522B\u6539\u4E3Adebug\uFF0C\u65B9\u4FBF\u5B9A\u4F4D\u95EE\u9898 2 | logging.level.org.springframework=info 3 | logging.level.com.qianmi.autotest=info 4 | logging.level.org.reflections=info 5 | # \u6D4B\u8BD5\u7A0B\u5E8F\u9700\u8981Spring\u626B\u63CF\u7684\u5305\u8DEF\u5F84 6 | autotest.scanPackage= 7 | #\u9875\u9762\u76F8\u5173\u65F6\u95F4\u76F8\u5173\u914D\u7F6E 8 | # wait\u5143\u7D20\u7684\u603B\u65F6\u95F4,\u5355\u4F4D\u4E3A\u6BEB\u79D2,\u8D85\u8FC7\u6B64\u65F6\u95F4\u5C31\u8BA4\u4E3A\u8D85\u65F6 9 | autotest.elementLoadTimeInMills=10000 10 | # wait\u5143\u7D20\u7684\u8F6E\u8BE2\u95F4\u9694,\u5355\u4F4D\u4E3A\u6BEB\u79D2,\u5373wait\u67D0\u4E2A\u5143\u7D20\u65F6,\u6BCF\u9694\u591A\u5C11\u65F6\u95F4\u53D6\u67E5\u770B\u4E00\u6B21\u754C\u9762\u662F\u5426\u5B58\u5728\u6B64\u5143\u7D20 11 | autotest.refreshIntervalInMills=100 12 | # \u65B0\u9875\u9762\u52A0\u8F7D\u540E\u7B49\u5F85\u65F6\u95F4,\u5355\u4F4D\u4E3A\u6BEB\u79D2 13 | autotest.pageLoadTimeInMills=5000 14 | # \u9875\u9762\u5DE6\u53F3\u6ED1\u52A8\u65F6\u95F4,\u5355\u4F4D\u4E3A\u6BEB\u79D2 15 | autotest.swipeTimeInMills=1000 16 | #\u6D4F\u89C8\u5668\u914D\u7F6E 17 | #\u9ED8\u8BA4\u6D4F\u89C8\u5668,\u53EA\u6709\u5728\u4E0D\u6307\u5B9A\u6D4F\u89C8\u5668\u7684\u60C5\u51B5\u4E0B\u751F\u6548,\u76EE\u524D\u652F\u6301Chrome Firefox IE 18 | autotest.defaultBrowserName=Chrome -------------------------------------------------------------------------------- /autotest-demo/autotest-demo-web/src/main/resources/data.properties: -------------------------------------------------------------------------------- 1 | input.startUrl=http://baidu.com/ 2 | input.searchProductName=\u534E\u4E3A Mate20Pro -------------------------------------------------------------------------------- /autotest-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | autotest-parent 7 | com.qianmi 8 | 2.0.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | autotest-demo 13 | pom 14 | ${project.artifactId} 15 | 自动化测试框架Demo 16 | 17 | autotest-demo-app 18 | autotest-demo-html5 19 | autotest-demo-web 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /autotest-html5/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | autotest-parent 6 | com.qianmi 7 | 2.0.0-SNAPSHOT 8 | ../pom.xml 9 | 10 | 4.0.0 11 | 12 | autotest-html5 13 | 移动端 HTML5 UI 自动化测试框架 14 | 15 | 16 | 17 | com.qianmi 18 | autotest-appium 19 | ${project.version} 20 | 21 | 22 | org.projectlombok 23 | lombok 24 | 25 | 26 | -------------------------------------------------------------------------------- /autotest-html5/src/main/java/com/qianmi/autotest/html5/Html5TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.html5; 2 | 3 | import com.qianmi.autotest.appium.testng.ScreenShotListener; 4 | import com.qianmi.autotest.base.AbstractTestApplication; 5 | import com.qianmi.autotest.base.testng.DefaultReporter; 6 | import com.qianmi.autotest.base.testng.QmDingNotifier; 7 | import com.qianmi.autotest.base.testng.TestRetryListener; 8 | import com.qianmi.autotest.html5.testng.Html5TestRetryAnalyzer; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | import org.testng.ITestNGListener; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * H5测试主程序 17 | * Created by liuzhaoming on 2016/12/5. 18 | */ 19 | @SpringBootApplication(scanBasePackages = {"com.qianmi.autotest", "${autotest.scanPackage:}"}) 20 | public class Html5TestApplication extends AbstractTestApplication { 21 | public static void main(String[] args) { 22 | Html5TestApplication h5Application = new Html5TestApplication(); 23 | h5Application.runTest(args); 24 | } 25 | 26 | /** 27 | * 子类可以重新此方法 28 | * 29 | * @return List 监听器 30 | */ 31 | @Override 32 | protected List getListeners() { 33 | List listeners = new ArrayList<>(); 34 | listeners.add(new DefaultReporter()); 35 | 36 | if (Boolean.valueOf(System.getProperty("screenshot"))) { 37 | listeners.add(new ScreenShotListener()); 38 | } 39 | 40 | if (Boolean.valueOf(System.getProperty("testRetry"))) { 41 | listeners.add(new TestRetryListener(Html5TestRetryAnalyzer.class)); 42 | } 43 | 44 | if (Boolean.valueOf(System.getProperty("dingNotice"))) { 45 | listeners.add(new QmDingNotifier()); 46 | } 47 | return listeners; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /autotest-html5/src/main/java/com/qianmi/autotest/html5/common/Html5PageTest.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.html5.common; 2 | 3 | import com.qianmi.autotest.appium.data.DataProvider; 4 | import com.qianmi.autotest.base.common.AutotestUtils; 5 | import com.qianmi.autotest.base.common.BasePageTest; 6 | import com.qianmi.autotest.base.common.BeanFactory; 7 | import com.qianmi.autotest.base.page.AppLoginPage; 8 | import com.qianmi.autotest.html5.Html5TestApplication; 9 | import com.qianmi.autotest.html5.page.Html5PageFacade; 10 | import io.appium.java_client.AppiumDriver; 11 | import io.appium.java_client.pagefactory.AppiumFieldDecorator; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.apache.commons.lang3.StringUtils; 14 | import org.openqa.selenium.support.PageFactory; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.testng.annotations.BeforeMethod; 18 | 19 | import javax.annotation.Resource; 20 | import java.lang.reflect.Method; 21 | import java.time.Duration; 22 | 23 | /** 24 | * H5 测试类基类 25 | * Created by liuzhaoming on 2016/12/6. 26 | */ 27 | @SuppressWarnings({"unused", "WeakerAccess"}) 28 | @SpringBootTest(classes = Html5TestApplication.class) 29 | @Slf4j 30 | public class Html5PageTest extends BasePageTest { 31 | 32 | @Autowired 33 | protected Html5PageFacade pageFacade; 34 | 35 | @Autowired 36 | protected AppiumDriver appiumDriver; 37 | 38 | @Resource 39 | protected DataProvider inputData; 40 | 41 | @Resource 42 | protected DataProvider outputData; 43 | 44 | @BeforeMethod 45 | public void login(Method method) { 46 | AppLoginPage loginPage = BeanFactory.getBeanByType(AppLoginPage.class); 47 | String sceneName = AutotestUtils.getSceneName(method); 48 | String userName = inputData.getProperty("userName", sceneName); 49 | String password = inputData.getProperty("password", sceneName); 50 | String startUrl = inputData.getProperty("startUrl", sceneName); 51 | 52 | try { 53 | appiumDriver.get(startUrl); 54 | AutotestUtils.sleep(1000); 55 | if (null != loginPage && StringUtils.isNoneBlank(userName)) { 56 | PageFactory.initElements(new AppiumFieldDecorator(appiumDriver, Duration.ofSeconds(1)), loginPage); 57 | loginPage.login(userName, password); 58 | } 59 | } catch (Exception e) { 60 | log.warn("Login has error", e); 61 | AutotestUtils.sleep(1000); 62 | try { 63 | if (null != loginPage && StringUtils.isNoneBlank(userName)) { 64 | loginPage.login(userName, password); 65 | } 66 | } catch (Exception ignored) { 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /autotest-html5/src/main/java/com/qianmi/autotest/html5/common/Html5ResourceLoader.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.html5.common; 2 | 3 | import com.qianmi.autotest.appium.common.AppiumAutotestProperties; 4 | import com.qianmi.autotest.appium.common.AppiumResourceLoader; 5 | import com.qianmi.autotest.appium.data.DataProvider; 6 | import com.qianmi.autotest.base.common.AutotestException; 7 | import io.appium.java_client.AppiumDriver; 8 | import io.appium.java_client.android.AndroidDriver; 9 | import io.appium.java_client.ios.IOSDriver; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.openqa.selenium.remote.DesiredCapabilities; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.stereotype.Component; 15 | 16 | import javax.annotation.Resource; 17 | import java.net.URL; 18 | import java.util.Properties; 19 | 20 | /** 21 | * 系统资源加载 22 | * Created by liuzhaoming on 2016/12/6. 23 | */ 24 | @Component 25 | @Slf4j 26 | @SuppressWarnings("unused") 27 | public class Html5ResourceLoader extends AppiumResourceLoader { 28 | 29 | @Resource(name = "inputData") 30 | private DataProvider inputData; 31 | 32 | /** 33 | * 页面跳转 34 | * 35 | * @param url 要跳转的页面URL 36 | */ 37 | public void gotoPage(String url) { 38 | try { 39 | AppiumDriver appiumDriver = getAppiumDriver(); 40 | 41 | if (null == appiumDriver) { 42 | log.error("Goto page fail because appium server is null"); 43 | return; 44 | } 45 | 46 | appiumDriver.get(url); 47 | } catch (Exception e) { 48 | log.error("Cannot gotoPage {}", url, e); 49 | } 50 | } 51 | 52 | @Bean 53 | public AppiumDriver appiumDriver(AppiumAutotestProperties appiumProperties) { 54 | String appiumServerUrl = initAppiumServer(appiumProperties); 55 | Properties driverConfig = appiumProperties.getDriverConfig(); 56 | DesiredCapabilities capabilities = new DesiredCapabilities(); 57 | for (String name : driverConfig.stringPropertyNames()) { 58 | setCapabilities(name, driverConfig.getProperty(name), capabilities); 59 | } 60 | 61 | 62 | try { 63 | AppiumDriver appiumDriver; 64 | String browserName = driverConfig.getProperty("browserName"); 65 | if (driverConfig.getProperty("platformName", "Android").equalsIgnoreCase("ios")) { 66 | if (StringUtils.isBlank(browserName)) { 67 | setCapabilities("browserName", "Safari", capabilities); 68 | } 69 | 70 | appiumDriver = new IOSDriver(new URL(appiumServerUrl), capabilities); 71 | } else { 72 | if (StringUtils.isBlank(browserName)) { 73 | setCapabilities("browserName", "Chrome", capabilities); 74 | } 75 | 76 | appiumDriver = new AndroidDriver(new URL(appiumServerUrl), capabilities); 77 | String startUrl = inputData.getProperty("startUrl"); 78 | if (StringUtils.isNotBlank(startUrl)) { 79 | appiumDriver.get(startUrl); 80 | } 81 | } 82 | return appiumDriver; 83 | } catch (Exception e) { 84 | log.error("Create appium driver fail {}", appiumServerUrl, e); 85 | throw new AutotestException("Create appium driver fail " + appiumServerUrl); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /autotest-html5/src/main/java/com/qianmi/autotest/html5/page/Html5Page.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.html5.page; 2 | 3 | import com.qianmi.autotest.appium.page.AppiumBasePage; 4 | import com.qianmi.autotest.base.common.AutotestUtils; 5 | import io.appium.java_client.MobileElement; 6 | import io.appium.java_client.TouchAction; 7 | import io.appium.java_client.ios.IOSDriver; 8 | import io.appium.java_client.touch.offset.PointOption; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.apache.commons.lang3.math.NumberUtils; 12 | import org.openqa.selenium.Dimension; 13 | import org.openqa.selenium.Keys; 14 | import org.openqa.selenium.NoSuchElementException; 15 | import org.openqa.selenium.WebElement; 16 | import org.openqa.selenium.support.FindBy; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | 19 | import java.lang.reflect.Field; 20 | import java.util.regex.Matcher; 21 | import java.util.regex.Pattern; 22 | import java.util.stream.IntStream; 23 | 24 | /** 25 | * H5页面基类 26 | * Created by liuzhaoming on 2016/12/6. 27 | */ 28 | @Slf4j 29 | @SuppressWarnings({"unused", "WeakerAcess"}) 30 | public abstract class Html5Page extends AppiumBasePage { 31 | /** 32 | * 安卓控件边界正则表达式 33 | */ 34 | private static final Pattern ANDROID_BOUNDS_PATTERN = Pattern.compile("\\[\\d+,\\d+\\]\\[\\d+,\\d+\\]"); 35 | 36 | /** 37 | * IOS控件边界正则表达式 38 | */ 39 | private static final Pattern IOS_BOUNDS_PATTERN = Pattern.compile("x=\"[\\d]+\" y=\"[\\d]+\" width=\"[\\d]+\" " + 40 | "height=\"[\\d]+\""); 41 | 42 | /** 43 | * 数值正则表达式 44 | */ 45 | private static final Pattern NUMBER_PATTERN = Pattern.compile("[\\d]+"); 46 | 47 | private static final String NATIVE_APP = "NATIVE_APP"; 48 | 49 | /** 50 | * 移动端WebView上方y坐标 51 | */ 52 | private static int WEB_VIEW_TOP = 0; 53 | 54 | /** 55 | * 移动端WebView下方y坐标 56 | */ 57 | private static int WEB_VIEW_BOTTOM = 0; 58 | 59 | @Autowired 60 | private TouchAction touchAction; 61 | 62 | /** 63 | * 在控件上点击键盘Enter键 64 | * 65 | * @param webElement Web元素 66 | */ 67 | protected void pressEnterKey(WebElement webElement) { 68 | if (null != webElement) { 69 | webElement.sendKeys(Keys.ENTER); 70 | } 71 | } 72 | 73 | /** 74 | * Page init time 75 | * 76 | * @return 默认值为1 77 | */ 78 | protected int getPageInitTime() { 79 | return 5; 80 | } 81 | 82 | /** 83 | * 后退到上个页面 84 | * 85 | * @param tClass 页面类 86 | * @param 页面类 87 | * @return 上一个页面 88 | */ 89 | protected T back(Class tClass) { 90 | driver.navigate().back(); 91 | return gotoPage(tClass); 92 | } 93 | 94 | /** 95 | * 前进到下一个页面 96 | * 97 | * @param tClass 页面类 98 | * @param 页面类 99 | * @return 下一个页面 100 | */ 101 | protected T forward(Class tClass) { 102 | driver.navigate().forward(); 103 | 104 | return gotoPage(tClass); 105 | } 106 | 107 | /** 108 | * 页面刷新 109 | * 110 | * @return 当前页面 111 | */ 112 | @SuppressWarnings("unchecked") 113 | protected T refresh() { 114 | driver.navigate().refresh(); 115 | 116 | return (T) gotoPage(this.getClass()); 117 | } 118 | 119 | /** 120 | * 判断Page 元素是否存在, 如果是WebView,元素不存在并不会抛出异常 121 | * 122 | * @param webElement Page元素 123 | * @return boolean 124 | */ 125 | protected boolean isExist(WebElement webElement) { 126 | if (null == webElement) { 127 | return false; 128 | } 129 | 130 | try { 131 | return webElement.isDisplayed(); 132 | } catch (NoSuchElementException e) { 133 | return false; 134 | } 135 | } 136 | 137 | /** 138 | * Web原始click事件,将WebView元素位置转化位native位置进行点击,因为移动端H5存在300ms的点击延时,有些网站捕获了事件 139 | * 140 | * @param webElement webElement 141 | */ 142 | protected void clickByNativePosition(WebElement webElement) { 143 | String originalContext = null; 144 | try { 145 | int screenWebViewWidth = ((Long) driver.executeScript("return window.innerWidth || document.body.clientWidth")).intValue(); 146 | int screenWebViewHeight = ((Long) driver.executeScript("return window.innerHeight || document.body.clientHeight")).intValue(); 147 | 148 | int elementWebViewX = webElement.getLocation().getX(); 149 | int elementWebViewY = webElement.getLocation().getY(); 150 | int height = webElement.getSize().getHeight(); 151 | 152 | // Switching to Native view to use the native supported methods 153 | originalContext = driver.getContext(); 154 | driver.context(NATIVE_APP); 155 | double screenWidth = driver.manage().window().getSize().getWidth(); 156 | // double screenHeight = driver.manage().window().getSize().getHeight(); 157 | double screenHeight = 1980; 158 | 159 | // Service URL bar height 160 | double serviceUrlBar = screenHeight * (0.135135); 161 | double relativeScreenViewHeight = screenHeight - serviceUrlBar; 162 | 163 | // From the WebView coordinates we will be calculating the native view coordinates using the width and 164 | // height 165 | int elementNativeViewX = (int) ((elementWebViewX * screenWidth) / screenWebViewWidth); 166 | double elementNativeViewY = ((elementWebViewY * relativeScreenViewHeight) / screenWebViewHeight); 167 | // Adding 1 just to remove the 0.9999999 error 168 | int relativeElementNativeViewY = (int) (elementNativeViewY + serviceUrlBar); 169 | 170 | if (driver instanceof IOSDriver) { 171 | touchAction.tap(PointOption.point(elementNativeViewX, relativeElementNativeViewY - height / 2)).perform(); 172 | } else { 173 | touchAction.tap(PointOption.point(elementNativeViewX, relativeElementNativeViewY)).perform(); 174 | } 175 | } catch (Exception e) { 176 | log.warn("Click web element has error {}", AutotestUtils.getWebElementDesc(webElement), e); 177 | } finally { 178 | if (null != originalContext) { 179 | driver.context(originalContext); 180 | } 181 | } 182 | } 183 | 184 | /** 185 | * Web按钮click事件, 将WebView元素位置转化位native位置进行点击,因为移动端H5存在300ms的点击延时,有些网站捕获了事件 186 | * 187 | * @param webElement webElement 188 | */ 189 | protected void clickByNativeWebViewPosition(WebElement webElement) { 190 | String originalContext = null; 191 | try { 192 | int screenWebViewWidth = ((Long) driver.executeScript("return window.innerWidth || document.body.clientWidth")).intValue(); 193 | int screenWebViewHeight = ((Long) driver.executeScript("return window.innerHeight || document.body.clientHeight")).intValue(); 194 | 195 | int elementWebViewX = getWebElementCenterX(webElement); 196 | int elementWebViewY = getWebElementCenterY(webElement); 197 | 198 | // Switching to Native view to use the native supported methods 199 | originalContext = driver.getContext(); 200 | driver.context(NATIVE_APP); 201 | 202 | Dimension size = driver.manage().window().getSize(); 203 | double screenWidth = size.getWidth(); 204 | 205 | if (driver instanceof IOSDriver) { 206 | calculateSafariWebViewY(); 207 | } else { 208 | calculateChromeWebViewY(); 209 | } 210 | 211 | int elementNativeViewX = (int) ((elementWebViewX * screenWidth) / screenWebViewWidth); 212 | int webViewNativeHeight = WEB_VIEW_BOTTOM - WEB_VIEW_TOP; 213 | int elementNativeViewY = WEB_VIEW_TOP + webViewNativeHeight * elementWebViewY / 214 | screenWebViewHeight; 215 | touchAction.tap(PointOption.point(elementNativeViewX, elementNativeViewY)).perform(); 216 | } catch (Exception e) { 217 | log.warn("Click web element {} has error ", AutotestUtils.getWebElementDesc(webElement), e); 218 | } finally { 219 | if (null != originalContext) { 220 | driver.context(originalContext); 221 | } 222 | } 223 | } 224 | 225 | /** 226 | * 通过native app 元素点击实现click效果 227 | * 228 | * @param webElement webElement 229 | */ 230 | protected void clickNativeElement(WebElement webElement) { 231 | String originContext = driver.getContext(); 232 | try { 233 | String nativeId = null; 234 | Field[] allFields = getClass().getDeclaredFields(); 235 | for (Field field : allFields) { 236 | field.setAccessible(true); 237 | if (field.get(this) == webElement) { 238 | FindBy annotation = field.getAnnotation(FindBy.class); 239 | nativeId = annotation.id(); 240 | if (StringUtils.isBlank(nativeId)) { 241 | String xpath = annotation.xpath(); 242 | String[] temps = xpath.replaceAll("[\\]\"]", "").split("\\["); 243 | temps = temps[temps.length - 1].split("="); 244 | nativeId = temps[temps.length - 1]; 245 | } 246 | break; 247 | } 248 | } 249 | if (StringUtils.isBlank(nativeId)) { 250 | log.info("Cannot find native Id {}", nativeId); 251 | return; 252 | } 253 | 254 | if (!originContext.equals(NATIVE_APP)) { 255 | driver.context(NATIVE_APP); 256 | } 257 | 258 | driver.findElementById(nativeId).click(); 259 | } catch (Exception e) { 260 | log.warn("nativeClick fail ", e); 261 | } finally { 262 | if (!originContext.equals(NATIVE_APP)) { 263 | driver.context(originContext); 264 | } 265 | } 266 | } 267 | 268 | /** 269 | * WebView 滚动 270 | * 271 | * @param during 毫秒时间 272 | */ 273 | protected void webSwipeUp(int during) { 274 | int screenWebViewHeight = ((Long) driver.executeScript("return window.innerHeight || document.body" + 275 | ".clientHeight")).intValue(); 276 | driver.executeScript(String.format("window.scrollBy(0, %d)", screenWebViewHeight / 2)); 277 | 278 | sleepInMillTime(during); 279 | } 280 | 281 | /** 282 | * WebView 滚动 283 | * 284 | * @param during 毫秒时间 285 | */ 286 | protected void webSwipeDown(int during) { 287 | int screenWebViewHeight = ((Long) driver.executeScript("return window.innerHeight || document.body" + 288 | ".clientHeight")).intValue(); 289 | driver.executeScript(String.format("window.scrollBy(0, -%d)", screenWebViewHeight / 2)); 290 | 291 | sleepInMillTime(during); 292 | } 293 | 294 | /** 295 | * 通过屏幕物理位置向左滑动 296 | * 297 | * @param webElement 元素 298 | * @param during 滑动时间 299 | */ 300 | protected void swipeLeft(WebElement webElement, int during) { 301 | swipeLeft(webElement, during, 1); 302 | } 303 | 304 | /** 305 | * 通过屏幕物理位置向左滑动多次 306 | * 307 | * @param webElement 元素 308 | * @param duringInMills 滑动时间(ms) 309 | * @param times 滑动次数 310 | */ 311 | protected void swipeLeft(WebElement webElement, int duringInMills, int times) { 312 | String originalContext = null; 313 | try { 314 | int screenWebViewWidth = ((Long) driver.executeScript("return window.innerWidth || document.body.clientWidth")).intValue(); 315 | int screenWebViewHeight = ((Long) driver.executeScript("return window.innerHeight || document.body.clientHeight")).intValue(); 316 | int elementWebViewX = getWebElementCenterX(webElement); 317 | int elementWebViewY = getWebElementCenterY(webElement); 318 | int elementWebWidth = webElement.getSize().getWidth(); 319 | 320 | // Switching to Native view to use the native supported methods 321 | originalContext = driver.getContext(); 322 | driver.context(NATIVE_APP); 323 | 324 | Dimension size = driver.manage().window().getSize(); 325 | double screenWidth = size.getWidth(); 326 | 327 | if (driver instanceof IOSDriver) { 328 | calculateSafariWebViewY(); 329 | } else { 330 | calculateChromeWebViewY(); 331 | } 332 | 333 | int elementNativeViewX = (int) ((elementWebViewX * screenWidth) / screenWebViewWidth); 334 | int webViewNativeHeight = WEB_VIEW_BOTTOM - WEB_VIEW_TOP; 335 | int elementNativeViewY = WEB_VIEW_TOP + webViewNativeHeight * elementWebViewY / screenWebViewHeight; 336 | int elementNativeWidth = (int) (elementWebWidth * screenWidth / screenWebViewWidth); 337 | 338 | PointOption startPoint = PointOption.point(elementNativeViewX + elementNativeWidth / 3, elementNativeViewY); 339 | PointOption finishPoint = PointOption.point(elementNativeViewX - elementNativeWidth / 3, elementNativeViewY); 340 | IntStream.range(0, times) 341 | .forEach(i -> swipe(startPoint, finishPoint, duringInMills)); 342 | 343 | } catch (Exception e) { 344 | log.warn("Click web element has error ", e); 345 | } finally { 346 | if (null != originalContext) { 347 | driver.context(originalContext); 348 | } 349 | } 350 | } 351 | 352 | /** 353 | * 通过屏幕物理位置向右滑动 354 | * 355 | * @param webElement 元素 356 | * @param duringInMills 滑动时间(ms) 357 | */ 358 | protected void swipeRight(WebElement webElement, int duringInMills) { 359 | swipeRight(webElement, duringInMills); 360 | } 361 | 362 | /** 363 | * 通过屏幕物理位置向右滑动多次 364 | * 365 | * @param webElement 元素 366 | * @param duringInMills 滑动时间(ms) 367 | * @param times 滑动次数 368 | */ 369 | protected void swipeRight(WebElement webElement, int duringInMills, int times) { 370 | String originalContext = null; 371 | try { 372 | int screenWebViewWidth = ((Long) driver.executeScript("return window.innerWidth || document.body.clientWidth")).intValue(); 373 | int screenWebViewHeight = ((Long) driver.executeScript("return window.innerHeight || document.body.clientHeight")).intValue(); 374 | int elementWebViewX = getWebElementCenterX(webElement); 375 | int elementWebViewY = getWebElementCenterY(webElement); 376 | int elementWebWidth = webElement.getSize().getWidth(); 377 | 378 | // Switching to Native view to use the native supported methods 379 | originalContext = driver.getContext(); 380 | driver.context(NATIVE_APP); 381 | 382 | Dimension size = driver.manage().window().getSize(); 383 | double screenWidth = size.getWidth(); 384 | 385 | if (driver instanceof IOSDriver) { 386 | calculateSafariWebViewY(); 387 | } else { 388 | calculateChromeWebViewY(); 389 | } 390 | 391 | int elementNativeViewX = (int) ((elementWebViewX * screenWidth) / screenWebViewWidth); 392 | int webViewNativeHeight = WEB_VIEW_BOTTOM - WEB_VIEW_TOP; 393 | int elementNativeViewY = WEB_VIEW_TOP + webViewNativeHeight * elementWebViewY / screenWebViewHeight; 394 | int elementNativeWidth = (int) (elementWebWidth * screenWidth / screenWebViewWidth); 395 | PointOption startPoint = PointOption.point(elementNativeViewX - elementNativeWidth / 3, elementNativeViewY); 396 | PointOption finishPoint = PointOption.point(elementNativeViewX + elementNativeWidth / 3, elementNativeViewY); 397 | 398 | IntStream.range(0, times).forEach(i -> swipe(startPoint, finishPoint, duringInMills)); 399 | } catch (Exception e) { 400 | log.warn("Click web element has error ", e); 401 | } finally { 402 | if (null != originalContext) { 403 | driver.context(originalContext); 404 | } 405 | } 406 | } 407 | 408 | /** 409 | * This Method for swipe up 410 | * 411 | * @param duringInMills 滑动速度 等待多少毫秒 412 | */ 413 | protected void swipeUp(int duringInMills) { 414 | String originContext = driver.getContext(); 415 | if (!originContext.equals(NATIVE_APP)) { 416 | driver.context(NATIVE_APP); 417 | } 418 | 419 | int width = driver.manage().window().getSize().width; 420 | int height = driver.manage().window().getSize().height; 421 | swipe(width / 2, height * 3 / 4, width / 2, height / 4, duringInMills); 422 | 423 | if (!originContext.equals(NATIVE_APP)) { 424 | driver.context(originContext); 425 | } 426 | } 427 | 428 | /** 429 | * This Method for swipe down 430 | * 431 | * @param duringInMills 滑动速度 等待多少毫秒 432 | */ 433 | protected void swipeDown(int duringInMills) { 434 | String originContext = driver.getContext(); 435 | if (!originContext.equals(NATIVE_APP)) { 436 | driver.context(NATIVE_APP); 437 | } 438 | 439 | int width = driver.manage().window().getSize().width; 440 | int height = driver.manage().window().getSize().height; 441 | swipe(width / 2, height / 4, width / 2, height * 3 / 4, duringInMills); 442 | 443 | if (!originContext.equals(NATIVE_APP)) { 444 | driver.context(originContext); 445 | } 446 | } 447 | 448 | /** 449 | * This Method for swipe up, 从屏幕中心点滑到顶部 450 | * 451 | * @param duringInMills 滑动速度 等待多少毫秒 452 | */ 453 | protected void swipeUpToTop(int duringInMills) { 454 | String originContext = driver.getContext(); 455 | if (!originContext.equals(NATIVE_APP)) { 456 | driver.context(NATIVE_APP); 457 | } 458 | 459 | int width = driver.manage().window().getSize().width; 460 | int height = driver.manage().window().getSize().height; 461 | swipe(width / 2, height / 2, width / 2, 0, duringInMills); 462 | 463 | if (!originContext.equals(NATIVE_APP)) { 464 | driver.context(originContext); 465 | } 466 | } 467 | 468 | /** 469 | * This Method for swipe down, 从屏幕顶部滑到中心点 470 | * 471 | * @param duringInMills 滑动速度 等待多少毫秒 472 | */ 473 | protected void swipeDownFromTop(int duringInMills) { 474 | String originContext = driver.getContext(); 475 | if (!originContext.equals(NATIVE_APP)) { 476 | driver.context(NATIVE_APP); 477 | } 478 | 479 | int width = driver.manage().window().getSize().width; 480 | int height = driver.manage().window().getSize().height; 481 | swipe(width / 2, 0, width / 2, height / 2, duringInMills); 482 | 483 | if (!originContext.equals(NATIVE_APP)) { 484 | driver.context(originContext); 485 | } 486 | } 487 | 488 | /** 489 | * 获取控件中心点X坐标 490 | * 491 | * @param webElement webElement 492 | * @return 中心点X坐标 493 | */ 494 | private int getWebElementCenterX(WebElement webElement) { 495 | if (webElement instanceof MobileElement) { 496 | return ((MobileElement) webElement).getCenter().getX(); 497 | } else { 498 | int width = webElement.getSize().getWidth(); 499 | return webElement.getLocation().getX() + width / 2; 500 | } 501 | } 502 | 503 | /** 504 | * 获取控件中心点Y坐标 505 | * 506 | * @param webElement webElement 507 | * @return 中心点Y坐标 508 | */ 509 | private int getWebElementCenterY(WebElement webElement) { 510 | if (webElement instanceof MobileElement) { 511 | return ((MobileElement) webElement).getCenter().getY(); 512 | } else { 513 | int height = webElement.getSize().getHeight(); 514 | return webElement.getLocation().getY() + height / 2; 515 | } 516 | } 517 | 518 | /** 519 | * 计算chrome WebView Y 坐标 520 | */ 521 | private void calculateChromeWebViewY() { 522 | if (WEB_VIEW_TOP > 0) { 523 | return; 524 | } 525 | 526 | synchronized (Html5Page.class) { 527 | String originContextName = driver.getContext(); 528 | if (!originContextName.equals(NATIVE_APP)) { 529 | driver.context(NATIVE_APP); 530 | } 531 | 532 | try { 533 | String pageSource = driver.getPageSource(); 534 | String[] temps = pageSource.split(" 0) { 564 | return; 565 | } 566 | 567 | synchronized (Html5Page.class) { 568 | String originContextName = driver.getContext(); 569 | if (!originContextName.equals(NATIVE_APP)) { 570 | driver.context(NATIVE_APP); 571 | } 572 | 573 | try { 574 | String pageSource = driver.getPageSource(); 575 | String[] temps = pageSource.split(" 2 | 4 | 5 | autotest-parent 6 | com.qianmi 7 | 2.0.0-SNAPSHOT 8 | ../pom.xml 9 | 10 | 4.0.0 11 | 12 | autotest-web 13 | ${project.artifactId} 14 | PC 端网页自动化测试框架 15 | 16 | 17 | 18 | org.seleniumhq.selenium 19 | selenium-java 20 | 21 | 22 | com.qianmi 23 | autotest-base 24 | ${project.version} 25 | 26 | 27 | org.projectlombok 28 | lombok 29 | 30 | 31 | commons-io 32 | commons-io 33 | 34 | 35 | -------------------------------------------------------------------------------- /autotest-web/src/main/java/com/qianmi/autotest/web/WebTestApplication.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.web; 2 | 3 | import com.qianmi.autotest.base.AbstractTestApplication; 4 | import com.qianmi.autotest.base.testng.DefaultReporter; 5 | import com.qianmi.autotest.base.testng.QmDingNotifier; 6 | import com.qianmi.autotest.base.testng.TestRetryListener; 7 | import com.qianmi.autotest.web.testng.ScreenShotListener; 8 | import com.qianmi.autotest.web.testng.WebTestRetryAnalyzer; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | import org.testng.ITestNGListener; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * Web自动化测试主程序入口 17 | * Created by liuzhaoming on 2016/12/7. 18 | */ 19 | @SpringBootApplication(scanBasePackages = {"com.qianmi.autotest", "${autotest.scanPackage:}"}) 20 | public class WebTestApplication extends AbstractTestApplication { 21 | public static void main(String[] args) { 22 | WebTestApplication webApplication = new WebTestApplication(); 23 | webApplication.runTest(args); 24 | } 25 | 26 | /** 27 | * 子类可以重新此方法 28 | * 29 | * @return List 监听器 30 | */ 31 | @Override 32 | protected List getListeners() { 33 | List listeners = new ArrayList<>(); 34 | listeners.add(new DefaultReporter()); 35 | 36 | if (Boolean.valueOf(System.getProperty("screenshot"))) { 37 | listeners.add(new ScreenShotListener()); 38 | } 39 | 40 | if (Boolean.valueOf(System.getProperty("testRetry"))) { 41 | listeners.add(new TestRetryListener(WebTestRetryAnalyzer.class)); 42 | } 43 | 44 | if (Boolean.valueOf(System.getProperty("dingNotice"))) { 45 | listeners.add(new QmDingNotifier()); 46 | } 47 | return listeners; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /autotest-web/src/main/java/com/qianmi/autotest/web/common/WebAutotestProperties.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.web.common; 2 | 3 | import com.qianmi.autotest.base.common.AutotestProperties; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.ToString; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.apache.commons.collections4.MapUtils; 9 | 10 | import java.util.Properties; 11 | 12 | /** 13 | * Web 测试程序配置参数 14 | * Created by liuzhaoming on 2016/12/6. 15 | */ 16 | @EqualsAndHashCode(callSuper = true) 17 | @ToString(callSuper = true) 18 | @Data 19 | @Slf4j 20 | public class WebAutotestProperties extends AutotestProperties { 21 | /** 22 | * driver配置属性 23 | */ 24 | protected Properties driver; 25 | 26 | /** 27 | * browser配置属性 28 | */ 29 | protected Properties browser; 30 | 31 | /** 32 | * 默认浏览器名称 33 | */ 34 | protected String defaultBrowserName; 35 | 36 | /** 37 | * 设备配置信息 38 | */ 39 | protected Properties browserConfig; 40 | 41 | /** 42 | * appium driver配置信息 43 | */ 44 | protected Properties driverConfig; 45 | 46 | /** 47 | * 获取当前生效的设备名称 48 | * 49 | * @return 设备名称 50 | */ 51 | public String getActiveBrowserName() { 52 | return System.getProperty("browserName", defaultBrowserName); 53 | } 54 | 55 | /** 56 | * 获取浏览器配置信息 57 | * 58 | * @return 浏览器配置信息 59 | */ 60 | public Properties getBrowserConfig() { 61 | if (MapUtils.isEmpty(browserConfig)) { 62 | synchronized (this) { 63 | if (MapUtils.isEmpty(browserConfig)) { 64 | String browserName = getActiveBrowserName(); 65 | Properties totalBrowserProperties = new Properties(); 66 | String browserNamePrefix = browserName + "."; 67 | log.info("Browser config bean {}", browserNamePrefix); 68 | 69 | browser.stringPropertyNames().stream() 70 | .filter(propertyName -> propertyName.startsWith(browserNamePrefix)) 71 | .forEach(propertyName -> 72 | totalBrowserProperties.setProperty(propertyName.substring(browserNamePrefix.length()), 73 | browser.getProperty(propertyName)) 74 | ); 75 | browserConfig = totalBrowserProperties; 76 | } 77 | } 78 | } 79 | 80 | return browserConfig; 81 | } 82 | 83 | /** 84 | * 获取驱动配置信息 85 | * 86 | * @return 驱动配置信息 87 | */ 88 | public Properties getDriverConfig() { 89 | if (MapUtils.isEmpty(driverConfig)) { 90 | synchronized (this) { 91 | if (MapUtils.isEmpty(driverConfig)) { 92 | String browserName = getActiveBrowserName(); 93 | Properties totalDriverProperties = new Properties(driver); 94 | String browserNamePrefix = browserName + ".driver."; 95 | log.info("Driver config bean {}", browserNamePrefix); 96 | 97 | if (MapUtils.isNotEmpty(browser)) { 98 | browser.stringPropertyNames().stream() 99 | .filter(propertyName -> propertyName.startsWith(browserNamePrefix)) 100 | .forEach(propertyName -> 101 | totalDriverProperties.setProperty(propertyName.substring(browserNamePrefix.length()), 102 | browser.getProperty(propertyName)) 103 | ); 104 | } 105 | driverConfig = totalDriverProperties; 106 | } 107 | } 108 | } 109 | 110 | return driverConfig; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /autotest-web/src/main/java/com/qianmi/autotest/web/common/WebPageTest.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.web.common; 2 | 3 | import com.qianmi.autotest.base.common.AutotestUtils; 4 | import com.qianmi.autotest.base.common.BasePageTest; 5 | import com.qianmi.autotest.base.common.BeanFactory; 6 | import com.qianmi.autotest.base.page.AppLoginPage; 7 | import com.qianmi.autotest.web.WebTestApplication; 8 | import com.qianmi.autotest.web.data.DataProvider; 9 | import com.qianmi.autotest.web.page.WebPageFacade; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.openqa.selenium.WebDriver; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.testng.annotations.BeforeMethod; 15 | 16 | import javax.annotation.Resource; 17 | import java.lang.reflect.Method; 18 | 19 | /** 20 | * Web网页测试类基类 21 | * Created by liuzhaoming on 2016/12/6. 22 | */ 23 | @SuppressWarnings({"WeakerAccess", "unused"}) 24 | @Slf4j 25 | @SpringBootTest(classes = WebTestApplication.class) 26 | public class WebPageTest extends BasePageTest { 27 | @Autowired 28 | protected WebPageFacade pageFacade; 29 | 30 | @Autowired 31 | protected WebDriver webDriver; 32 | 33 | @Resource 34 | protected DataProvider inputData; 35 | 36 | @Resource 37 | protected DataProvider outputData; 38 | 39 | @BeforeMethod 40 | public void login(Method method) { 41 | String sceneName = AutotestUtils.getSceneName(method); 42 | String userName = inputData.getProperty("userName", sceneName); 43 | String password = inputData.getProperty("password", sceneName); 44 | String startUrl = inputData.getProperty("startUrl", sceneName); 45 | try { 46 | log.info("Begin login startUrl={} userName={}", startUrl, userName); 47 | webDriver.get(startUrl); 48 | AutotestUtils.sleep(1000); 49 | AppLoginPage loginPage = BeanFactory.getBean(AppLoginPage.class); 50 | if (null != loginPage) { 51 | loginPage.login(userName, password); 52 | } 53 | } catch (Exception e) { 54 | log.warn("Login has error", e); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /autotest-web/src/main/java/com/qianmi/autotest/web/common/WebResourceLoader.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.web.common; 2 | 3 | import com.qianmi.autotest.base.common.AutotestException; 4 | import com.qianmi.autotest.base.common.BeanFactory; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.openqa.selenium.WebDriver; 7 | import org.openqa.selenium.chrome.ChromeDriver; 8 | import org.openqa.selenium.chrome.ChromeOptions; 9 | import org.openqa.selenium.edge.EdgeDriver; 10 | import org.openqa.selenium.edge.EdgeOptions; 11 | import org.openqa.selenium.firefox.FirefoxDriver; 12 | import org.openqa.selenium.firefox.FirefoxOptions; 13 | import org.openqa.selenium.ie.InternetExplorerDriver; 14 | import org.openqa.selenium.ie.InternetExplorerOptions; 15 | import org.openqa.selenium.remote.DesiredCapabilities; 16 | import org.openqa.selenium.safari.SafariDriver; 17 | import org.openqa.selenium.safari.SafariOptions; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.boot.context.properties.ConfigurationProperties; 20 | import org.springframework.context.annotation.Bean; 21 | import org.springframework.stereotype.Component; 22 | 23 | import javax.annotation.PostConstruct; 24 | import java.util.Properties; 25 | 26 | /** 27 | * 系统资源加载 28 | * Created by liuzhaoming on 2016/12/6. 29 | */ 30 | @SuppressWarnings("WeakerAccess") 31 | @Slf4j 32 | @Component 33 | public class WebResourceLoader { 34 | 35 | @Autowired 36 | private WebAutotestProperties webAutotestProperties; 37 | 38 | @Bean 39 | @ConfigurationProperties("autotest") 40 | public WebAutotestProperties webAutotestProperties() { 41 | return new WebAutotestProperties(); 42 | } 43 | 44 | @PostConstruct 45 | public void printStartLog() { 46 | log.warn("~~~~~||~~~~~ Start test by browserName : {}", webAutotestProperties.getActiveBrowserName()); 47 | } 48 | 49 | /** 50 | * 页面跳转 51 | * 52 | * @param url 要跳转的页面URL 53 | */ 54 | public void gotoPage(String url) { 55 | try { 56 | WebDriver webDriver = getWebDriver(); 57 | 58 | if (null == webDriver) { 59 | log.error("Goto page fail because web server is null"); 60 | return; 61 | } 62 | 63 | webDriver.get(url); 64 | } catch (Exception e) { 65 | log.error("Cannot gotoPage {}", url, e); 66 | } 67 | } 68 | 69 | @Bean 70 | public WebDriver webDriver() { 71 | DesiredCapabilities capabilities = new DesiredCapabilities(); 72 | Properties driverConfig = webAutotestProperties.getDriverConfig(); 73 | for (String name : driverConfig.stringPropertyNames()) { 74 | setCapabilities(name, driverConfig.getProperty(name), capabilities); 75 | } 76 | 77 | try { 78 | WebDriver webDriver; 79 | String browserName = webAutotestProperties.getActiveBrowserName(); 80 | if (browserName.equalsIgnoreCase("Chrome")) { 81 | ChromeOptions options = new ChromeOptions().merge(capabilities); 82 | webDriver = new ChromeDriver(options); 83 | } else if (browserName.equalsIgnoreCase("Firefox")) { 84 | FirefoxOptions options = new FirefoxOptions().merge(capabilities); 85 | webDriver = new FirefoxDriver(options); 86 | } else if (browserName.equalsIgnoreCase("IE")) { 87 | InternetExplorerOptions options = new InternetExplorerOptions().merge(capabilities); 88 | webDriver = new InternetExplorerDriver(options); 89 | } else if (browserName.equalsIgnoreCase("Safari")) { 90 | SafariOptions options = new SafariOptions().merge(capabilities); 91 | webDriver = new SafariDriver(options); 92 | } else if (browserName.equalsIgnoreCase("Edge")) { 93 | EdgeOptions options = new EdgeOptions().merge(capabilities); 94 | webDriver = new EdgeDriver(options); 95 | } else { 96 | log.error("Create web driver fail, because the browserName is null or default.browserName is null"); 97 | throw new AutotestException(); 98 | } 99 | 100 | // webDriver.manage().window().maximize(); 101 | return webDriver; 102 | } catch (Exception e) { 103 | log.error("Create web driver fail", e); 104 | throw new AutotestException("Create appium driver fail "); 105 | } 106 | } 107 | 108 | 109 | /** 110 | * 获取Selenium 驱动 111 | * 112 | * @return Selenium驱动 113 | */ 114 | protected WebDriver getWebDriver() { 115 | return BeanFactory.getBeanByType(WebDriver.class); 116 | } 117 | 118 | /** 119 | * 设置配置属性 120 | * 121 | * @param name 属性名称 122 | * @param value 属性值 123 | * @param capabilities 配置容器 124 | */ 125 | protected void setCapabilities(String name, String value, DesiredCapabilities capabilities) { 126 | if (null == value) { 127 | return; 128 | } 129 | 130 | capabilities.setCapability(name, value); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /autotest-web/src/main/java/com/qianmi/autotest/web/data/DataInitialization.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.web.data; 2 | 3 | import com.qianmi.autotest.web.common.WebAutotestProperties; 4 | import lombok.Data; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.PropertySource; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.util.Properties; 13 | 14 | /** 15 | * 测试数据 16 | * Created by liuzhaoming on 16/9/27. 17 | */ 18 | 19 | @Data 20 | @Slf4j 21 | @Component 22 | @ConfigurationProperties 23 | @PropertySource("classpath:data.properties") 24 | public class DataInitialization { 25 | 26 | private Properties input; 27 | 28 | private Properties output; 29 | 30 | private Properties browser; 31 | 32 | @Autowired 33 | private WebAutotestProperties webAutotestProperties; 34 | 35 | @Bean 36 | public DataProvider inputData() { 37 | Properties totalInputProperties = new Properties(); 38 | if (null != input) { 39 | totalInputProperties.putAll(input); 40 | } 41 | 42 | String browserNamePrefix = webAutotestProperties.getActiveBrowserName() + ".input."; 43 | log.info("Input data bean browser name prefix is {}", browserNamePrefix); 44 | if (null != browser) { 45 | browser.stringPropertyNames().stream() 46 | .filter(propertyName -> propertyName.startsWith(browserNamePrefix)) 47 | .forEach(propertyName -> 48 | totalInputProperties.setProperty(propertyName.substring(browserNamePrefix.length()), 49 | browser.getProperty(propertyName)) 50 | ); 51 | } 52 | return new DataProvider(totalInputProperties); 53 | } 54 | 55 | @Bean 56 | public DataProvider outputData() { 57 | Properties totalOutputProperties = new Properties(); 58 | if (null != output) { 59 | totalOutputProperties.putAll(output); 60 | } 61 | 62 | String browserNamePrefix = webAutotestProperties.getActiveBrowserName() + ".output."; 63 | log.info("Output data bean browser name prefix is {}", browserNamePrefix); 64 | if (null != browser) { 65 | browser.stringPropertyNames().stream() 66 | .filter(propertyName -> propertyName.startsWith(browserNamePrefix)) 67 | .forEach(propertyName -> 68 | totalOutputProperties.setProperty(propertyName.substring(browserNamePrefix.length()), 69 | browser.getProperty(propertyName)) 70 | ); 71 | } 72 | return new DataProvider(totalOutputProperties); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /autotest-web/src/main/java/com/qianmi/autotest/web/data/DataProvider.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.web.data; 2 | 3 | import java.io.Serializable; 4 | import java.util.Properties; 5 | 6 | /** 7 | * 测试数据封装类 8 | * Created by liuzhaoming on 2016/11/14. 9 | */ 10 | @SuppressWarnings("unused") 11 | public class DataProvider implements Serializable { 12 | private Properties originData; 13 | 14 | public DataProvider(Properties originData) { 15 | this.originData = originData; 16 | } 17 | 18 | public String getProperty(String name) { 19 | return originData.getProperty(name); 20 | } 21 | 22 | public String getProperty(String name, String sceneName) { 23 | String propertyName = String.format("scene.%s.%s", sceneName, name); 24 | if (originData.containsKey(propertyName)) { 25 | return originData.getProperty(propertyName); 26 | } 27 | 28 | return originData.getProperty(name); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /autotest-web/src/main/java/com/qianmi/autotest/web/page/WebBasePage.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.web.page; 2 | 3 | import com.qianmi.autotest.base.common.BeanFactory; 4 | import com.qianmi.autotest.base.page.PageObject; 5 | import com.qianmi.autotest.web.common.WebAutotestProperties; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.openqa.selenium.*; 8 | import org.openqa.selenium.interactions.internal.Coordinates; 9 | import org.openqa.selenium.interactions.internal.Locatable; 10 | import org.openqa.selenium.remote.RemoteWebDriver; 11 | import org.openqa.selenium.support.PageFactory; 12 | import org.openqa.selenium.support.ui.ExpectedCondition; 13 | import org.openqa.selenium.support.ui.WebDriverWait; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | 16 | import javax.annotation.Nullable; 17 | 18 | /** 19 | * Web测试基类 20 | * Created by liuzhaoming on 16/9/23. 21 | */ 22 | @SuppressWarnings({"unused", "WeakerAccess"}) 23 | @Slf4j 24 | public abstract class WebBasePage implements PageObject { 25 | @Autowired 26 | protected RemoteWebDriver driver; 27 | 28 | @Autowired 29 | private WebAutotestProperties webProperties; 30 | 31 | 32 | /** 33 | * 页面跳转 34 | * 35 | * @param tClass Page Class 36 | * @param 泛型 37 | * @return Page页面 38 | */ 39 | public T gotoPage(Class tClass) { 40 | log.info("Begin goto page " + tClass.getName()); 41 | T page = BeanFactory.getBean(tClass); 42 | PageFactory.initElements(driver, page); 43 | page.afterConstruct(); 44 | return page; 45 | } 46 | 47 | /** 48 | * 初始化页面 49 | * 50 | * @param page page 51 | * @param 泛型 52 | * @return 页面 53 | */ 54 | protected T initPage(T page) { 55 | PageFactory.initElements(driver, page); 56 | page.afterConstruct(); 57 | return page; 58 | } 59 | 60 | /** 61 | * 线程阻塞,供页面渲染 62 | */ 63 | protected void sleep() { 64 | sleepInMillTime(webProperties.getPageLoadTimeInMills()); 65 | } 66 | 67 | /** 68 | * Page元素构造完成后需要执行的操作 69 | */ 70 | protected void afterConstruct() { 71 | 72 | } 73 | 74 | /** 75 | * 等待Page加载某个元素或者查询条件完成 76 | * 77 | * @param id 元素id 78 | * @return WebElement 79 | */ 80 | protected WebElement wait(String id) { 81 | return new WebDriverWait(driver, webProperties.getElementLoadTimeInMills() / 1000, webProperties.getRefreshIntervalInMills()) 82 | .until((ExpectedCondition) driver -> { 83 | try { 84 | return driver.findElement(By.id(id)); 85 | } catch (Exception e) { 86 | return null; 87 | } 88 | }); 89 | } 90 | 91 | /** 92 | * 等待Page加载某个元素 93 | * 94 | * @param webElement 页面元素 95 | * @return WebElement 96 | */ 97 | protected WebElement wait(WebElement webElement) { 98 | return wait(webElement, webProperties.getElementLoadTimeInMills()); 99 | } 100 | 101 | /** 102 | * 等待Page加载某个元素 103 | * 104 | * @param webElement 页面元素 105 | * @param timeOutInMills 最大等待时间,毫秒值 106 | * @return WebElement 107 | */ 108 | protected WebElement wait(WebElement webElement, int timeOutInMills) { 109 | new WebDriverWait(driver, timeOutInMills / 1000, webProperties.getRefreshIntervalInMills()) 110 | .until(new ExpectedCondition() { 111 | @Nullable 112 | @Override 113 | public WebElement apply(@Nullable WebDriver driver) { 114 | if (isExist(webElement)) { 115 | return webElement; 116 | } else { 117 | return null; 118 | } 119 | } 120 | }); 121 | 122 | return webElement; 123 | } 124 | 125 | /** 126 | * 判断Page 元素是否存在 127 | * 128 | * @param webElement Page元素 129 | * @return boolean 130 | */ 131 | protected boolean isExist(WebElement webElement) { 132 | if (null == webElement) { 133 | return false; 134 | } 135 | 136 | try { 137 | webElement.isDisplayed(); 138 | return true; 139 | } catch (NoSuchElementException e) { 140 | return false; 141 | } 142 | } 143 | 144 | 145 | /** 146 | * 判断Page 元素是否存在 147 | * 148 | * @param webElement Page元素 149 | * @param timeOutInMills 超时时间, 毫秒值 150 | * @return boolean 151 | */ 152 | protected boolean isExist(WebElement webElement, int timeOutInMills) { 153 | if (null == webElement) { 154 | return false; 155 | } 156 | 157 | try { 158 | WebElement element = new WebDriverWait(driver, timeOutInMills, webProperties.getRefreshIntervalInMills()) 159 | .until((ExpectedCondition) driver -> { 160 | if (isExist(webElement)) { 161 | return webElement; 162 | } else { 163 | return null; 164 | } 165 | }); 166 | 167 | return null != element; 168 | } catch (Exception e) { 169 | return false; 170 | } 171 | } 172 | 173 | /** 174 | * 网页滚动到指定的位置 175 | * 176 | * @param xCoordinate x坐标 177 | * @param yCoordinate y坐标 178 | */ 179 | protected void scrollTo(int xCoordinate, int yCoordinate) { 180 | String script = String.format("window.scrollBy(%d, %d)", xCoordinate, yCoordinate); 181 | driver.executeScript("window.scrollBy(0, 700)"); 182 | } 183 | 184 | /** 185 | * 网页滚动到指定的元素位置 186 | * 187 | * @param webElement 元素 188 | */ 189 | protected void scrollTo(WebElement webElement) { 190 | Coordinates coordinates = ((Locatable) webElement).getCoordinates(); 191 | coordinates.inViewPort(); 192 | } 193 | 194 | /** 195 | * 在控件上点击键盘Enter键 196 | * 197 | * @param webElement Web元素 198 | */ 199 | protected void pressEnterKey(WebElement webElement) { 200 | if (null != webElement) { 201 | webElement.sendKeys(Keys.ENTER); 202 | } 203 | } 204 | 205 | /** 206 | * 后退到上个页面 207 | * 208 | * @param tClass 页面类 209 | * @param 页面类 210 | * @return 上一个页面 211 | */ 212 | protected T back(Class tClass) { 213 | driver.navigate().back(); 214 | return gotoPage(tClass); 215 | } 216 | 217 | /** 218 | * 前进到下一个页面 219 | * 220 | * @param tClass 页面类 221 | * @param 页面类 222 | * @return 下一个页面 223 | */ 224 | protected T forward(Class tClass) { 225 | driver.navigate().forward(); 226 | return gotoPage(tClass); 227 | } 228 | 229 | /** 230 | * 页面刷新 231 | * 232 | * @return 当前页面 233 | */ 234 | @SuppressWarnings("unchecked") 235 | protected T refresh() { 236 | driver.navigate().refresh(); 237 | return (T) gotoPage(this.getClass()); 238 | } 239 | 240 | /** 241 | * 向上滚动 242 | * 243 | * @param duringInMills 毫秒时间 244 | */ 245 | protected void swipeUp(int duringInMills) { 246 | JavascriptExecutor jsExecutor = driver; 247 | int screenWebViewHeight = ((Long) jsExecutor.executeScript("return window.innerHeight || document.body.clientHeight")).intValue(); 248 | jsExecutor.executeScript(String.format("window.scrollBy(0, %d)", screenWebViewHeight / 2)); 249 | 250 | sleepInMillTime(duringInMills); 251 | } 252 | 253 | /** 254 | * 向下滚动 255 | * 256 | * @param duringInMills 毫秒时间 257 | */ 258 | protected void swipeDown(int duringInMills) { 259 | JavascriptExecutor jsExecutor = driver; 260 | int screenWebViewHeight = ((Long) jsExecutor.executeScript("return window.innerHeight || document.body.clientHeight")).intValue(); 261 | jsExecutor.executeScript(String.format("window.scrollBy(0, -%d)", screenWebViewHeight / 2)); 262 | 263 | sleepInMillTime(duringInMills); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /autotest-web/src/main/java/com/qianmi/autotest/web/page/WebPageFacade.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.web.page; 2 | 3 | import com.qianmi.autotest.base.common.BeanFactory; 4 | import com.qianmi.autotest.base.common.Logoutable; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * Web页面门户 10 | * Created by liuzhaoming on 16/9/26. 11 | */ 12 | @SuppressWarnings("unused") 13 | @Slf4j 14 | @Component 15 | public class WebPageFacade extends WebBasePage { 16 | 17 | /** 18 | * 退出当前登录用户 19 | */ 20 | public void logout() { 21 | Logoutable logoutable = BeanFactory.getBeanByType(Logoutable.class); 22 | if (null != logoutable) { 23 | logoutable.logout(); 24 | } else { 25 | log.info("Cannot find Logoutable bean"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /autotest-web/src/main/java/com/qianmi/autotest/web/testng/ScreenShotListener.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.web.testng; 2 | 3 | import com.qianmi.autotest.base.common.BeanFactory; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.apache.commons.io.FileUtils; 6 | import org.apache.commons.lang3.StringUtils; 7 | import org.openqa.selenium.OutputType; 8 | import org.openqa.selenium.TakesScreenshot; 9 | import org.openqa.selenium.WebDriver; 10 | import org.testng.ITestContext; 11 | import org.testng.ITestListener; 12 | import org.testng.ITestResult; 13 | import org.testng.Reporter; 14 | 15 | import java.io.File; 16 | import java.text.SimpleDateFormat; 17 | import java.util.Date; 18 | 19 | /** 20 | * 失败用例截屏 21 | * Created by liuzhaoming on 16/10/10. 22 | */ 23 | @Slf4j 24 | public class ScreenShotListener implements ITestListener { 25 | 26 | /** 27 | * Invoked each time before a test will be invoked. 28 | * The ITestResult is only partially filled with the references to 29 | * class, method, start millis and status. 30 | * 31 | * @param result the partially filled ITestResult 32 | * @see ITestResult#STARTED 33 | */ 34 | @Override 35 | public void onTestStart(ITestResult result) { 36 | 37 | } 38 | 39 | /** 40 | * Invoked each time a test succeeds. 41 | * 42 | * @param result ITestResult containing information about the run test 43 | * @see ITestResult#SUCCESS 44 | */ 45 | @Override 46 | public void onTestSuccess(ITestResult result) { 47 | } 48 | 49 | /** 50 | * Invoked each time a test fails. 51 | * 52 | * @param result ITestResult containing information about the run test 53 | * @see ITestResult#FAILURE 54 | */ 55 | @Override 56 | public void onTestFailure(ITestResult result) { 57 | log.info("onTestFailure is called"); 58 | saveScreenShot(result); 59 | } 60 | 61 | /** 62 | * Invoked each time a test is skipped. 63 | * 64 | * @param result ITestResult containing information about the run test 65 | * @see ITestResult#SKIP 66 | */ 67 | @Override 68 | public void onTestSkipped(ITestResult result) { 69 | } 70 | 71 | /** 72 | * Invoked each time a method fails but has been annotated with 73 | * successPercentage and this failure still keeps it within the 74 | * success percentage requested. 75 | * 76 | * @param result ITestResult containing information about the run test 77 | * @see ITestResult#SUCCESS_PERCENTAGE_FAILURE 78 | */ 79 | @Override 80 | public void onTestFailedButWithinSuccessPercentage(ITestResult result) { 81 | 82 | } 83 | 84 | /** 85 | * Invoked after the test class is instantiated and before 86 | * any configuration method is called. 87 | * 88 | * @param context context 89 | */ 90 | @Override 91 | public void onStart(ITestContext context) { 92 | 93 | } 94 | 95 | /** 96 | * Invoked after all the tests have run and all their 97 | * Configuration methods have been called. 98 | * 99 | * @param context context 100 | */ 101 | @Override 102 | public void onFinish(ITestContext context) { 103 | 104 | } 105 | 106 | private void saveScreenShot(ITestResult result) { 107 | SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); 108 | String mDateTime = formatter.format(new Date()); 109 | String fileName = mDateTime + "_" + result.getName(); 110 | String filePath; 111 | try { 112 | WebDriver webDriver = BeanFactory.getBean(WebDriver.class); 113 | if (null == webDriver) { 114 | return; 115 | } 116 | File screenshot = ((TakesScreenshot) webDriver).getScreenshotAs(OutputType.FILE); 117 | filePath = new File(result.getTestContext().getOutputDirectory()).getParentFile().getAbsolutePath() + 118 | "/screenshot/" + fileName + ".jpg"; 119 | File destFile = new File(filePath); 120 | FileUtils.copyFile(screenshot, destFile); 121 | 122 | } catch (Exception e) { 123 | log.error("Fail to save screenshot {}", fileName, e); 124 | return; 125 | } 126 | 127 | if (StringUtils.isNoneBlank(filePath)) { 128 | Reporter.setCurrentTestResult(result); 129 | Reporter.log("Not passed test " + result.getName() + " screenshot is : "); 130 | //把截图写入到Html报告中方便查看 131 | Reporter.log(""); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /autotest-web/src/main/java/com/qianmi/autotest/web/testng/WebTestRetryAnalyzer.java: -------------------------------------------------------------------------------- 1 | package com.qianmi.autotest.web.testng; 2 | 3 | import com.qianmi.autotest.base.common.AutotestUtils; 4 | import com.qianmi.autotest.base.common.BeanFactory; 5 | import com.qianmi.autotest.base.testng.BaseTestRetryAnalyzer; 6 | import com.qianmi.autotest.web.common.WebResourceLoader; 7 | import com.qianmi.autotest.web.data.DataProvider; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.testng.ITestResult; 10 | 11 | import java.lang.reflect.Method; 12 | 13 | /** 14 | * Web自动化测试程序失败重试接口 15 | * Created by liuzhaoming on 2016/12/8. 16 | */ 17 | @Slf4j 18 | public class WebTestRetryAnalyzer extends BaseTestRetryAnalyzer { 19 | /** 20 | * 重启URL 21 | * 22 | * @param result 测试结果 23 | */ 24 | @Override 25 | protected void restart(ITestResult result) { 26 | try { 27 | Method method = result.getMethod().getConstructorOrMethod().getMethod(); 28 | String sceneName = AutotestUtils.getScene(method).value(); 29 | DataProvider inputData = BeanFactory.getBean("inputData"); 30 | String homeUrl = inputData.getProperty("homeUrl", sceneName); 31 | 32 | WebResourceLoader resourceContainer = BeanFactory.getBean(WebResourceLoader.class); 33 | log.info("Restart is called, and begin goto {}", homeUrl); 34 | resourceContainer.gotoPage(homeUrl); 35 | } catch (Exception e) { 36 | log.error("WebTestRetryAnalyzer restart app fail " + result.getMethod().getMethodName(), e); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 1.5.7.RELEASE 10 | 11 | 12 | autotest-parent 13 | 2.0.0-SNAPSHOT 14 | pom 15 | ${project.artifactId} 16 | UI自动化测试框架,支持APP、Web、HTML5三端 17 | 18 | 19 | autotest-app 20 | autotest-html5 21 | autotest-base 22 | autotest-appium 23 | autotest-web 24 | autotest-demo 25 | 26 | 27 | 1.8 28 | 3.12.0 29 | 6.1.0 30 | 1.16.10 31 | 6.14.3 32 | 0.9.11 33 | 3.7 34 | 4.2 35 | 2.7 36 | git@github.com:jingpeicomp/autotest-framework.git 37 | 38 | 39 | 40 | scm:git:${git.url} 41 | scm:git:${git.url} 42 | ${git.url} 43 | HEAD 44 | 45 | 46 | 47 | 48 | 49 | org.projectlombok 50 | lombok 51 | ${lombok.version} 52 | provided 53 | 54 | 55 | org.testng 56 | testng 57 | ${testng.version} 58 | 59 | 60 | org.seleniumhq.selenium 61 | selenium-java 62 | ${selenium.version} 63 | 64 | 65 | org.reflections 66 | reflections 67 | ${reflections.version} 68 | 69 | 70 | io.appium 71 | java-client 72 | ${appium.version} 73 | 74 | 75 | org.apache.commons 76 | commons-lang3 77 | ${common.lang3.version} 78 | 79 | 80 | org.apache.commons 81 | commons-collections4 82 | ${collections.version} 83 | 84 | 85 | commons-io 86 | commons-io 87 | ${common.io.version} 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | org.apache.maven.plugins 96 | maven-source-plugin 97 | 98 | 99 | org.apache.maven.plugins 100 | maven-release-plugin 101 | 102 | ${git.username} 103 | ${git.password} 104 | 105 | 106 | 107 | org.apache.maven.plugins 108 | maven-javadoc-plugin 109 | 110 | -Xdoclint:none 111 | 112 | 113 | 114 | 115 | 116 | 117 | --------------------------------------------------------------------------------