diff --git a/.gitignore b/.gitignore index a1c2a238a965f004ff76978ac1086aa6fe95caea..5e0ec5bbdd85c5cba7e893c60b8c256633493a6b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,8 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* +/.idea/.gitignore +/.idea +*.iml +*/target/* +*/allure-report/* diff --git a/LICENSE-notice.md b/LICENSE-notice.md new file mode 100644 index 0000000000000000000000000000000000000000..520713de1c3b9bcde051c7c5c707a94c3014572b --- /dev/null +++ b/LICENSE-notice.md @@ -0,0 +1,8 @@ +Open Source Licenses +==================== + +This product may include a number of subcomponents with separate +copyright notices and license terms. Your use of the source code for +these subcomponents is subject to the terms and conditions of the +subcomponent's license, as noted in the LICENSE-.md +files. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000000000000000000000000000000000..a32decd83d5bc83f257063b4cefa81e900def75e --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,98 @@ +Eclipse Public License - v 2.0 +============================== + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE (“AGREEMENT”). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +### 1. Definitions + +“Contribution” means: +* **a)** in the case of the initial Contributor, the initial content Distributed under this Agreement, and +* **b)** in the case of each subsequent Contributor: + * **i)** changes to the Program, and + * **ii)** additions to the Program; +where such changes and/or additions to the Program originate from and are Distributed by that particular Contributor. A Contribution “originates” from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include changes or additions to the Program that are not Modified Works. + +“Contributor” means any person or entity that Distributes the Program. + +“Licensed Patents” mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. + +“Program” means the Contributions Distributed in accordance with this Agreement. + +“Recipient” means anyone who receives the Program under this Agreement or any Secondary License (as applicable), including Contributors. + +“Derivative Works” shall mean any work, whether in Source Code or other form, that is based on (or derived from) the Program and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. + +“Modified Works” shall mean any work in Source Code or other form that results from an addition to, deletion from, or modification of the contents of the Program, including, for purposes of clarity any new file in Source Code form that contains any contents of the Program. Modified Works shall not include works that contain only declarations, interfaces, types, classes, structures, or files of the Program solely in each case in order to link to, bind by name, or subclass the Program or Modified Works thereof. + +“Distribute” means the acts of **a)** distributing or **b)** making available in any manner that enables the transfer of a copy. + +“Source Code” means the form of a Program preferred for making modifications, including but not limited to software source code, documentation source, and configuration files. + +“Secondary License” means either the GNU General Public License, Version 2.0, or any later versions of that license, including any exceptions or additional permissions as identified by the initial Contributor. + +### 2. Grant of Rights + +**a)** Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, Distribute and sublicense the Contribution of such Contributor, if any, and such Derivative Works. + +**b)** Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in Source Code or other form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. + +**c)** Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to Distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. + +**d)** Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. + +**e)** Notwithstanding the terms of any Secondary License, no Contributor makes additional grants to any Recipient (other than those set forth in this Agreement) as a result of such Recipient's receipt of the Program under the terms of a Secondary License (if permitted under the terms of Section 3). + +### 3. Requirements + +**3.1** If a Contributor Distributes the Program in any form, then: + +* **a)** the Program must also be made available as Source Code, in accordance with section 3.2, and the Contributor must accompany the Program with a statement that the Source Code for the Program is available under this Agreement, and informs Recipients how to obtain it in a reasonable manner on or through a medium customarily used for software exchange; and + +* **b)** the Contributor may Distribute the Program under a license different than this Agreement, provided that such license: + * **i)** effectively disclaims on behalf of all other Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; + * **ii)** effectively excludes on behalf of all other Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; + * **iii)** does not attempt to limit or alter the recipients' rights in the Source Code under section 3.2; and + * **iv)** requires any subsequent distribution of the Program by any party to be under a license that satisfies the requirements of this section 3. + +**3.2** When the Program is Distributed as Source Code: + +* **a)** it must be made available under this Agreement, or if the Program **(i)** is combined with other material in a separate file or files made available under a Secondary License, and **(ii)** the initial Contributor attached to the Source Code the notice described in Exhibit A of this Agreement, then the Program may be made available under the terms of such Secondary Licenses, and +* **b)** a copy of this Agreement must be included with each copy of the Program. + +**3.3** Contributors may not remove or alter any copyright, patent, trademark, attribution notices, disclaimers of warranty, or limitations of liability (“notices”) contained within the Program from any copy of the Program which they Distribute, provided that Contributors may add their own appropriate notices. + +### 4. Commercial Distribution + +Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor (“Commercial Contributor”) hereby agrees to defend and indemnify every other Contributor (“Indemnified Contributor”) against any losses, damages and costs (collectively “Losses”) arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: **a)** promptly notify the Commercial Contributor in writing of such claim, and **b)** allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. + +### 5. No Warranty + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. + +### 6. Disclaimer of Liability + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +### 7. General + +If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be Distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to Distribute the Program (including its Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. Nothing in this Agreement is intended to be enforceable by any entity that is not a Contributor or Recipient. No third-party beneficiary rights are created under this Agreement. + +#### Exhibit A - Form of Secondary Licenses Notice + +> “This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), version(s), and exceptions or additional permissions here}.” + +Simply including a copy of this Agreement, including this Exhibit A is not sufficient to license the Source Code under Secondary Licenses. + +If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. diff --git a/README.md b/README.md index 91ba6c3297527cbced37384f24edee408f98f029..8f322430ddb18400fa3c2d7adf092b7b875019a1 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,72 @@ # testnewbie-automation - +`TestNewbie-automation`是一个基于`Junit5`、`allure`的,开源的自动化测试脚手架。 #### 介绍 -{**以下是 Gitee 平台说明,您可以替换此简介** -Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN)。专为开发者提供稳定、高效、安全的云端软件开发协作平台 -无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)} +`TestNewbie-automation`是一个基于`Junit5`、`allure`的,开源的自动化测试脚手架。 +依赖`Junit5`的扩展体系,`TestNewbie-automation`封装了一些实用的功能... + + +#### 包含组件 +- automation-interface 接口自动化测试模块,接口自动化测试脚本请添加该依赖 +- automation-ui UI自动化测试模块,UI自动化测试脚本请添加该依赖 +- automation-core 核心的 或者 公共的代码 +- automation-dingding 用例执行完成,发送钉钉通知 +- automation-email 用例执行完成,发送邮件通知 +- automation-agent 开发`Test Newbie`过程中,方便查看方法耗时使用,不用关注 + +#### demo脚本 +- example 接口自动化测试脚本、普通工具类测试脚本 +- example-ui UI自动化测试脚本 + + + +#### 快速开始 +环境要求: +本地安装: java环境,建议java8(java8以下的不支持,8以上的未测试) +本地安装: allure 命令(百度“allure安装”,或者访问allure官网 http://allure.qatools.ru/ ) + + +下载代码到本地,打包 +```sh +git clone xxxxx +cd xxxx +maven-install.bat # 请执行这个脚本打包,用idea的打包,会把测试工程 example、example-ui 也一起打包,从而打包失败 +``` + +执行demo脚本,将下面的项目导入IDEA(或者其他编程工具),打开Terminal,执行 +```sh +cd example +start.bat ## 需要先安装allure +``` +脚本会自动打开浏览器,显示报告 -#### 软件架构 -软件架构说明 +其他方法查看报告: +- 使用IDEA:找到`example`根目录下生成的`allure-report`文件夹,打开`index.html`,点击编辑框右上角的浏览器按钮 -#### 安装教程 -1. xxxx -2. xxxx -3. xxxx +#### 已知问题 +钉钉文本不能自定义 +Csv转Bean 未充分测试 +邮件中的case数据不准确 + + +#### 开发中的功能(TODO) +1. Csv 文件 转Bean + + +#### 关联开源项目 +[JUnit 5](https://junit.org/junit5/): The de-facto standard for unit testing Java applications. +[hutool](https://www.hutool.cn/docs/#/): 一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅 +[AssertJ](https://assertj.github.io/doc/): A fluent assertion library. +[Hamcrest](https://github.com/hamcrest/JavaHamcrest): A library of matcher objects (also known as constraints or predicates). +[JSONassert](https://github.com/skyscreamer/JSONassert): An assertion library for JSON. +[JsonPath](https://github.com/jayway/JsonPath): XPath for JSON. + + #### 使用说明 -1. xxxx -2. xxxx -3. xxxx +1. todo1 #### 参与贡献 @@ -27,13 +74,3 @@ Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN 2. 新建 Feat_xxx 分支 3. 提交代码 4. 新建 Pull Request - - -#### 特技 - -1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md -2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) -3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 -4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 -5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) -6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/automation-agent/README.md b/automation-agent/README.md new file mode 100644 index 0000000000000000000000000000000000000000..a589ef734dd7d67a379f6d8b43165b6f92adac49 --- /dev/null +++ b/automation-agent/README.md @@ -0,0 +1,33 @@ +### 计算耗时代理类 +https://www.cnblogs.com/yihuihui/p/12509416.html + +- Java Agent(java 探针)虽说在 jdk1.5 之后就有了,但是对于绝大多数的业务开发 javaer 来说,这个东西还是比较神奇和陌生的; + +- 我们需要统计方法耗时,所以想到的就是在方法的执行前,记录一个时间,执行完之后统计一下时间差。 +直接修改字节码有点麻烦,因此我们借助神器javaassist来修改字节码。实现自定义的ClassFileTransformer。 + +### 打包 +~~~ +mvn assembly:assembly +~~~ + + +### 使用 + jvm 参数形式:调用 premain 方法 + +其中 jvm 方式,也就是说要使用这个 agent 的目标应用,在启动的时候, +需要指定 jvm 参数 -javaagent:xxx.jar,当我们提供的 agent 属于基础必备服务时,可以用这种方式 + +~~~ +-javaagent:G:/repository/com/testnewbie/automation-agent/1.0.0/automation-agent-1.0.0.jar +-javaagent:G:/repository/com/testnewbie/automation-agent/1.0.0/automation-agent-1.0.0-jar-with-dependencies.jar +-javaagent:D:/dev/Java/repository/com/testnewbie/automation-agent/1.0.0/automation-agent-1.0.0-jar-with-dependencies.jar + +-javaagent:"${settings.localRepository}/com/testnewbie/automation-agent/1.0.0/automation-agent-1.0.0.jar + + + + +~~~ + + diff --git a/automation-agent/pom.xml b/automation-agent/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..95e4c9c971ee69abf24704a9d39d1b88ff441137 --- /dev/null +++ b/automation-agent/pom.xml @@ -0,0 +1,63 @@ + + + + cn.testnewbie.automation + automation-parent + 1.0.0 + + 4.0.0 + + cn.testnewbie.automation + automation-agent + 1.0.0 + jar + + + + cn.testnewbie.automation + automation-core + 1.1.13-SNAPSHOT + + + org.javassist + javassist + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + jar-with-dependencies + + + + + + + cn.testnewbie.agent.SimpleAgent + cn.testnewbie.agent.SimpleAgent + true + true + + + + + + + + attached + + package + + + + + + + diff --git a/automation-agent/src/main/java/cn/testnewbie/agent/CostTransformer.java b/automation-agent/src/main/java/cn/testnewbie/agent/CostTransformer.java new file mode 100644 index 0000000000000000000000000000000000000000..9dc6b1d74549ed078d4373e69691ece8cd7a8bd6 --- /dev/null +++ b/automation-agent/src/main/java/cn/testnewbie/agent/CostTransformer.java @@ -0,0 +1,65 @@ +package cn.testnewbie.agent; + +import cn.testnewbie.automation.core.annotation.TimeCost; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtMethod; +import javassist.bytecode.CodeAttribute; + +import java.io.ByteArrayInputStream; +import java.lang.instrument.ClassFileTransformer; +import java.security.ProtectionDomain; + +/** + * https://www.cnblogs.com/yihuihui/p/12509416.html + * + * @author zhanglx + */ +public class CostTransformer implements ClassFileTransformer { + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer) { + // 这里我们限制下,只针对目标包下进行耗时统计 + if (!className.startsWith("cn/testnewbie/")) { + return classfileBuffer; + } + + CtClass cl = null; + try { + ClassPool classPool = ClassPool.getDefault(); + cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer)); + + // 判断类上是否有注解,如果有,全部方法统计耗时 + boolean hasClassAnnotation = false; + if (cl.getAnnotation(TimeCost.class) != null) { + hasClassAnnotation = true; + } + + for (CtMethod method : cl.getDeclaredMethods()) { + if (!hasClassAnnotation) { + // 判断方法上是否有注解,如果有,即使类上没有,也统计该方法的耗时 + if (method.getAnnotation(TimeCost.class) == null) { + continue; + } + } + + CodeAttribute codeAttribute = method.getMethodInfo().getCodeAttribute(); + if (codeAttribute != null) { + // 所有方法,统计耗时;请注意,需要通过`addLocalVariable`来声明局部变量 + method.addLocalVariable("start", CtClass.longType); + method.insertBefore("start = System.currentTimeMillis();"); + String methodName = method.getLongName(); + // String methodName = method.getDeclaringClass().getName() + "." + method.getName(); + method.insertAfter("System.out.println(\"" + methodName + " 耗时: \" + (System" + + ".currentTimeMillis() - start) + \" ms\");"); + } + } + + byte[] transformed = cl.toBytecode(); + return transformed; + } catch (Exception e) { + e.printStackTrace(); + } + return classfileBuffer; + } +} diff --git a/automation-agent/src/main/java/cn/testnewbie/agent/SimpleAgent.java b/automation-agent/src/main/java/cn/testnewbie/agent/SimpleAgent.java new file mode 100644 index 0000000000000000000000000000000000000000..4f2ae065b1b1aba118c2061481804560262a10e1 --- /dev/null +++ b/automation-agent/src/main/java/cn/testnewbie/agent/SimpleAgent.java @@ -0,0 +1,53 @@ +package cn.testnewbie.agent; + +import java.lang.instrument.Instrumentation; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; + +/** + * https://www.cnblogs.com/yihuihui/p/12509416.html + * + * @author zhanglx + */ +public class SimpleAgent { + + /** + * jvm 参数形式启动,运行此方法 + * manifest需要配置属性Premain-Class + * + * @param agentArgs + * @param inst + */ + public static void premain(String agentArgs, Instrumentation inst) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + LocalDateTime now = LocalDateTime.now(); + String nowText = now.format(formatter); + System.out.println("[" + nowText + "] testnewbie agent start ..."); + customLogic(inst); + } + + /** + * 动态 attach 方式启动,运行此方法 + * manifest需要配置属性Agent-Class + * + * @param agentArgs + * @param inst + */ + public static void agentmain(String agentArgs, Instrumentation inst) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + LocalDateTime now = LocalDateTime.now(); + String nowText = now.format(formatter); + System.out.println("[" + nowText + "] testnewbie agent attach ..."); + customLogic(inst); + } + + /** + * 统计方法耗时 + * + * @param inst + */ + private static void customLogic(Instrumentation inst) { + inst.addTransformer(new CostTransformer(), true); + } +} diff --git a/automation-agent/src/main/resources/META-INF/MANIFEST.MF b/automation-agent/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000000000000000000000000000000000000..fe64ae830cbd2ea93cf4de9c51c1b05b6ee03b91 --- /dev/null +++ b/automation-agent/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,5 @@ +Manifest-Version: 1.0 +Premain-Class: cn.testnewbie.agent.SimpleAgent +Agent-Class: cn.testnewbie.agent.SimpleAgent +Can-Redefine-Classes: true +Can-Retransform-Classes: true diff --git a/automation-core/pom.xml b/automation-core/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..6f0c516d7ed21c546460d9675afb997c018b8095 --- /dev/null +++ b/automation-core/pom.xml @@ -0,0 +1,116 @@ + + + + cn.testnewbie.automation + automation-parent + 1.0.0 + + 4.0.0 + + cn.testnewbie.automation + automation-core + 1.1.13-SNAPSHOT + jar + + + + org.junit.jupiter + junit-jupiter-api + + + org.junit.jupiter + junit-jupiter-params + + + org.junit.jupiter + junit-jupiter + test + + + cn.hutool + hutool-all + + + org.projectlombok + lombok + + + io.qameta.allure + allure-junit5 + ${allure.version} + + + + + + + + + + org.skyscreamer + jsonassert + 1.5.0 + + + org.assertj + assertj-db + 2.0.2 + + + org.apache.commons + commons-dbcp2 + 2.8.0 + + + mysql + mysql-connector-java + test + 8.0.19 + + + ch.qos.logback + logback-classic + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + + maven-surefire-plugin + 2.22.2 + + + + true + + true + + + SAME_THREAD + + dynamic + + + + + + + + + + diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/AssertUtil.java b/automation-core/src/main/java/cn/testnewbie/automation/core/AssertUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..f8fb8ac9fd769e9f2e05d9f070a0539952c9d4ec --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/AssertUtil.java @@ -0,0 +1,89 @@ +package cn.testnewbie.automation.core; + +import cn.hutool.json.JSON; +import cn.hutool.json.JSONNull; +import cn.hutool.json.JSONUtil; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.core.annotation.TimeCost; +import io.qameta.allure.Allure; +import io.qameta.allure.Step; +import org.apiguardian.api.API; +import org.junit.jupiter.api.Assertions; + +import java.util.Map; +import java.util.function.Supplier; + +import static org.apiguardian.api.API.Status.MAINTAINED; + +/** + * @author zhanglx + */ +@TimeCost +@API(status = MAINTAINED, since = "1.0") +public class AssertUtil extends Assertions { + private static final Log log = LogFactory.get(); + + public static org.assertj.db.api.Assertions db; + + /** + * 使用JSONPath检查返回值 + * + * @param json json字符串 + * @param jsonPath JSONPath表达式 + * @param expected 期望值 + */ + public static void assertByJSONPath(String jsonPath, Object expected, String json) { + JSON parse = JSONUtil.parseObj(json, false); + assertByJSONPath(jsonPath, expected, parse); + } + + /** + * 使用JSONPath检查返回值 + * + * @param json json对象 + * @param jsonPath JSONPath表达式 + * @param expected 期望值 + */ + @Step("JSONPath校验") + public static void assertByJSONPath(String jsonPath, Object expected, JSON json) { + Object object = JSONUtil.getByPath(json, jsonPath); + String message; + if (object == null) { + message = "JSONPath = " + jsonPath + + ",期望值 = " + expected + + ",未找到对应的元素"; + Allure.attachment(message, json.toStringPretty()); + fail(message); + } else { + message = "JSONPath = " + jsonPath + + ",期望值 = " + expected + + ",实际值 = " + object; + + if (object instanceof JSONNull) { + assertTrue(object.equals(expected)); + } else { + Supplier failMessage = () -> { + Allure.attachment(message, json.toStringPretty()); + return "JSONPath = " + jsonPath; + }; + assertEquals(expected, object, failMessage); + } + log.debug(message); + } + } + + /** + * 使用JSONPath检查返回值 + * + * @param json json字符串 + * @param jsonPathMap key 为jsonPath,value为expected + */ + public static void assertByJSONPath(Map jsonPathMap, String json) { + JSON parse = JSONUtil.parseObj(json, false); + for (Map.Entry entry : jsonPathMap.entrySet()) { + assertByJSONPath(entry.getKey(), entry.getValue(), parse); + } + } + +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/DataBaseStep.java b/automation-core/src/main/java/cn/testnewbie/automation/core/DataBaseStep.java new file mode 100644 index 0000000000000000000000000000000000000000..645007abe68c047128dd328e8f597c55d21d6211 --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/DataBaseStep.java @@ -0,0 +1,118 @@ +package cn.testnewbie.automation.core; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.core.db.SqlFileTools; +import io.qameta.allure.Step; +import org.apiguardian.api.API; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author zhanglx + */ +@API(status = EXPERIMENTAL, since = "1.0") +public class DataBaseStep { + private static final Log log = LogFactory.get(); + + /** + * 执行测试类目录下的跟测试类同名的sql文件 + * + * @throws Exception + */ + @Step("运行sql文件") + public void execSqlFile() { + String clazzName = this.getClass().getSimpleName(); + File f = new File(this.getClass().getResource("").getPath()); + + List sqlList = null; + try { + sqlList = SqlFileTools.loadSql(f.toPath() + File.separator + clazzName + ".sql"); + for (String sql : sqlList) { +// execSql(sql); + } + log.debug("====================execSqlFile End...===================="); + } catch (Exception e) { + fail("执行测试类目录下的跟测试类同名的sql文件:执行失败!"); + } + } + + /** + * 执行一个普通sql + * + * @param sql + * @throws Exception + */ + @Step("执行sql:'{sql}' ") + public void execSql(String sql) { + if (StrUtil.isEmpty(sql)) { + log.error("请传入一个sql语句"); + fail("请传入一个sql语句"); + } + String upperCaseSql = sql.toUpperCase(); + upperCaseSql = upperCaseSql.replaceAll("\t", " "); + + if (upperCaseSql.indexOf(";") > -1) { + log.error("只能执行单个sql语句,且语句末尾不能带“;”"); + return; + } else if (upperCaseSql.indexOf("DELETE ") > -1) { + if (upperCaseSql.indexOf("WHERE ") == -1) { + log.error("delete语句必须包含where条件,防止误删,全表清空的情况"); + return; + } + } else if (upperCaseSql.indexOf("UPDATE ") > -1) { + if (upperCaseSql.indexOf("WHERE ") == -1) { + log.error("UPDATE语句必须包含where条件,防止误更新,全表更新的情况"); + return; + } + } +// execute(sql); + } + + /** + * 检查数据库 + * + * @param sql 查询sql + * @param checkData 查询结果集合 + * @throws Exception + */ + @Step("检查数据库:'{sql}' '{checkData}'") + public void checkDataBase(String sql, List checkData) { + if (StrUtil.isEmpty(sql)) { + log.error("请传入一个select语句"); + fail("请传入一个select语句"); + } + String upperCaseSql = sql.toUpperCase(); + upperCaseSql = upperCaseSql.replaceAll("\t", " "); + + if (upperCaseSql.indexOf(";") > -1) { + log.error("只能执行单个sql语句,且语句末尾不能带“;”"); + fail("只能执行单个sql语句,且语句末尾不能带“;”"); + } else if (upperCaseSql.indexOf("DELETE ") > -1) { + log.error("不能传入DELETE语句"); + fail("不能传入DELETE语句"); + } else if (upperCaseSql.indexOf("UPDATE ") > -1) { + log.error("不能传入UPDATE语句"); + fail("不能传入UPDATE语句"); + } else if (upperCaseSql.indexOf("SELECT ") == -1) { + log.error("请传入一个select语句"); + fail("请传入一个select语句"); + } + + for (Map map : checkData) { + Set keys = map.keySet(); + for (String key : keys) { + System.out.println(key + "=" + map.get(key)); + // todo + } + } + } + +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/ExcelUtil.java b/automation-core/src/main/java/cn/testnewbie/automation/core/ExcelUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..1752a9a08f807a3a080f2b48069aad61568f32cf --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/ExcelUtil.java @@ -0,0 +1,5 @@ +package cn.testnewbie.automation.core; + +public class ExcelUtil { + +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/annotation/CsvBeanArgument.java b/automation-core/src/main/java/cn/testnewbie/automation/core/annotation/CsvBeanArgument.java new file mode 100644 index 0000000000000000000000000000000000000000..954abd01675e8e822edfe34424ea722bd2a686cc --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/annotation/CsvBeanArgument.java @@ -0,0 +1,34 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package cn.testnewbie.automation.core.annotation; + + +import org.apiguardian.api.API; +import org.junit.jupiter.params.converter.ConvertWith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +/** + * 将map形式的CsvSource转为Bean + * + * @author zhanglx + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@ConvertWith(CsvBeanArgumentConverter.class) +@API(status = EXPERIMENTAL, since = "1.1") +public @interface CsvBeanArgument { +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/annotation/CsvBeanArgumentConverter.java b/automation-core/src/main/java/cn/testnewbie/automation/core/annotation/CsvBeanArgumentConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..95f4a11eadb3e87e06e23fcca074ea73a6593eda --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/annotation/CsvBeanArgumentConverter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package cn.testnewbie.automation.core.annotation; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import org.apiguardian.api.API; +import org.junit.jupiter.params.converter.ArgumentConversionException; +import org.junit.jupiter.params.converter.SimpleArgumentConverter; + +import java.util.HashMap; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 将map形式的CsvSource转为Bean + * + * @author zhanglx + */ +@API(status = EXPERIMENTAL, since = "1.1") +public class CsvBeanArgumentConverter extends SimpleArgumentConverter { + @Override + protected Object convert(Object source, Class targetType) throws ArgumentConversionException { + assertTrue(source instanceof HashMap, "只能由HashMap转成Bean..."); + + HashMap map = (HashMap) source; + CopyOptions copyOptions = CopyOptions.create(); + Object mapToBean = BeanUtil.mapToBean(map, targetType, false, copyOptions); + return mapToBean; + } +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/annotation/CsvBeanSource.java b/automation-core/src/main/java/cn/testnewbie/automation/core/annotation/CsvBeanSource.java new file mode 100644 index 0000000000000000000000000000000000000000..ed7c65e7d2a0770b96fb4995b8179a25ed06c2bd --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/annotation/CsvBeanSource.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package cn.testnewbie.automation.core.annotation; + + +import org.apiguardian.api.API; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import java.lang.annotation.*; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + + +/** + * 将map形式的CsvSource转为Bean + * 使用方法: + * 模版用例 LoginTestUseCsvBeanSource.java + * + * @author zhanglx + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ArgumentsSource(CsvBeanSourceArgumentsProvider.class) +@API(status = EXPERIMENTAL, since = "1.1") +public @interface CsvBeanSource { + + /** + * map形式的参数,不能为空,举例: + * + * @CsvBeanSource( {"customerNo=0001, account=apitest1, password=123456, except=token", + * "customerNo=0001, account=apitest2, password=123456, except=\"status\":true", + * "customerNo=0002, account=apitest3, password=123456, except=账户已被禁用,请联系管理员", + * "customerNo=0002, account=apitest4, password=123456, except=\"status\":false"}) + */ + String[] value(); + + + /** + * 用例之间的分隔符 + */ + String delimiterString() default ","; + + + /** + * 将指定的值转为null,如: + * "N/A" -> null + * "NIL" -> null + * + */ + String[] nullValues() default {}; + + +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/annotation/CsvBeanSourceArgumentsProvider.java b/automation-core/src/main/java/cn/testnewbie/automation/core/annotation/CsvBeanSourceArgumentsProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..91b3afb6dcd81b15b3efca34c67256bcbc5629bb --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/annotation/CsvBeanSourceArgumentsProvider.java @@ -0,0 +1,109 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package cn.testnewbie.automation.core.annotation; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.CsvParsingException; +import org.junit.jupiter.params.support.AnnotationConsumer; +import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.commons.util.UnrecoverableExceptions; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.junit.platform.commons.util.CollectionUtils.toSet; + + +/** + * 将CsvBeanSource的内容,转为Bean + *

+ * 参考 CsvArgumentsProvider.java + * + * @author zhanglx + */ +@API(status = EXPERIMENTAL, since = "1.1") +public class CsvBeanSourceArgumentsProvider implements ArgumentsProvider, AnnotationConsumer { + + private static final String SEPARATOR = "\n"; + private static final String KEY_VALUE_SEPARATOR = "="; + + private CsvBeanSource annotation; + private Set nullValues; + private String delimiterString; + private boolean nullValuesIsEmpty; + + + @Override + public void accept(CsvBeanSource annotation) { + this.annotation = annotation; + this.nullValues = toSet(annotation.nullValues()); + this.nullValuesIsEmpty = this.nullValues.isEmpty(); + this.delimiterString = annotation.delimiterString(); + } + + @Override + public Stream provideArguments(ExtensionContext context) { + AtomicLong index = new AtomicLong(0); + // @formatter:off + return Arrays.stream(this.annotation.value()) + .map(line -> parseLine(index.getAndIncrement(), line)) + .map(Arguments::of); + // @formatter:on + } + + private HashMap parseLine(long index, String line) { + String[] parsedLine = null; + String key; + String value; + + HashMap hashMap = new HashMap<>(); + try { + parsedLine = line.split(delimiterString); + + for (int i = 0; i < parsedLine.length; i++) { + String[] split = parsedLine[i].split(KEY_VALUE_SEPARATOR); + key = split[0].trim(); + value = split[1].trim(); + + // 将特定的值,替换为 null + if (!nullValuesIsEmpty) { + if (this.nullValues.contains(value)) { + value = null; + } + } + hashMap.put(key, value); + } + + } catch (Throwable throwable) { + handleCsvException(throwable, this.annotation); + } + Preconditions.condition(hashMap.size() > 0, () -> "Line at index " + index + " contains invalid CSV: \"" + line + "\""); + return hashMap; + } + + static void handleCsvException(Throwable throwable, Annotation annotation) { + UnrecoverableExceptions.rethrowIfUnrecoverable(throwable); + if (throwable instanceof PreconditionViolationException) { + throw (PreconditionViolationException) throwable; + } + throw new CsvParsingException("Failed to parse CsvBean input configured via " + annotation, throwable); + } + +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/annotation/TimeCost.java b/automation-core/src/main/java/cn/testnewbie/automation/core/annotation/TimeCost.java new file mode 100644 index 0000000000000000000000000000000000000000..434a35804c4aad8836fbe00123d4a93036a57a85 --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/annotation/TimeCost.java @@ -0,0 +1,22 @@ +package cn.testnewbie.automation.core.annotation; + + +import org.apiguardian.api.API; + +import java.lang.annotation.*; + +import static org.apiguardian.api.API.Status.STABLE; + +/** + * 自动统计代码的执行时间,可以标记一个类或者方法 + * 注意:运行时测试时,需要添加命令行参数才会生效,如: + * “-javaagent:/repository/com/testnewbie/automation/automation-agent/1.0.0/automation-agent-1.0.0-jar-with-dependencies.jar” + * + * @author zhanglx + */ +@API(status = STABLE, since = "1.1") +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TimeCost { +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/cache/HttpCacheManage.java b/automation-core/src/main/java/cn/testnewbie/automation/core/cache/HttpCacheManage.java new file mode 100644 index 0000000000000000000000000000000000000000..294141b25696e38150a711188df2907f85fa147b --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/cache/HttpCacheManage.java @@ -0,0 +1,195 @@ +package cn.testnewbie.automation.core.cache; + +import cn.hutool.http.HttpResponse; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.core.annotation.TimeCost; +import org.apiguardian.api.API; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.platform.commons.util.AnnotationUtils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.util.HashMap; +import java.util.Optional; +import java.util.concurrent.*; + +import static org.apiguardian.api.API.Status.MAINTAINED; + +/** + * http接口测试过程中的缓存管理 + * 以map方式保存,执行case前,将map注入测试实例的 + * `public HashMap headerMap = new HashMap<>();` 属性中(如 automation-interface 包下的 HttpStep) + *

+ * 如果不满足测试需要,可以自己copy一份,然后让注解 `T` 关联的实现类(如 example 包下的 LoginWithExtension)继承 + * + * @author zhanglx + */ +@API(status = MAINTAINED, since = "1.1") +public abstract class HttpCacheManage implements ICacheManage { + private static final Log log = LogFactory.get(); + + /*** 已登录的账户的信息 ***/ + protected static final ConcurrentHashMap> CACHE = new ConcurrentHashMap<>(); + /*** 正在登录中的账户 ***/ + protected static final ConcurrentHashMap> PROCESSING = new ConcurrentHashMap<>(); + /*** 处理登录操作的线程池 ***/ + private static final ExecutorService service = new ThreadPoolExecutor( + 4, + 8, + 60, + TimeUnit.SECONDS, + new ArrayBlockingQueue(16), + Executors.defaultThreadFactory(), + new ThreadPoolExecutor.CallerRunsPolicy()); + + /** + * LoginWith 注解放在类上,会进到beforeAll方法(BeforeAllCallback必须在类级别注册) + * LoginWith 注解放在方法上,不会进到下面的方法 + * + * @param context the current extension context; + * @throws Exception 各种异常 + */ + @Override + public void beforeAll(ExtensionContext context) throws Exception { + loginWith(context); + } + + /** + * LoginWith 注解放在类上,会进到beforeAll方法(BeforeAllCallback必须在类级别注册) + * LoginWith 注解放在方法上,不会进到下面的方法 + * + * @param context the current extension context + * @throws Exception 各种异常 + */ + @Override + public void beforeEach(ExtensionContext context) throws Exception { + HashMap authInfoMap = loginWith(context); + updateTestInstance(context, getAnnotation(context), authInfoMap); + } + + /** + * @param context the current extension context + * @return 登录信息map + * @throws ExecutionException 用户提供的方法抛异常 Callable callable = doLogin(loginWith) + * @throws InterruptedException 用户提供的方法抛异常 Callable callable = doLogin(loginWith) + */ + @TimeCost + private HashMap loginWith(ExtensionContext context) throws ExecutionException, InterruptedException { + T loginWith = getAnnotation(context); + String uniqueKey = getUniqueKey(loginWith); + + HashMap authInfoMap = CACHE.get(uniqueKey); + if (authInfoMap == null) { + FutureTask task = PROCESSING.get(uniqueKey); + if (task == null) { + + //双重检查 + synchronized (PROCESSING) { + authInfoMap = CACHE.get(uniqueKey); + if (authInfoMap == null) { + task = PROCESSING.get(uniqueKey); + if (task == null) { + Callable callable = doLogin(loginWith); + task = new FutureTask<>(callable); + service.submit(task); + PROCESSING.put(uniqueKey, task); + } + } else { + return authInfoMap; + } + } + } + HttpResponse httpResponse = task.get(); + log.debug("LoginWith--" + uniqueKey + "--已经登录。"); + return dealHttpResponse(httpResponse, uniqueKey); + + } else { + PROCESSING.remove(uniqueKey); + log.debug("LoginWith--" + uniqueKey + "--已经登录。"); + } + return authInfoMap; + } + + private T getAnnotation(ExtensionContext context) { + Optional element = context.getElement(); + Optional annotation = AnnotationUtils.findAnnotation(element, getTClass()); + + T loginInfo; + if (annotation.isPresent()) { + loginInfo = annotation.get(); + } else { + Optional> testClass = context.getTestClass(); + Assertions.assertTrue(testClass.isPresent()); + loginInfo = testClass.get().getAnnotation(getTClass()); + } + log.debug("---LoginWith--" + this.hashCode()); + Assertions.assertNotNull(loginInfo); + return loginInfo; + } + + /** + * 保存或者获取缓存的登录信息时,使用的key + * 如业务系统唯一的`手机号`、`客户号+帐号`等等 + * + * @param loginInfo 注解里面的信息 + * @return 每个帐号密码的唯一标识 + */ + protected abstract String getUniqueKey(T loginInfo); + + /** + * 登录过程,需要子类自己实现具体逻辑 + * 比如将password加密,然后组装一个json发送给后台请求登录。 + * + * @param loginInfo 注解里面的信息 + * @return 返回整个 HttpResponse + */ + protected abstract Callable doLogin(T loginInfo); + + /** + * 将登录接口返回的HttpResponse,处理成一个map,用例执行时,会作为http信息头,一起发送给服务器 + * 需要子类自己实现具体逻辑,处理token\cookie等 + * + * @param httpResponse doLogin 的返回值。 + * @param uniqueKey 登录信息的唯一标识 + * @return 登录信息map + */ + protected abstract HashMap dealHttpResponse(HttpResponse httpResponse, String uniqueKey); + + + /** + * 将登录信息以HashMap形式,注入到测试实例中 + * 对于 HttpStep 来说,信息头就是一个map + * + * @param extensionContext the current extension context; + * @param annotation 登录信息 + * @throws NoSuchFieldException `测试类`或者`测试类的父类`,没有`public HashMap headerMap = new HashMap<>();` + * @throws IllegalAccessException 上面的headerMap,没有以public修饰 + */ + @TimeCost + protected void updateTestInstance(ExtensionContext extensionContext, T annotation, HashMap authInfoMap) + throws NoSuchFieldException, IllegalAccessException { + Optional testInstance = extensionContext.getTestInstance(); + Assertions.assertTrue(testInstance.isPresent()); + + Object testClassInstance = testInstance.get(); + Field field = testClassInstance.getClass().getField("headerMap"); + field.setAccessible(true); + + HashMap fieldValue = (HashMap) field.get(testClassInstance); + if (authInfoMap != null && authInfoMap.size() != 0) { + fieldValue.putAll(authInfoMap); + field.set(testClassInstance, fieldValue); + log.debug("testClassInstance = {},成功注入登录信息 = {}", testClassInstance.hashCode(), authInfoMap.toString()); + } + } + + + public Class getTClass() { + Class tClass = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + return tClass; + } +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/cache/ICacheManage.java b/automation-core/src/main/java/cn/testnewbie/automation/core/cache/ICacheManage.java new file mode 100644 index 0000000000000000000000000000000000000000..5135834acccf564ba68ea87acada01d262ce59a6 --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/cache/ICacheManage.java @@ -0,0 +1,16 @@ +package cn.testnewbie.automation.core.cache; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; + +import static org.apiguardian.api.API.Status.MAINTAINED; + +/** + * 缓存管理 + * + * @author zhanglx + */ +@API(status = MAINTAINED, since = "1.1") +public interface ICacheManage extends BeforeAllCallback, BeforeEachCallback { +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/common/TNConstants.java b/automation-core/src/main/java/cn/testnewbie/automation/core/common/TNConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..2712c9850f8827fcec8a1736c9fde239490544a1 --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/common/TNConstants.java @@ -0,0 +1,25 @@ +package cn.testnewbie.automation.core.common; + +import org.apiguardian.api.API; + +import static org.apiguardian.api.API.Status.INTERNAL; + +/** + * @author zhanglx + */ +@API(status = INTERNAL, since = "1.1") +public class TNConstants { + + /*** 通用常量 ***/ + public static final String DOT = "."; + + /*** 数据库常量 ***/ + public static final String NAME = "name"; + public static final String DRIVER = "driver"; + public static final String URL = "url"; + public static final String USERNAME = "username"; + public static final String PASSWORD = "password"; + public static final String DATE_FORMAT_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS"; + public static final String TIME_FORMAT_PATTERN = "HH:mm:ss.SSS"; + +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/config/PropertyMgr.java b/automation-core/src/main/java/cn/testnewbie/automation/core/config/PropertyMgr.java new file mode 100644 index 0000000000000000000000000000000000000000..3a83fc317d096d4782480d17abb6bb67a2b4b207 --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/config/PropertyMgr.java @@ -0,0 +1,42 @@ +package cn.testnewbie.automation.core.config; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import org.apiguardian.api.API; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import static org.apiguardian.api.API.Status.MAINTAINED; + +/** + * @author zhanglx + */ +@API(status = MAINTAINED, since = "1.0") +public class PropertyMgr { + private static final Log log = LogFactory.get(); + + private static Properties properties = new Properties(); + + static { + try { + InputStream inputStream = PropertyMgr.class.getClassLoader().getResourceAsStream("testnewbie.properties"); + properties.load(inputStream); + } catch (IOException e) { + log.warn("未找到配置文件'testnewbie.properties'。"); + } + } + + public static String getString(String key) { + if (properties == null) { + return null; + } + return properties.getProperty(key); + } + + public static Properties getProperties() { + return properties; + } + +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/db/DataBase.java b/automation-core/src/main/java/cn/testnewbie/automation/core/db/DataBase.java new file mode 100644 index 0000000000000000000000000000000000000000..5a590e9e07fed38eb5dfe2d5be0d3c3a7364e477 --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/db/DataBase.java @@ -0,0 +1,93 @@ +package cn.testnewbie.automation.core.db; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.core.config.PropertyMgr; +import org.apache.commons.dbcp2.BasicDataSourceFactory; +import org.apiguardian.api.API; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +import static org.apiguardian.api.API.Status.MAINTAINED; + +/** + * @author zhanglx + */ +@API(status = MAINTAINED, since = "1.0") +public class DataBase { + private static final Log log = LogFactory.get(); + + private static DataSource dataSource; + private static Connection conn; + + private void init() { + if (dataSource == null) { + synchronized (DataBase.class) { + // Double checked + if (dataSource == null) { + // 根据配置文件内容获得数据源对象 + try { + dataSource = BasicDataSourceFactory.createDataSource(PropertyMgr.getProperties()); + } catch (Exception e) { + log.warn("初始化DataBase错误,请检查配置文件'testnewbie.properties':{}", e.getMessage()); + } + } + } + } + } + + public DataSource getDataSource() { + if (dataSource == null) { + init(); + } + return dataSource; + } + + public Connection getConnection() throws SQLException { + // 获得连接 + try { + conn = getDataSource().getConnection(); + } catch (SQLException e) { + log.error("获取数据库连接失败...", e); + } + return conn; + } + + + public List executeQuery(String sql) throws SQLException { + if (conn == null) { + conn = getConnection(); + } + Statement stmt = null; + ResultSet rset = null; + List list = null; + try { + stmt = conn.createStatement(); + rset = stmt.executeQuery(sql); + list = ResultSetUtil.convertList(rset); + } catch (SQLException e) { + log.error(e); + } finally { + try { + if (rset != null) { + rset.close(); + } + } catch (Exception e) { + log.error(e); + } + try { + if (stmt != null) { + stmt.close(); + } + } catch (Exception e) { + log.error(e); + } + } + return list; + } +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/db/IDataBase.java b/automation-core/src/main/java/cn/testnewbie/automation/core/db/IDataBase.java new file mode 100644 index 0000000000000000000000000000000000000000..e4fc4229becf094d0247288b4e3c51a44613363d --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/db/IDataBase.java @@ -0,0 +1,13 @@ +package cn.testnewbie.automation.core.db; + +import org.apiguardian.api.API; + +import static org.apiguardian.api.API.Status.MAINTAINED; + +/** + * @author zhanglx + */ +@API(status = MAINTAINED, since = "1.0") +public interface IDataBase { + DataBase db = new DataBase(); +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/db/ResultSetUtil.java b/automation-core/src/main/java/cn/testnewbie/automation/core/db/ResultSetUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..b53d984e0b5f73eb97251b69a4bb6ec1303dd6df --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/db/ResultSetUtil.java @@ -0,0 +1,66 @@ +package cn.testnewbie.automation.core.db; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import org.apiguardian.api.API; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.apiguardian.api.API.Status.STABLE; + +/** + * @author zhanglx + */ +@API(status = STABLE, since = "1.0") +public class ResultSetUtil { + + /** + * 通用取结果方案,返回list + * + * @param rs + * @return + * @throws SQLException + */ + public static List convertList(ResultSet rs) throws SQLException { + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + List arrayList = new ArrayList(); + while (rs.next()) { + Map hashMap = new HashMap(columnCount); + for (int i = 1; i <= columnCount; i++) { + hashMap.put(metaData.getColumnName(i), rs.getObject(i)); + } + arrayList.add(hashMap); + } + return arrayList; + } + + /** + * 通用取结果方案,返回JSONArray + * + * @param rs + * @return + * @throws SQLException + */ + public JSONArray convertJSONArray(ResultSet rs) throws SQLException { + ResultSetMetaData metaData = rs.getMetaData(); + int num = metaData.getColumnCount(); + JSONArray jsonArray = new JSONArray(); + while (rs.next()) { + JSONObject mapOfColValues = new JSONObject(); + for (int i = 1; i <= num; i++) { + mapOfColValues.put(metaData.getColumnName(i), rs.getObject(i)); + } + jsonArray.add(mapOfColValues); + } + return jsonArray; + } + + +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/db/SqlFileTools.java b/automation-core/src/main/java/cn/testnewbie/automation/core/db/SqlFileTools.java new file mode 100644 index 0000000000000000000000000000000000000000..19dba4152c20f45b220133165a1af8a0b01676fe --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/db/SqlFileTools.java @@ -0,0 +1,81 @@ +package cn.testnewbie.automation.core.db; + +import org.apiguardian.api.API; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.List; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +/** + * 执行sql文件,导入测试数据 + * + * @author zhanglixing + */ +@API(status = EXPERIMENTAL, since = "1.0") +public class SqlFileTools { + + public static List loadSql(String fileName) throws Exception { + File sqlFile = new File(fileName); + return loadSql(sqlFile); + } + + /** + * 加载解析sql 读取方式字符流 行读取 + * + * @param file + * @return + * @throws Exception + */ + public static List loadSql(File file) throws Exception { + if (!file.exists()) { + throw new FileNotFoundException("文件不存在"); + } + + List sqlList = new ArrayList(); + try { + FileReader fr = new FileReader(file); + BufferedReader br = new BufferedReader(fr); + String line = ""; + StringBuffer singleSql = new StringBuffer(); + Boolean isAnnotation = false; + + while ((line = br.readLine()) != null) { + // 判断单行注释 -- ,判断跨行注释 /**/ + if (line.trim().startsWith("--")) { + + } else if (line.trim().startsWith("/*")) { + singleSql.append(line); + isAnnotation = true; + + } else if (line.trim().endsWith("*/")) { + singleSql.delete(0, singleSql.length()); + isAnnotation = false; + + } else if (line.trim().endsWith(";") && !isAnnotation) { + // 删除最后的分号,不然执行sql报错 + line = line.substring(0, line.lastIndexOf(";")); + + singleSql.append(line); + sqlList.add(singleSql.toString()); + singleSql.delete(0, singleSql.length()); + } else { + singleSql.append(line); + } + + } + fr.close(); + br.close(); + // for(String sql : sqlList){ + // System.out.println("sql:"+sql); + // } + } catch (Exception e) { + throw new Exception(e.getMessage()); + } + return sqlList; + } +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/junit5/TNNamespace.java b/automation-core/src/main/java/cn/testnewbie/automation/core/junit5/TNNamespace.java new file mode 100644 index 0000000000000000000000000000000000000000..afee8091f32cfb4327f6738d15184e4d63b2c2e3 --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/junit5/TNNamespace.java @@ -0,0 +1,15 @@ +package cn.testnewbie.automation.core.junit5; + + +import org.apiguardian.api.API; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +/** + * 命令空间 + * @author zhanglx + */ +@API(status = EXPERIMENTAL, since = "1.1") +public class TNNamespace { + +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/other/LogbackColorful.java b/automation-core/src/main/java/cn/testnewbie/automation/core/other/LogbackColorful.java new file mode 100644 index 0000000000000000000000000000000000000000..800059d101c2023bf6b0ee06491873c5f74c9e01 --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/other/LogbackColorful.java @@ -0,0 +1,35 @@ +package cn.testnewbie.automation.core.other; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.pattern.color.ANSIConstants; +import ch.qos.logback.core.pattern.color.ForegroundCompositeConverterBase; + +/** + * 版权声明:本文为CSDN博主「MrXionGe」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 + * 原文链接:https://blog.csdn.net/qq_31226223/article/details/82559355 + */ +public class LogbackColorful extends ForegroundCompositeConverterBase { + + @Override + protected String getForegroundColorCode(ILoggingEvent event) { + Level level = event.getLevel(); + switch (level.toInt()) { + //ERROR等级为红色 + case Level.ERROR_INT: + return ANSIConstants.RED_FG; + //WARN等级为黄色 + case Level.WARN_INT: + return ANSIConstants.YELLOW_FG; + //INFO等级为绿色 + case Level.INFO_INT: + return ANSIConstants.GREEN_FG; + //DEBUG等级为蓝色 + case Level.DEBUG_INT: + return ANSIConstants.WHITE_FG; + //其他为默认颜色 + default: + return ANSIConstants.DEFAULT_FG; + } + } +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/util/GuiCamera.java b/automation-core/src/main/java/cn/testnewbie/automation/core/util/GuiCamera.java new file mode 100644 index 0000000000000000000000000000000000000000..8f2080eea627b4565e9b08cf49d3813c61ad9f17 --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/util/GuiCamera.java @@ -0,0 +1,84 @@ +package cn.testnewbie.automation.core.util; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.core.common.TNConstants; +import org.apiguardian.api.API; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; + +import static org.apiguardian.api.API.Status.STABLE; + +/** + * 截图工具类 + * + * @author zhanglixing + */ +@API(status = STABLE, since = "1.0") +public class GuiCamera { + private static final Log log = LogFactory.get(); + + /*** 图像文件的绝对路径 ***/ + private String filePath; + /*** 图像文件的绝对路径 ***/ + private static String defaultFilePath = System.getProperty("user.dir") + File.separator; + /*** 图像文件的格式 ***/ + private static String DEFAULT_IMAGE_FORMAT = "png"; + + public String getDateTime() { + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS"); + return df.format(new Date()); + } + + Dimension d = Toolkit.getDefaultToolkit().getScreenSize(); + + public GuiCamera() { + this.filePath = defaultFilePath; + } + + public GuiCamera(String filePath) { + this.filePath = filePath; + } + + public GuiCamera(String filePath, String subPath) { + this.filePath = StrUtil.isNotBlank(filePath) ? + filePath + File.separator + subPath + File.separator + : defaultFilePath + subPath + File.separator; + } + + public String screenShot() throws Exception { + String fileName = getDateTime(); + return screenShot(fileName, DEFAULT_IMAGE_FORMAT); + } + + public String screenShot(String fileName, String imageFormat) throws Exception { + + // 判断是否存在该目录,如果不存在则新建一个目录 + File folder = new File(filePath); + if (!folder.isDirectory()) { + folder.mkdir(); + } + + // 拷贝屏幕到一个BufferedImage对象screenshot + BufferedImage newImage = new Robot().createScreenCapture( + new Rectangle(0, 0, (int) d.getWidth(), (int) d.getHeight())); + + String newImageName = filePath + fileName + TNConstants.DOT + imageFormat; + File newImageFile = new File(newImageName); + log.debug("Saving File ... {}", newImageName); + // 将screenshot对象写入图像文件 + boolean isSucceed = ImageIO.write(newImage, imageFormat, newImageFile); + if (!isSucceed) { + throw new RuntimeException("写文件失败"); + } + log.debug("Saving File Finished ."); + return newImageName; + } + +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/util/InputStreamToBytes.java b/automation-core/src/main/java/cn/testnewbie/automation/core/util/InputStreamToBytes.java new file mode 100644 index 0000000000000000000000000000000000000000..eac18d710fe3731c2ed9b1f706730be2f5caed95 --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/util/InputStreamToBytes.java @@ -0,0 +1,81 @@ +package cn.testnewbie.automation.core.util; + +import org.apiguardian.api.API; + +import java.awt.*; +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.apiguardian.api.API.Status.STABLE; + +/** + * @author zhanglx + */ +@API(status = STABLE, since = "1.0") +public class InputStreamToBytes { + + /** + * 将输入流转成bytes,方便allure添加到报告里面,如图片 + * + * @param inputStream + * @return + * @throws IOException + */ + public static byte[] convert(InputStream inputStream) throws IOException { + BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + byte[] buffer = null; + int len = 0; + byte[] buf = new byte[2048]; + while ((len = bufferedInputStream.read(buf)) != -1) { + byteArrayOutputStream.write(buf, 0, len); + } + byteArrayOutputStream.flush(); + buffer = byteArrayOutputStream.toByteArray(); + return buffer; + } + + /** + * 将输入流转成特定字符类型的字符串,再转成bytes,方便allure添加到报告里面,如中文文本 + * + * @param inputStream + * @return + * @throws IOException + */ + public static byte[] convertToText(InputStream inputStream, Charset charset) throws IOException { + BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); + StringBuffer stringBuffer = new StringBuffer(); + byte[] buffer = null; + int len = 0; + byte[] buf = new byte[2048]; + while ((len = bufferedInputStream.read(buf)) != -1) { + stringBuffer.append(new String(buf, 0, len, charset)); + } + + buffer = stringBuffer.toString().getBytes(); + return buffer; + } + + /** + * 截图,并返回bytes + * + * @return + * @throws IOException + * @throws AWTException + */ + public static byte[] screenshot() throws Exception { + GuiCamera cam1 = new GuiCamera(null, "target/screenShot"); + String fileName = cam1.screenShot(); + Path content = Paths.get(fileName); + InputStream is = Files.newInputStream(content); + + return convert(is); + } + +} diff --git a/automation-core/src/main/java/cn/testnewbie/automation/core/util/StopWatchUtil.java b/automation-core/src/main/java/cn/testnewbie/automation/core/util/StopWatchUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..a3ac220c4daa4a76214ac7958aaad07d0b401dc8 --- /dev/null +++ b/automation-core/src/main/java/cn/testnewbie/automation/core/util/StopWatchUtil.java @@ -0,0 +1,80 @@ +package cn.testnewbie.automation.core.util; + +import cn.hutool.core.date.StopWatch; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import org.apiguardian.api.API; + +import java.text.NumberFormat; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.apiguardian.api.API.Status.STABLE; + +/** + * @author zhanglx + */ +@API(status = STABLE, since = "1.0") +public class StopWatchUtil { + private static final Log log = LogFactory.get(); + + private AtomicInteger atomicInteger = new AtomicInteger(1); + private StopWatch stopWatch; + private String step = "-->步骤:"; + + public StopWatchUtil(String task) { + stopWatch = new StopWatch(task); + step = task + step; + } + + public void goOn(String step) { + if (stopWatch.isRunning()) { + stopWatch.stop(); + } + stopWatch.start(step); + atomicInteger.incrementAndGet(); + } + + public void stop() { + if (stopWatch.isRunning()) { + stopWatch.stop(); + } + log.debug(prettyPrint()); + } + + public String prettyPrint() { + StringBuilder sb = new StringBuilder(StrUtil + .format("StopWatch '{}': running time = {} ms", + stopWatch.getId(), + stopWatch.getTotalTimeMillis())); + + sb.append(FileUtil.getLineSeparator()); + if (0 == stopWatch.getTaskInfo().length) { + sb.append("No task info kept"); + } else { + sb.append("---------------------------------------------").append(FileUtil.getLineSeparator()); + sb.append("ns % Task name").append(FileUtil.getLineSeparator()); + sb.append("---------------------------------------------").append(FileUtil.getLineSeparator()); + + final NumberFormat nf = NumberFormat.getNumberInstance(); + nf.setMinimumIntegerDigits(9); + nf.setGroupingUsed(false); + + final NumberFormat pf = NumberFormat.getPercentInstance(); + pf.setMinimumIntegerDigits(3); + pf.setGroupingUsed(false); + String format; + for (StopWatch.TaskInfo task : stopWatch.getTaskInfo()) { + sb.append(nf.format(task.getTimeNanos())); + sb.insert(sb.length() - 6, ","); + sb.insert(sb.length() - 3, ","); + sb.append(" "); + sb.append(pf.format((double) task.getTimeNanos() / stopWatch.getTotalTimeNanos())).append(" "); + sb.append(task.getTaskName()).append(FileUtil.getLineSeparator()); + } + } + return sb.toString(); + } + +} diff --git a/automation-core/src/test/resources/allure/allure.properties b/automation-core/src/test/resources/allure/allure.properties new file mode 100644 index 0000000000000000000000000000000000000000..206859214cbe917b96c87adda3894f3afd5d2028 --- /dev/null +++ b/automation-core/src/test/resources/allure/allure.properties @@ -0,0 +1,3 @@ +allure.link.mylink.pattern={} +allure.link.issue.pattern={} +allure.link.tms.pattern={} \ No newline at end of file diff --git a/automation-core/src/test/resources/allure/categories.json b/automation-core/src/test/resources/allure/categories.json new file mode 100644 index 0000000000000000000000000000000000000000..fe34def0b2d05cdb47fd25c28117d622178366c7 --- /dev/null +++ b/automation-core/src/test/resources/allure/categories.json @@ -0,0 +1,23 @@ +[ + { + "name": "通过的case", + "matchedStatuses": ["passed"] + }, + { + "name": "失败的case", + "matchedStatuses": ["failed"] + }, + { + "name": "跳过的case", + "matchedStatuses": ["skipped"] + }, + { + "name": "异常的case", + "matchedStatuses": ["broken", "unknown"] + }, + { + "name": "找不到文件的case", + "matchedStatuses": ["broken"], + "traceRegex": ".*FileNotFoundException.*" + } +] diff --git a/automation-core/src/test/resources/allure/environment.properties b/automation-core/src/test/resources/allure/environment.properties new file mode 100644 index 0000000000000000000000000000000000000000..54e72f574ffe14ab800b2ef4e1cc372bcf9a57eb --- /dev/null +++ b/automation-core/src/test/resources/allure/environment.properties @@ -0,0 +1,3 @@ +Browser=Chrome +Browser.Version=63.0 +Stand=Production diff --git a/automation-core/src/test/resources/log4j.properties b/automation-core/src/test/resources/log4j.properties new file mode 100644 index 0000000000000000000000000000000000000000..01698b29c55f49ce65fc4c50da72b0377b73fcb3 --- /dev/null +++ b/automation-core/src/test/resources/log4j.properties @@ -0,0 +1,18 @@ +#־ TARCE < DEBUG < INFO < WARN < ERROR < FATAL +# ռǼ() ļ/̨ +#log4j.rootLogger=INFO, stdout,file +log4j.rootLogger=DEBUG,stdout + +## Redirect log messages to console +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n +# +## Rirect log messages to a log file +#log4j.appender.file=org.apache.log4j.RollingFileAppender +#log4j.appender.file.File=test.log +#log4j.appender.file.MaxFileSize=5MB +#log4j.appender.file.MaxBackupIndex=10 +#log4j.appender.file.layout=org.apache.log4j.PatternLayout +#log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n diff --git a/automation-core/src/test/resources/testcase/calculator.csv b/automation-core/src/test/resources/testcase/calculator.csv new file mode 100644 index 0000000000000000000000000000000000000000..0c3bc74f880edd8dceb7b950e969bf38a9bafba6 --- /dev/null +++ b/automation-core/src/test/resources/testcase/calculator.csv @@ -0,0 +1,3 @@ +a,b,c +19,21,40 +55,56,111 diff --git a/automation-core/src/test/resources/testcase/content.xml b/automation-core/src/test/resources/testcase/content.xml new file mode 100644 index 0000000000000000000000000000000000000000..bab812d63b43ea025eaf022ef64d2b6d545b414a --- /dev/null +++ b/automation-core/src/test/resources/testcase/content.xml @@ -0,0 +1,2 @@ + +content \ No newline at end of file diff --git a/automation-core/src/test/resources/testcase/page.html b/automation-core/src/test/resources/testcase/page.html new file mode 100644 index 0000000000000000000000000000000000000000..703f41101f73ea1ae0f78dd0b491deceb49476e8 --- /dev/null +++ b/automation-core/src/test/resources/testcase/page.html @@ -0,0 +1 @@ +

Page html file

\ No newline at end of file diff --git a/automation-core/src/test/resources/testcase/sample.csv b/automation-core/src/test/resources/testcase/sample.csv new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/automation-core/src/test/resources/testnewbie.properties b/automation-core/src/test/resources/testnewbie.properties new file mode 100644 index 0000000000000000000000000000000000000000..480d2c5ed7a18d9360c18d62cdf036232f719985 --- /dev/null +++ b/automation-core/src/test/resources/testnewbie.properties @@ -0,0 +1,36 @@ +# +#name=testnewbie +#driver=com.mysql.cj.jdbc.Driver +#url=jdbc:mysql://127.0.0.1:3306/testnewbie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false +#username=root +#password=123456 + + +#ӻ +driverClassName=com.mysql.cj.jdbc.Driver +url=jdbc:mysql://localhost:3306/testnewbie?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true +username=root +password=123456 + +#-------------ӳشСӳʱ-------------------------------- +#ʼ:ӳʱijʼ +#ĬΪ0 +initialSize=0 + +#:ӳͬһʱܹӵ, Ϊʾ +#ĬΪ8 +maxTotal=8 + +#:ӳֿ״̬,Ŀӽͷ,Ϊʾ +#ĬΪ8 +maxIdle=8 + +#С:ӳֿ״̬С,µ,Ϊ0򲻴 +#ע⣺timeBetweenEvictionRunsMillisΪʱЧ +#ĬΪ0 +minIdle=0 + +#ȴʱ +#ûпʱ,ӳصȴӱ黹ʱ(Ժ),ʱ׳쳣,Ϊ<=0ʾ޵ȴ +#Ĭ-1 +maxWaitMillis=-1 diff --git a/automation-core/start.bat b/automation-core/start.bat new file mode 100644 index 0000000000000000000000000000000000000000..a32a77f35c21aed8f02a8712b5166de1ac8b7b8d --- /dev/null +++ b/automation-core/start.bat @@ -0,0 +1,39 @@ +@rem 执行测试(需要安装maven命令) +call mvn clean test + +echo. +echo ------------------------------------------------------------------------ +echo errorlevel = %errorlevel% +echo ------------------------------------------------------------------------ +echo. + +set "results=.\target\allure-results\" +if not exist "%results%" ( + goto fail +) else ( + goto succeed +) + +:succeed +@rem 显示趋势图 +xcopy .\allure-report\history .\target\allure-results\history /e /Y /I + +@rem 显示环境 +xcopy .\src\test\resources\allure\environment.properties .\target\allure-results\ /e /Y /I + +@rem 分类 +xcopy .\src\test\resources\allure\categories.json .\target\allure-results\ /e /Y /I + +echo. +echo generate allure-report +echo ------------------------------------------------------------------------ +echo. + +@rem 生成报告(需要安装allure命令) +call allure generate target/allure-results/ -o allure-report --clean +goto end + +:fail +echo maven error + +:end diff --git a/automation-dingding/pom.xml b/automation-dingding/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5a4896f0f0e829dce853fbe1ac6984ee6fadf782 --- /dev/null +++ b/automation-dingding/pom.xml @@ -0,0 +1,50 @@ + + + + cn.testnewbie.automation + automation-parent + 1.0.0 + + 4.0.0 + + cn.testnewbie.automation + automation-dingding + 1.0.0 + jar + + + 1.8 + ${maven.compiler.source} + + + + + cn.testnewbie.automation + automation-core + 1.1.13-SNAPSHOT + + + org.junit.platform + junit-platform-launcher + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + src/main/resources/META-INF/MANIFEST.MF + + + + + + + + diff --git a/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/DingDingListener.java b/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/DingDingListener.java new file mode 100644 index 0000000000000000000000000000000000000000..70ca59bad05c98b2b1e548d6e3fa5e1f9629a305 --- /dev/null +++ b/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/DingDingListener.java @@ -0,0 +1,56 @@ +package cn.testnewbie.automation.dingding; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.core.annotation.TimeCost; +import cn.testnewbie.automation.core.common.TNConstants; +import cn.testnewbie.automation.dingding.config.DingDingConfig; +import cn.testnewbie.automation.dingding.config.DingDingHelper; +import org.junit.platform.launcher.TestPlan; +import org.junit.platform.launcher.listeners.SummaryGeneratingListener; +import org.junit.platform.launcher.listeners.TestExecutionSummary; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + + +/** + * 用例执行后存在失败的case,发送钉钉到 testnewbie.properties 中配置的webhook + * https://developers.dingtalk.com/document/app/custom-robot-access + * + * @author zhanglx + */ +public class DingDingListener extends SummaryGeneratingListener { + private static final Log log = LogFactory.get(); + + @Override + @TimeCost + public void testPlanExecutionFinished(TestPlan testPlan) { + super.testPlanExecutionFinished(testPlan); + + // 判断是否需要发送钉钉通知 + if (DingDingConfig.isSendDingDing) { + TestExecutionSummary summary = super.getSummary(); + long testsFailedCount = summary.getTestsFailedCount(); + + if (testsFailedCount > 0) { + List mobiles = DingDingHelper.getMobiles(); + StringBuilder atString = new StringBuilder(); + for (String mob : mobiles) { + atString.append("@").append(mob).append(" "); + } + DingDingHelper.sendDingDing(String.format("请注意,有%s个用例失败 %s", + testsFailedCount, + atString.toString())); + } else { + log.info("用例失败数量为0,不发送钉钉通知"); + } + } else { + log.warn("未找到配置文件testnewbie.properties,或者配置文件未配置钉钉参数,或者isSendDingDing设置为false,不发送钉钉通知"); + } + + } + + +} diff --git a/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/bo/At.java b/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/bo/At.java new file mode 100644 index 0000000000000000000000000000000000000000..014ffc098c597cd32f9cda2894dea56aae49a570 --- /dev/null +++ b/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/bo/At.java @@ -0,0 +1,17 @@ +package cn.testnewbie.automation.dingding.bo; + +import lombok.Data; + +import java.util.List; + + +@Data +public class At { + private List atMobiles; + private boolean isAtAll = false; + + public At(List atMobiles) { + this.atMobiles = atMobiles; + } + +} diff --git a/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/bo/Text.java b/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/bo/Text.java new file mode 100644 index 0000000000000000000000000000000000000000..ddd6f9485be1b539a88340ea694e7cb482007cde --- /dev/null +++ b/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/bo/Text.java @@ -0,0 +1,13 @@ +package cn.testnewbie.automation.dingding.bo; + + +import lombok.Data; + +@Data +public class Text { + private String content; + + public Text(String content) { + this.content = content; + } +} diff --git a/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/bo/TextMessage.java b/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/bo/TextMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..907e13b89353f7a0a7ed07f2d6b8847aea0576d3 --- /dev/null +++ b/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/bo/TextMessage.java @@ -0,0 +1,27 @@ +package cn.testnewbie.automation.dingding.bo; + +import lombok.Data; + +import java.util.List; + +/** + * @author zhanglx + */ +@Data +public class TextMessage { + private String msgtype = "text"; + private Text text; + private At at; + + public TextMessage(String content, List atMobiles) { + this.msgtype = msgtype; + this.text = new Text(content); + this.at = new At(atMobiles); + } + + + +} + + + diff --git a/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/config/DingDingConfig.java b/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/config/DingDingConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..047bc76df26bba74e3681f76167aad312b3f0d8b --- /dev/null +++ b/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/config/DingDingConfig.java @@ -0,0 +1,52 @@ +package cn.testnewbie.automation.dingding.config; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Properties; + + +/** + * @author zhanglx + */ +public class DingDingConfig { + private static final String PROPERTIES_DEFAULT = "testnewbie.properties"; + public static boolean isSendDingDing; + public static String dingDingWebhookUrl; + public static String mobiles; + public static String secret; + public static Properties properties; + + static { + init(); + } + + /** + * 初始化 + */ + private static void init() { + properties = new Properties(); + InputStreamReader inputStreamReader = null; + try { + InputStream propertiesIn = DingDingConfig.class.getClassLoader().getResourceAsStream(PROPERTIES_DEFAULT); + if (propertiesIn == null) { + isSendDingDing = false; + return; + } + inputStreamReader = new InputStreamReader( + DingDingConfig.class.getClassLoader().getResourceAsStream(PROPERTIES_DEFAULT), + "UTF-8"); + properties.load(inputStreamReader); + + inputStreamReader.close(); + isSendDingDing = Boolean.parseBoolean(properties.getProperty("isSendDingDing")); + if (isSendDingDing) { + dingDingWebhookUrl = properties.getProperty("dingDingWebhookUrl"); + mobiles = properties.getProperty("mobiles"); + secret = properties.getProperty("secret"); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/config/DingDingHelper.java b/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/config/DingDingHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..37808d563ba653daa49ffea94cd9d45d6d91f1f5 --- /dev/null +++ b/automation-dingding/src/main/java/cn/testnewbie/automation/dingding/config/DingDingHelper.java @@ -0,0 +1,51 @@ +package cn.testnewbie.automation.dingding.config; + +import cn.hutool.http.Header; +import cn.hutool.http.HttpRequest; +import cn.hutool.json.JSONUtil; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.dingding.bo.TextMessage; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * @author zhanglx + */ +public class DingDingHelper { + private static final Log log = LogFactory.get(); + + public static final boolean IS_SENDMAIL = DingDingConfig.isSendDingDing; + private static final String DING_DING_WEBHOOK_URL = DingDingConfig.dingDingWebhookUrl; + private static final String MOBILES = DingDingConfig.mobiles; + private static final String SECRET = DingDingConfig.secret; + + public static void sendDingDing(String content) { + + TextMessage textMessage = new TextMessage(content, getMobiles()); + + String httpBody = JSONUtil.toJsonStr(textMessage); + log.debug("发送钉钉:{}", httpBody); + + String httpResponse = HttpRequest.post(DING_DING_WEBHOOK_URL) +// .header(Header.USER_AGENT, "Hutool http") + .header(Header.CONTENT_TYPE, "application/json") + .body(httpBody) + .execute().body(); + log.debug("请求结果:msg = {}", httpResponse); + } + + static public List getMobiles() { + String[] mobilesArray = MOBILES.split(","); + // 简单过滤 + List collect = Arrays.stream(mobilesArray) + .filter(str -> str.length() == 11) + .distinct() + .collect(Collectors.toList()); + return collect; + } + +} diff --git a/automation-dingding/src/main/resources/META-INF/MANIFEST.MF b/automation-dingding/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000000000000000000000000000000000000..4a23eeff96ca4d63f48ea2ad50930ab2f9b25cca --- /dev/null +++ b/automation-dingding/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Implementation-Title: Testnewbie-mail +Automatic-Module-Name: com.testnewbie.automation.mail diff --git a/automation-dingding/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener b/automation-dingding/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener new file mode 100644 index 0000000000000000000000000000000000000000..24d1ac70335aea929ad1e1239bf7938d9d189941 --- /dev/null +++ b/automation-dingding/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener @@ -0,0 +1 @@ +cn.testnewbie.automation.dingding.DingDingListener diff --git a/automation-email/pom.xml b/automation-email/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..02fb207a0d0149c3b1ac179b31f0a0b9e8a34a34 --- /dev/null +++ b/automation-email/pom.xml @@ -0,0 +1,56 @@ + + + + 4.0.0 + + cn.testnewbie.automation + automation-parent + 1.0.0 + + + cn.testnewbie.automation + automation-email + 1.0.0 + jar + + + 1.8 + ${maven.compiler.source} + + + + + cn.testnewbie.automation + automation-core + 1.1.13-SNAPSHOT + + + org.junit.platform + junit-platform-launcher + 1.7.1 + + + javax.mail + mail + 1.4.7 + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + src/main/resources/META-INF/MANIFEST.MF + + + + + + + diff --git a/automation-email/src/main/java/cn/testnewbie/automation/mail/Constant.java b/automation-email/src/main/java/cn/testnewbie/automation/mail/Constant.java new file mode 100644 index 0000000000000000000000000000000000000000..71ba52c222515be9fadd3761cd297c55c78086e2 --- /dev/null +++ b/automation-email/src/main/java/cn/testnewbie/automation/mail/Constant.java @@ -0,0 +1,12 @@ +package cn.testnewbie.automation.mail; + +public class Constant { + + public static final String TAB = "   "; + public static final String DOUBLE_TAB = TAB + TAB; + public static final int DEFAULT_MAX_STACKTRACE_LINES = 10; + + public static final int DEFAULT_MAX_CASE_INFO_LINES = 10; + public static final int DEFAULT_MAX_STACK_TRACE_COUNT = 2; + +} diff --git a/automation-email/src/main/java/cn/testnewbie/automation/mail/EmailListener.java b/automation-email/src/main/java/cn/testnewbie/automation/mail/EmailListener.java new file mode 100644 index 0000000000000000000000000000000000000000..c7f3aaed181de49d7522edc982682091f3f4e856 --- /dev/null +++ b/automation-email/src/main/java/cn/testnewbie/automation/mail/EmailListener.java @@ -0,0 +1,257 @@ +package cn.testnewbie.automation.mail; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.core.annotation.TimeCost; +import cn.testnewbie.automation.core.common.TNConstants; +import cn.testnewbie.automation.mail.config.MailConfig; +import cn.testnewbie.automation.mail.config.MailHelper; +import org.junit.platform.launcher.TestPlan; +import org.junit.platform.launcher.listeners.TestExecutionSummary; + +import javax.mail.MessagingException; +import java.io.UnsupportedEncodingException; +import java.text.NumberFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.List; + +import static java.lang.String.format; + + +/** + * 用例执行后存在失败的case,发送邮件到 testnewbie.properties 中配置的邮箱 + * + * @author zhanglx + */ +public class EmailListener extends MailSummaryGeneratingListener { + private static final Log log = LogFactory.get(); + + /*** 邮件主题 ***/ + public String subject = "自动化测试"; + + + @Override + @TimeCost + public void testPlanExecutionFinished(TestPlan testPlan) { + super.testPlanExecutionFinished(testPlan); + MailTestExecutionSummary summary = super.getSummary(); + summary.printTo(); + + // 判断是否需要发送邮件 + if (MailHelper.isSendMail) { + long testsFailedCount = summary.getTestsFailedCount(); + + if (testsFailedCount > 0) { + subject = MailConfig.subject; + generateReport(summary); + return; + } else { + log.info("用例失败数量为0,不发送邮件通知"); + } + } else { + log.warn("未找到配置文件testnewbie.properties,或者配置文件未配置邮箱参数,或者isSendMail设置为false,不发送邮件"); + } + } + + + @TimeCost + public void generateReport(MailTestExecutionSummary summary) { + long testsFailedCount = summary.getTestsFailedCount(); + if (testsFailedCount == 0) { + log.info("用例失败数量为0,不发送邮件通知"); + return; + } + + String tableContent = getTableContent(summary); + String failStackTrace = getFailStackTrace(summary); + + String mailHtml = getMailHtml(tableContent, failStackTrace); + + failTestMail(mailHtml); + + // 写到文件中,方便调试 +// try (FileWriter fileWriter = new FileWriter("ccc.html")) { +// fileWriter.write(mailHtml); +// fileWriter.flush(); +// } catch (IOException e) { +// e.printStackTrace(); +// } + } + + private String getTableTile() { + return " \n" + + " 执行时间\n" + + " 通过\n" + + " 跳过\n" + + " 失败\n" + + " 通过率%\n" + + " 失败的case\n" + + " \n"; + } + + private String getMailHtml(String tableContent, String failStackTrace) { + return "\n" + + "\n" + + "\n" + + "" + subject + "\n" + + "\n" + + getCss() + + "\n" + + "\n" + + "\n" + + getTableTile() + + tableContent + +// lastrow + + "
数据汇总
\n" + + failStackTrace + +// "

备注:详细内容见附件

\n" + + "\n" + + ""; + } + + private String getCss() { + return " "; + } + + private String getTableContent(MailTestExecutionSummary summary) { + String CAUSED_BY = "Caused by: "; + String SUPPRESSED = "Suppressed: "; + String CIRCULAR = "Circular reference: "; + + long testsFoundCount = summary.getTestsFoundCount(); + long testsSkippedCount = summary.getTestsSkippedCount(); + long testsStartedCount = summary.getTestsStartedCount(); + long testsAbortedCount = summary.getTestsAbortedCount(); + long testsSucceededCount = summary.getTestsSucceededCount(); + long testsFailedCount = summary.getTestsFailedCount(); + + // 创建一个数值格式化对象 + NumberFormat numberFormat = NumberFormat.getInstance(); + // 设置精确到小数点后2位 + numberFormat.setMaximumFractionDigits(2); + //所占百分比 + String passPercent = numberFormat.format((float) testsSucceededCount / testsStartedCount * 100); + + String failCase = getFailCase(summary); + + StringBuilder tableContent = new StringBuilder(); + String format = DateUtil.format(new Date(summary.getTimeStarted()), "yyyy-MM-dd HH:mm:ss"); + float timecost = (float) (summary.getTimeFinished() - summary.getTimeStarted()) / 1000; + + // 执行时间 + String execTime = format("%s
( %s 秒)", format, timecost); + + tableContent = tableContent.append(" \n" + + " " + execTime + "\n" + + " " + testsSucceededCount + "\n" + + " " + testsSkippedCount + "\n" + + " " + testsFailedCount + "\n" + + " " + passPercent + "%\n" + + // 输出失败的用例信息 + " " + failCase + "\n" + + " \n"); + return tableContent.toString(); + } + + private String getFailCase(MailTestExecutionSummary summary) { + StringBuilder failCase = new StringBuilder(); + List failures = summary.getFailures(); + + int count = 1; + for (TestExecutionSummary.Failure failure : failures) { + if (count > Constant.DEFAULT_MAX_CASE_INFO_LINES) { + failCase.append("...
剩余" + (failures.size() - Constant.DEFAULT_MAX_CASE_INFO_LINES) + "个失败用例,请直接查看报告。"); + break; + } + failCase.append(summary.getCaseInfo(failure.getTestIdentifier())); + count++; + } + return failCase.toString(); + } + + /** + * 生成堆栈信息 + * + * @param summary + * @return + */ + private String getFailStackTrace(MailTestExecutionSummary summary) { + StringBuilder failStackTrace = new StringBuilder(); + failStackTrace.append("

"); + failStackTrace.append("Failures ("); + failStackTrace.append(summary.getTotalFailureCount()); + failStackTrace.append("):
"); + + List failures = summary.getFailures(); + + int count = 1; + for (TestExecutionSummary.Failure failure : failures) { + if (count > Constant.DEFAULT_MAX_STACK_TRACE_COUNT) { + failStackTrace.append("
剩余" + (failures.size() - Constant.DEFAULT_MAX_STACK_TRACE_COUNT) + "个失败用例,请直接查看报告。"); + break; + } + failStackTrace.append(Constant.TAB); + failStackTrace.append(summary.describeTest(failure.getTestIdentifier())); + failStackTrace.append("
"); + failStackTrace.append(summary.getSource(failure.getTestIdentifier())); + failStackTrace.append(Constant.DOUBLE_TAB); + failStackTrace.append("=> "); + failStackTrace.append(failure.getException()); + failStackTrace.append("
"); + failStackTrace.append(summary.getStackTrace(failure.getException(), 10)); + count++; + } + + failStackTrace.append("

"); + return failStackTrace.toString(); + } + + /** + * 发送邮件 + * + * @param mailHtml 邮件内容 + */ + @TimeCost + public void failTestMail(String mailHtml) { + + log.debug("******* start send email ... *******"); + MailHelper mailUtil = new MailHelper(); + try { + mailUtil.sendMail(subject, mailHtml); + } catch (MessagingException e) { + log.error("发送邮件失败", e); + } catch (UnsupportedEncodingException e) { + log.error("发送邮件失败", e); + } + } + + +} diff --git a/automation-email/src/main/java/cn/testnewbie/automation/mail/MailSummaryGeneratingListener.java b/automation-email/src/main/java/cn/testnewbie/automation/mail/MailSummaryGeneratingListener.java new file mode 100644 index 0000000000000000000000000000000000000000..c7440456d4424e16f3ae49edc19302fe6fc55097 --- /dev/null +++ b/automation-email/src/main/java/cn/testnewbie/automation/mail/MailSummaryGeneratingListener.java @@ -0,0 +1,128 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package cn.testnewbie.automation.mail; + +import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.TestPlan; +import org.junit.platform.launcher.listeners.SummaryGeneratingListener; + +import java.util.stream.Stream; + +import static java.util.stream.Stream.concat; + +/** + * 参考自: + * https://github.com/junit-team/junit5/blob/main/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/SummaryGeneratingListener.java + * + * @author zhanglx + * @see SummaryGeneratingListener + */ +public class MailSummaryGeneratingListener implements TestExecutionListener { + + private TestPlan testPlan; + private MailTestExecutionSummary summary; + + public MailTestExecutionSummary getSummary() { + return this.summary; + } + + @Override + public void testPlanExecutionStarted(TestPlan testPlan) { + this.testPlan = testPlan; + this.summary = new MailTestExecutionSummary(testPlan); + } + + @Override + public void testPlanExecutionFinished(TestPlan testPlan) { + this.summary.timeFinished = System.currentTimeMillis(); + } + + @Override + public void dynamicTestRegistered(TestIdentifier testIdentifier) { + if (testIdentifier.isContainer()) { + this.summary.containersFound.incrementAndGet(); + } + if (testIdentifier.isTest()) { + this.summary.testsFound.incrementAndGet(); + } + } + + @Override + public void executionSkipped(TestIdentifier testIdentifier, String reason) { + // @formatter:off + long skippedContainers = concat(Stream.of(testIdentifier), testPlan.getDescendants(testIdentifier).stream()) + .filter(TestIdentifier::isContainer) + .count(); + long skippedTests = concat(Stream.of(testIdentifier), testPlan.getDescendants(testIdentifier).stream()) + .filter(TestIdentifier::isTest) + .count(); + // @formatter:on + this.summary.containersSkipped.addAndGet(skippedContainers); + this.summary.testsSkipped.addAndGet(skippedTests); + } + + @Override + public void executionStarted(TestIdentifier testIdentifier) { + if (testIdentifier.isContainer()) { + this.summary.containersStarted.incrementAndGet(); + } + if (testIdentifier.isTest()) { + this.summary.testsStarted.incrementAndGet(); + } + } + + @Override + public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + + switch (testExecutionResult.getStatus()) { + + case SUCCESSFUL: { + if (testIdentifier.isContainer()) { + this.summary.containersSucceeded.incrementAndGet(); + } + if (testIdentifier.isTest()) { + this.summary.testsSucceeded.incrementAndGet(); + } + break; + } + + case ABORTED: { + if (testIdentifier.isContainer()) { + this.summary.containersAborted.incrementAndGet(); + } + if (testIdentifier.isTest()) { + this.summary.testsAborted.incrementAndGet(); + } + break; + } + + case FAILED: { + if (testIdentifier.isContainer()) { + this.summary.containersFailed.incrementAndGet(); + } + if (testIdentifier.isTest()) { + this.summary.testsFailed.incrementAndGet(); + } + testExecutionResult.getThrowable().ifPresent( + throwable -> this.summary.addFailure(testIdentifier, throwable)); + break; + } + + default: + throw new PreconditionViolationException( + "Unsupported execution status:" + testExecutionResult.getStatus()); + } + } + +} diff --git a/automation-email/src/main/java/cn/testnewbie/automation/mail/MailTestExecutionSummary.java b/automation-email/src/main/java/cn/testnewbie/automation/mail/MailTestExecutionSummary.java new file mode 100644 index 0000000000000000000000000000000000000000..5de47609a591d6324ac882985b6c4dc9d36cdf5d --- /dev/null +++ b/automation-email/src/main/java/cn/testnewbie/automation/mail/MailTestExecutionSummary.java @@ -0,0 +1,433 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package cn.testnewbie.automation.mail; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.descriptor.CompositeTestSource; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.engine.support.descriptor.PackageSource; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.TestPlan; +import org.junit.platform.launcher.listeners.TestExecutionSummary; + +import java.io.PrintWriter; +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static java.lang.String.format; +import static java.lang.String.join; + +/** + * 参考自: + * https://github.com/junit-team/junit5/blob/main/junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/MutableTestExecutionSummary.java + * + * @author zhanglx + */ +public class MailTestExecutionSummary implements TestExecutionSummary { + private static final Log log = LogFactory.get(); + + private static final String TAB = "   "; + + private static final String CAUSED_BY = "Caused by: "; + private static final String SUPPRESSED = "Suppressed: "; + private static final String CIRCULAR = "Circular reference: "; + + final AtomicLong containersFound = new AtomicLong(); + final AtomicLong containersStarted = new AtomicLong(); + final AtomicLong containersSkipped = new AtomicLong(); + final AtomicLong containersAborted = new AtomicLong(); + final AtomicLong containersSucceeded = new AtomicLong(); + final AtomicLong containersFailed = new AtomicLong(); + + final AtomicLong testsFound = new AtomicLong(); + final AtomicLong testsStarted = new AtomicLong(); + final AtomicLong testsSkipped = new AtomicLong(); + final AtomicLong testsAborted = new AtomicLong(); + final AtomicLong testsSucceeded = new AtomicLong(); + final AtomicLong testsFailed = new AtomicLong(); + + private final TestPlan testPlan; + private final List failures = new ArrayList<>(); + private final long timeStarted; + long timeFinished; + + MailTestExecutionSummary(TestPlan testPlan) { + this.testPlan = testPlan; + this.containersFound.set(testPlan.countTestIdentifiers(TestIdentifier::isContainer)); + this.testsFound.set(testPlan.countTestIdentifiers(TestIdentifier::isTest)); + this.timeStarted = System.currentTimeMillis(); + } + + void addFailure(TestIdentifier testIdentifier, Throwable throwable) { + this.failures.add(new DefaultFailure(testIdentifier, throwable)); + } + + @Override + public long getTimeStarted() { + return this.timeStarted; + } + + @Override + public long getTimeFinished() { + return this.timeFinished; + } + + @Override + public long getTotalFailureCount() { + return getTestsFailedCount() + getContainersFailedCount(); + } + + @Override + public long getContainersFoundCount() { + return this.containersFound.get(); + } + + @Override + public long getContainersStartedCount() { + return this.containersStarted.get(); + } + + @Override + public long getContainersSkippedCount() { + return this.containersSkipped.get(); + } + + @Override + public long getContainersAbortedCount() { + return this.containersAborted.get(); + } + + @Override + public long getContainersSucceededCount() { + return this.containersSucceeded.get(); + } + + @Override + public long getContainersFailedCount() { + return this.containersFailed.get(); + } + + @Override + public long getTestsFoundCount() { + return this.testsFound.get(); + } + + @Override + public long getTestsStartedCount() { + return this.testsStarted.get(); + } + + @Override + public long getTestsSkippedCount() { + return this.testsSkipped.get(); + } + + @Override + public long getTestsAbortedCount() { + return this.testsAborted.get(); + } + + @Override + public long getTestsSucceededCount() { + return this.testsSucceeded.get(); + } + + @Override + public long getTestsFailedCount() { + return this.testsFailed.get(); + } + + @Override + public void printTo(PrintWriter writer) { + // @formatter:off + writer.printf("%nTest run finished after %d ms%n" + + + "[%10d containers found ]%n" + + "[%10d containers skipped ]%n" + + "[%10d containers started ]%n" + + "[%10d containers aborted ]%n" + + "[%10d containers successful ]%n" + + "[%10d containers failed ]%n" + + + "[%10d tests found ]%n" + + "[%10d tests skipped ]%n" + + "[%10d tests started ]%n" + + "[%10d tests aborted ]%n" + + "[%10d tests successful ]%n" + + "[%10d tests failed ]%n" + + "%n", + + (this.timeFinished - this.timeStarted), + + getContainersFoundCount(), + getContainersSkippedCount(), + getContainersStartedCount(), + getContainersAbortedCount(), + getContainersSucceededCount(), + getContainersFailedCount(), + + getTestsFoundCount(), + getTestsSkippedCount(), + getTestsStartedCount(), + getTestsAbortedCount(), + getTestsSucceededCount(), + getTestsFailedCount() + ); + // @formatter:on + + writer.flush(); + } + + public void printTo() { + // @formatter:off + log.info("测试统计:\n" + + "---------------------------------\n" + + "Test run finished after {} ms\n" + + "---------------------------------\n" + + + "|containers found {}\n" + + "|containers skipped {}\n" + + "|containers started {}\n" + + "|containers aborted {}\n" + + "|containers successful {}\n" + + "|containers failed {}\n" + + + "|tests found {}\n" + + "|tests skipped {}\n" + + "|tests started {}\n" + + "|tests aborted {}\n" + + "|tests successful {}\n" + + "|tests failed {}\n", + + (this.timeFinished - this.timeStarted), + + getContainersFoundCount(), + getContainersSkippedCount(), + getContainersStartedCount(), + getContainersAbortedCount(), + getContainersSucceededCount(), + getContainersFailedCount(), + + getTestsFoundCount(), + getTestsSkippedCount(), + getTestsStartedCount(), + getTestsAbortedCount(), + getTestsSucceededCount(), + getTestsFailedCount() + ); + } + + @Override + public void printFailuresTo(PrintWriter writer) { + printFailuresTo(writer, Constant.DEFAULT_MAX_STACKTRACE_LINES); + } + + @Override + public void printFailuresTo(PrintWriter writer, int maxStackTraceLines) { + Preconditions.notNull(writer, "PrintWriter must not be null"); + Preconditions.condition(maxStackTraceLines >= 0, "maxStackTraceLines must be a positive number"); + + if (getTotalFailureCount() > 0) { + writer.printf("%nFailures (%d):%n", getTotalFailureCount()); + this.failures.forEach(failure -> { + writer.printf("%s%s%n", TAB, describeTest(failure.getTestIdentifier())); + printSource(writer, failure.getTestIdentifier()); + writer.printf("%s=> %s%n", Constant.DOUBLE_TAB, failure.getException()); + printStackTrace(writer, failure.getException(), maxStackTraceLines); + }); + writer.flush(); + } + } + + @Override + public List getFailures() { + return Collections.unmodifiableList(failures); + } + + public String describeTest(TestIdentifier testIdentifier) { + List descriptionParts = new ArrayList<>(); + collectTestDescription(testIdentifier, descriptionParts); + return join(":", descriptionParts); + } + + private void collectTestDescription(TestIdentifier identifier, List descriptionParts) { + descriptionParts.add(0, identifier.getDisplayName()); + this.testPlan.getParent(identifier).ifPresent(parent -> collectTestDescription(parent, descriptionParts)); + } + + private void printSource(PrintWriter writer, TestIdentifier testIdentifier) { + testIdentifier.getSource().ifPresent(source -> writer.printf("%s%s%n", Constant.DOUBLE_TAB, source)); + } + + public String getSource(TestIdentifier testIdentifier) { + AtomicReference sourceString = new AtomicReference(); + testIdentifier.getSource().ifPresent(source -> sourceString.set(format("%s%s
", Constant.DOUBLE_TAB, source))); + return sourceString.toString(); + } + + public String getCaseInfo(TestIdentifier testIdentifier) { + AtomicReference sourceString = new AtomicReference(); + if (testIdentifier.getSource().isPresent()) { + TestSource testSource = testIdentifier.getSource().get(); + + if (testSource instanceof MethodSource) { + MethodSource source = (MethodSource) testSource; + sourceString.set(format("%s.%s
", + source.getClassName(), + source.getMethodName() + )); + } else if (testSource instanceof ClassSource) { + ClassSource source = (ClassSource) testSource; + sourceString.set(format("%s.%s
", + source.getClassName(), + "*" + )); + } else if (testSource instanceof PackageSource) { + PackageSource source = (PackageSource) testSource; + sourceString.set(format("%s.%s
", + source.getPackageName(), + "*" + )); + } else if (testSource instanceof CompositeTestSource) { + CompositeTestSource source = (CompositeTestSource) testSource; + sourceString.set(format("%s.%s
", + source.getSources().toString(), + "*" + )); + } else { + log.error(testSource.toString()); + } + } + return sourceString.toString(); + } + + public void printStackTrace(PrintWriter writer, Throwable throwable, int max) { + if (throwable.getCause() != null + || (throwable.getSuppressed() != null && throwable.getSuppressed().length > 0)) { + max = max / 2; + } + printStackTrace(writer, new StackTraceElement[]{}, throwable, "", Constant.DOUBLE_TAB + " ", new HashSet<>(), max); + writer.flush(); + } + + public String getStackTrace(Throwable throwable, int max) { + if (throwable.getCause() != null + || (throwable.getSuppressed() != null && throwable.getSuppressed().length > 0)) { + max = max / 2; + } + return getStackTrace(new StackTraceElement[]{}, throwable, "", Constant.DOUBLE_TAB + " ", new HashSet<>(), max); + } + + public String getStackTrace(StackTraceElement[] parentTrace, Throwable throwable, + String caption, String indentation, Set seenThrowables, int max) { + StringBuilder stringBuilder = new StringBuilder(); + String temp = ""; + + if (seenThrowables.contains(throwable)) { + stringBuilder.append(format("%s%s[%s%s]
", indentation, TAB, CIRCULAR, throwable)); + return stringBuilder.toString(); + } + seenThrowables.add(throwable); + + StackTraceElement[] trace = throwable.getStackTrace(); + if (parentTrace != null && parentTrace.length > 0) { + stringBuilder.append(format("%s%s%s
", indentation, caption, throwable)); + } + int duplicates = numberOfCommonFrames(trace, parentTrace); + int numDistinctFrames = trace.length - duplicates; + int numDisplayLines = (numDistinctFrames > max) ? max : numDistinctFrames; + for (int i = 0; i < numDisplayLines; i++) { + stringBuilder.append(format("%s%s%s
", indentation, TAB, trace[i])); + } + if (trace.length > max || duplicates != 0) { + stringBuilder.append(format("%s%s%s
", indentation, TAB, "[...]")); + } + + for (Throwable suppressed : throwable.getSuppressed()) { + stringBuilder.append(getStackTrace(trace, suppressed, SUPPRESSED, indentation + TAB, seenThrowables, max)); + } + if (throwable.getCause() != null) { + stringBuilder.append(getStackTrace(trace, throwable.getCause(), CAUSED_BY, indentation, seenThrowables, max)); + } + + return stringBuilder.toString(); + } + + public void printStackTrace(PrintWriter writer, StackTraceElement[] parentTrace, Throwable throwable, + String caption, String indentation, Set seenThrowables, int max) { + if (seenThrowables.contains(throwable)) { + writer.printf("%s%s[%s%s]%n", indentation, TAB, CIRCULAR, throwable); + return; + } + seenThrowables.add(throwable); + + StackTraceElement[] trace = throwable.getStackTrace(); + if (parentTrace != null && parentTrace.length > 0) { + writer.printf("%s%s%s%n", indentation, caption, throwable); + } + int duplicates = numberOfCommonFrames(trace, parentTrace); + int numDistinctFrames = trace.length - duplicates; + int numDisplayLines = (numDistinctFrames > max) ? max : numDistinctFrames; + for (int i = 0; i < numDisplayLines; i++) { + writer.printf("%s%s%s%n", indentation, TAB, trace[i]); + } + if (trace.length > max || duplicates != 0) { + writer.printf("%s%s%s%n", indentation, TAB, "[...]"); + } + + for (Throwable suppressed : throwable.getSuppressed()) { + printStackTrace(writer, trace, suppressed, SUPPRESSED, indentation + TAB, seenThrowables, max); + } + if (throwable.getCause() != null) { + printStackTrace(writer, trace, throwable.getCause(), CAUSED_BY, indentation, seenThrowables, max); + } + } + + private int numberOfCommonFrames(StackTraceElement[] currentTrace, StackTraceElement[] parentTrace) { + int currentIndex = currentTrace.length - 1; + for (int parentIndex = parentTrace.length - 1; currentIndex >= 0 + && parentIndex >= 0; currentIndex--, parentIndex--) { + if (!currentTrace[currentIndex].equals(parentTrace[parentIndex])) { + break; + } + } + return currentTrace.length - 1 - currentIndex; + } + + private static class DefaultFailure implements Failure { + + private static final long serialVersionUID = 1L; + + private final TestIdentifier testIdentifier; + private final Throwable exception; + + DefaultFailure(TestIdentifier testIdentifier, Throwable exception) { + this.testIdentifier = testIdentifier; + this.exception = exception; + } + + @Override + public TestIdentifier getTestIdentifier() { + return testIdentifier; + } + + @Override + public Throwable getException() { + return exception; + } + } + +} diff --git a/automation-email/src/main/java/cn/testnewbie/automation/mail/TestNewbieExtension.java b/automation-email/src/main/java/cn/testnewbie/automation/mail/TestNewbieExtension.java new file mode 100644 index 0000000000000000000000000000000000000000..a1c71532d6a8f08856f3a61225cf72a6c80d2847 --- /dev/null +++ b/automation-email/src/main/java/cn/testnewbie/automation/mail/TestNewbieExtension.java @@ -0,0 +1,23 @@ +package cn.testnewbie.automation.mail; + +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + + +/** + * @author zhanglx + */ +public class TestNewbieExtension implements BeforeAllCallback, AfterAllCallback { + + @Override + public void beforeAll(ExtensionContext context) { +// System.out.println("-------- 测试开始 --------"); + } + + @Override + public void afterAll(ExtensionContext context) { +// System.out.println("-------- 测试结束 --------"); + } + +} diff --git a/automation-email/src/main/java/cn/testnewbie/automation/mail/config/MailConfig.java b/automation-email/src/main/java/cn/testnewbie/automation/mail/config/MailConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..aabe8dd78d7add86b6b3533f302f5a51bcb53d18 --- /dev/null +++ b/automation-email/src/main/java/cn/testnewbie/automation/mail/config/MailConfig.java @@ -0,0 +1,68 @@ +package cn.testnewbie.automation.mail.config; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Properties; + + +/** + * @author zhanglx + */ +public class MailConfig { + private static final String PROPERTIES_DEFAULT = "testnewbie.properties"; + public static boolean isSendMail; + public static String host; + public static Integer port; + public static String userName; + public static String passWord; + public static String emailForm; + public static String emailTo; + public static String timeout; + public static String subject; + public static Properties properties; + + static { + init(); + } + + /** + * 初始化 + */ + private static void init() { + properties = new Properties(); + InputStreamReader inputStreamReader = null; + try { + InputStream propertiesIn = MailConfig.class.getClassLoader().getResourceAsStream(PROPERTIES_DEFAULT); + if (propertiesIn == null) { + isSendMail = false; + return; + } + inputStreamReader = new InputStreamReader( + propertiesIn, + "UTF-8"); + properties.load(inputStreamReader); + + inputStreamReader.close(); + isSendMail = Boolean.parseBoolean(properties.getProperty("isSendMail")); + if (isSendMail) { + host = properties.getProperty("mailHost"); + userName = properties.getProperty("mailUsername"); + passWord = properties.getProperty("mailPassword"); + emailForm = properties.getProperty("mailFrom"); + emailTo = properties.getProperty("mailTo"); + timeout = properties.getProperty("mailTimeout"); + subject = properties.getProperty("subject"); + + String mailPort = properties.getProperty("mailPort"); + if (mailPort == null) { + port = 25; + } else { + port = Integer.parseInt(properties.getProperty("mailPort")); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/automation-email/src/main/java/cn/testnewbie/automation/mail/config/MailHelper.java b/automation-email/src/main/java/cn/testnewbie/automation/mail/config/MailHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..9b009f180a9c95ade4a4d7b7eaf14c57f3312f9d --- /dev/null +++ b/automation-email/src/main/java/cn/testnewbie/automation/mail/config/MailHelper.java @@ -0,0 +1,75 @@ +package cn.testnewbie.automation.mail.config; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.extra.mail.MailAccount; +import cn.hutool.extra.mail.MailUtil; + +import javax.mail.MessagingException; +import javax.mail.internet.AddressException; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.List; + + +/** + * @author zhanglx + */ +public class MailHelper { + public static final boolean isSendMail = MailConfig.isSendMail; + private static final String HOST = MailConfig.host; + private static final Integer PORT = MailConfig.port; + private static final String USERNAME = MailConfig.userName; + private static final String PASSWORD = MailConfig.passWord; + private static final String EMAIL_FORM = MailConfig.emailForm; + private static final String EMAIL_TO = MailConfig.emailTo; + private static final String TIMEOUT = MailConfig.timeout; + private static final String SUBJECT = MailConfig.subject; + + private MailAccount mailSender = createMailSender(); + + /** + * 邮件发送器 + * + * @return 配置好的工具 + */ + private static MailAccount createMailSender() { + MailAccount account = new MailAccount(); + account.setHost(HOST); + account.setPort(PORT); + account.setFrom(EMAIL_FORM); + account.setUser(USERNAME); + account.setPass(PASSWORD); + account.setCharset(CharsetUtil.CHARSET_UTF_8); + account.setTimeout(Integer.parseInt(TIMEOUT)); + account.setAuth(true); + account.setDebug(true); + return account; + } + + /** + * 发送邮件 + * + * @param subject 主题 + * @param mailHtml 发送html内容 + * @throws MessagingException 异常 + * @throws UnsupportedEncodingException 异常 + */ + public void sendMail(String subject, String mailHtml) throws MessagingException, UnsupportedEncodingException { + MailUtil.send(mailSender, + getEmailTo(), + subject, + mailHtml, true); + + } + + private List getEmailTo() throws AddressException { + String[] emailArray = EMAIL_TO.split(","); + // 简单过滤 + Arrays.stream(emailArray).distinct() + .filter(str -> str.length() > 0) + .filter(str -> str.indexOf(".") > 0) + .toArray(); + return Arrays.asList(emailArray); + } + +} diff --git a/automation-email/src/main/resources/META-INF/MANIFEST.MF b/automation-email/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000000000000000000000000000000000000..4a23eeff96ca4d63f48ea2ad50930ab2f9b25cca --- /dev/null +++ b/automation-email/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Implementation-Title: Testnewbie-mail +Automatic-Module-Name: com.testnewbie.automation.mail diff --git a/automation-email/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/automation-email/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 0000000000000000000000000000000000000000..d4245d8c6634c7e37169a3237826dd7114c5d2b6 --- /dev/null +++ b/automation-email/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +cn.testnewbie.automation.mail.TestNewbieExtension diff --git a/automation-email/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener b/automation-email/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener new file mode 100644 index 0000000000000000000000000000000000000000..faed4932055e0ec9cfcd3ade2f00809eaa3a078e --- /dev/null +++ b/automation-email/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener @@ -0,0 +1 @@ +cn.testnewbie.automation.mail.EmailListener \ No newline at end of file diff --git a/automation-interface/pom.xml b/automation-interface/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..bf3367ae892ff448d015ed96957a9fca335b9420 --- /dev/null +++ b/automation-interface/pom.xml @@ -0,0 +1,36 @@ + + + + 4.0.0 + + cn.testnewbie.automation + automation-parent + 1.0.0 + + + cn.testnewbie.automation + automation-interface + 1.2.11-SNAPSHOT + + + UTF-8 + 1.8 + ${maven.compiler.source} + + + + + cn.testnewbie.automation + automation-core + 1.1.13-SNAPSHOT + + + org.junit.jupiter + junit-jupiter + 5.7.1 + + + + diff --git a/automation-interface/src/main/java/cn/testnewbie/automation/interfacetest/junit5/DBExtension.java b/automation-interface/src/main/java/cn/testnewbie/automation/interfacetest/junit5/DBExtension.java new file mode 100644 index 0000000000000000000000000000000000000000..1d36f882624ecfbcc0c46acb4e0aec43436cf1ae --- /dev/null +++ b/automation-interface/src/main/java/cn/testnewbie/automation/interfacetest/junit5/DBExtension.java @@ -0,0 +1,113 @@ + +package cn.testnewbie.automation.interfacetest.junit5; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.*; + +import java.util.Optional; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +/** + * @author zhanglx + */ +@API(status = EXPERIMENTAL, since = "1.0") +public class DBExtension implements + BeforeAllCallback, + AfterAllCallback, + TestInstancePostProcessor, + BeforeEachCallback, + AfterEachCallback, + BeforeTestExecutionCallback, + AfterTestExecutionCallback, TestWatcher, ParameterResolver { + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + System.out.println("222beforeAll context = " + context); + +// context. + } + + @Override + public void afterAll(ExtensionContext context) throws Exception { + System.out.println("222afterAll context = " + context); + } + + /** + * TestInstancePostProcessor定义了用于发布流程测试实例的`Extensions`的API。 + * 常见的用例包括将依赖注入到测试实例中,在测试实例上调用自定义的初始化方法等等。 + * 对于具体的例子,请参考[`MockitoExtension`]和[`SpringExtension`]的源代码。 + * @param testInstance + * @param context + * @throws Exception + */ + @Override + public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { + System.out.println("222postProcessTestInstance testInstance = " + testInstance + ", context = " + context); + + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + System.out.println("222beforeEach context = " + context); +// Optional testMethod = context.getTestMethod(); +// boolean annotated = AnnotationSupport.isAnnotated(testMethod, LoginWith.class); +// System.out.println("annotated = " + annotated); + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + System.out.println("222afterEach context = " + context); + } + + @Override + public void afterTestExecution(ExtensionContext context) throws Exception { + System.out.println("222afterTestExecution context = " + context); + + } + + @Override + public void beforeTestExecution(ExtensionContext context) throws Exception { + System.out.println("222beforeTestExecution context = " + context); + + } + + @Override + public void testSuccessful(ExtensionContext context) { + System.out.println("testSuccessful context = " + context); + } + + @Override + public void testFailed(ExtensionContext context, Throwable cause) { + System.out.println("222testFailed context = " + context); + System.out.println("222testFailed Throwable = " + cause); + + } + + + @Override + public void testDisabled(ExtensionContext context, Optional reason) { + System.out.println("222testDisabled context = " + context); + System.out.println("222testDisabled reason = " + reason.orElse("no reason")); + } + + @Override + public void testAborted(ExtensionContext context, Throwable cause) { + System.out.println("222testAborted context = " + context); + System.out.println("222testAborted Throwable = " + cause); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + System.out.println("222supportsParameter ParameterContext = " + parameterContext); + System.out.println("222supportsParameter ExtensionContext = " + extensionContext); + return false; + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + System.out.println("222resolveParameter ParameterContext = " + parameterContext); + System.out.println("222resolveParameter ExtensionContext = " + extensionContext); + return null; + } +} diff --git a/automation-interface/src/main/java/cn/testnewbie/automation/interfacetest/junit5/Junit5Extension.java b/automation-interface/src/main/java/cn/testnewbie/automation/interfacetest/junit5/Junit5Extension.java new file mode 100644 index 0000000000000000000000000000000000000000..b9d216720c4793c7c53f89a95739e7c24d37a84a --- /dev/null +++ b/automation-interface/src/main/java/cn/testnewbie/automation/interfacetest/junit5/Junit5Extension.java @@ -0,0 +1,97 @@ + +package cn.testnewbie.automation.interfacetest.junit5; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.*; + +import java.util.Optional; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +@API(status = EXPERIMENTAL, since = "1.0") +public class Junit5Extension implements + BeforeAllCallback, + AfterAllCallback, + TestInstancePostProcessor, + BeforeEachCallback, + AfterEachCallback, + BeforeTestExecutionCallback, + AfterTestExecutionCallback, TestWatcher, ParameterResolver { + + @Override + public void beforeAll(ExtensionContext context) throws Exception { +// System.out.println("beforeAll context = " + context); + } + + @Override + public void afterAll(ExtensionContext context) throws Exception { +// System.out.println("afterAll context = " + context); + } + + @Override + public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { +// System.out.println("postProcessTestInstance testInstance = " + testInstance + ", context = " + context); + + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { +// System.out.println("beforeEach context = " + context); + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { +// System.out.println("afterEach context = " + context); + } + + @Override + public void afterTestExecution(ExtensionContext context) throws Exception { +// System.out.println("afterTestExecution context = " + context); + + } + + @Override + public void beforeTestExecution(ExtensionContext context) throws Exception { +// System.out.println("beforeTestExecution context = " + context); + + } + + @Override + public void testSuccessful(ExtensionContext context) { +// System.out.println("testSuccessful context = " + context); + } + + @Override + public void testFailed(ExtensionContext context, Throwable cause) { +// System.out.println("testFailed context = " + context); +// System.out.println("testFailed Throwable = " + cause); + + } + + + @Override + public void testDisabled(ExtensionContext context, Optional reason) { +// System.out.println("testDisabled context = " + context); + System.out.println("testDisabled reason = " + reason.orElse("no reason")); + } + + @Override + public void testAborted(ExtensionContext context, Throwable cause) { +// System.out.println("testAborted context = " + context); +// System.out.println("testAborted Throwable = " + cause); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { +// System.out.println("supportsParameter ParameterContext = " + parameterContext); +// System.out.println("supportsParameter ExtensionContext = " + extensionContext); + return false; + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { +// System.out.println("resolveParameter ParameterContext = " + parameterContext); +// System.out.println("resolveParameter ExtensionContext = " + extensionContext); + return null; + } +} diff --git a/automation-interface/src/main/java/cn/testnewbie/automation/interfacetest/step/HttpStep.java b/automation-interface/src/main/java/cn/testnewbie/automation/interfacetest/step/HttpStep.java new file mode 100644 index 0000000000000000000000000000000000000000..147df92066dc8532d703feb68403d20fc06e985f --- /dev/null +++ b/automation-interface/src/main/java/cn/testnewbie/automation/interfacetest/step/HttpStep.java @@ -0,0 +1,100 @@ +package cn.testnewbie.automation.interfacetest.step; + +import cn.hutool.http.Header; +import cn.hutool.http.HttpRequest; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.core.annotation.TimeCost; +import io.qameta.allure.Step; +import org.apiguardian.api.API; + +import java.util.HashMap; + +import static org.apiguardian.api.API.Status.STABLE; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * http请求封装 + * + * @author zhanglx + */ +@TimeCost +@API(status = STABLE, since = "1.0") +public class HttpStep { + private static final Log log = LogFactory.get(); + + /*** http请求的请求头信息,可以放入token\cookie等 ***/ + public HashMap headerMap = new HashMap<>(); + + /** + * 通过给定的url,和参数,请求并给出请求结果 + * + * @param url 接口地址 + * @param params 表单内容 + * @return + * @throws Exception + */ + @Step("发送http get请求:'{url}' '{params}'") + public String get(String url, HashMap params) { + log.debug("请求地址:{}", url); + log.debug("请求参数:{}", params.toString()); + log.debug("请求头 :{}", headerMap == null ? "null" : headerMap.toString()); + + return HttpRequest.get(url) + .header(Header.USER_AGENT, "Hutool http").form(params).addHeaders(headerMap) + .execute().body(); + } + + /** + * 通过给定的url,和参数,请求并给出请求结果 + * + * @param url 接口地址 + * @param body body内容,需要传入一个JSON或者XML字符串,Hutool会自动绑定其对应的Content-Type + * @return + */ + @Step("发送http post请求") + public String post(String url, String body) { + String httpBody = body.replaceAll("\r|\n", ""); + log.debug("请求地址:{}", url); + log.debug("请求body:{}", httpBody); + log.debug("请求头:{}", headerMap == null ? "未设置" : headerMap.toString()); + + String httpResponse = HttpRequest.post(url) + .header(Header.USER_AGENT, "Hutool http").body(httpBody).addHeaders(headerMap) + .execute().body(); + log.debug("请求结果:msg = {}", httpResponse); + return httpResponse; + } + + /** + * 通过给定的url,和参数,请求并给出请求结果 + * + * @param url 接口地址 + * @param params 表单内容 + * @return + * @throws Exception + */ + @Step("发送http post请求:'{url}' '{params}'") + public String post(String url, HashMap params) { + log.debug("请求地址:{}", url); + log.debug("请求params:{}", params.toString()); + log.debug("请求头:{}", headerMap == null ? "未设置" : headerMap.toString()); + + return HttpRequest.post(url) + .header(Header.USER_AGENT, "Hutool http").form(params).addHeaders(headerMap) + .execute().body(); + } + + /** + * 验证http接口返回的内容 + * + * @param except + * @param httpResponse + */ + @Step("检验返回值是否含有:'{except}'") + public void checkHttpResponse(String except, String httpResponse) { + assertTrue(httpResponse.contains(except)); + } + + +} diff --git a/automation-ui/pom.xml b/automation-ui/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..ba80e945d90ec539e22e585b93da65d27648d2fe --- /dev/null +++ b/automation-ui/pom.xml @@ -0,0 +1,72 @@ + + + + 4.0.0 + + cn.testnewbie.automation + automation-parent + 1.0.0 + + + cn.testnewbie.automation + automation-ui + 1.0.2-SNAPSHOT + + + + cn.testnewbie.automation + automation-core + 1.1.13-SNAPSHOT + + + org.junit.jupiter + junit-jupiter + 5.7.1 + + + com.sikulix + sikulixapi + 2.0.4 + + + log4j + log4j + + + log4j-over-slf4j + org.slf4j + + + commons-logging + commons-logging + + + jboss-logging + org.jboss.logging + + + + + + org.seleniumhq.selenium + selenium-java + 3.141.59 + + + + + + + + + + + + + + + + + diff --git a/automation-ui/src/main/java/cn/testnewbie/automation/ui/SikuliXExecutor.java b/automation-ui/src/main/java/cn/testnewbie/automation/ui/SikuliXExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..7f89fa6bc3074b4d020ff3a92010b7abb78ed2e9 --- /dev/null +++ b/automation-ui/src/main/java/cn/testnewbie/automation/ui/SikuliXExecutor.java @@ -0,0 +1,545 @@ +package cn.testnewbie.automation.ui; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.ui.util.SikulixScreenShot; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.sikuli.basics.Debug; +import org.sikuli.script.Button; +import org.sikuli.script.*; + +import java.awt.*; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.util.concurrent.TimeUnit; + +/** + * sikulix工具类 + * + * @author zhanglixing + */ +public class SikuliXExecutor { + private static final Log log = LogFactory.get(); + /** + * 是否自动截图,默认关闭。 + * 一般情况下,建议selenium和sikulix混合使用(selenium执行速度快,api复杂;sikulix执行速度慢,api简单) + * 如果 saveScreenShotWhenFail = true,只会截取sikulix的图。所以只在纯sikulix项目中才建议 = true + * 混合项目,建议用try{}catch{} 中统一截图 + */ + static boolean saveScreenShotWhenFail = false; + + /** + * 默认模糊查找精度,识别率较低时适当调低以提高识别率 + */ + static float similarNum = 0.75f; + + public static Screen screen = new Screen(); + + static { + // 设置超时等待时间,sikuli查找时找不到,默认等待时间,1d表示1秒; + screen.setAutoWaitTimeout(0.8d); + // Settings.MoveMouseDelay = 0.25f;// 鼠标移动速度 + // Settings.setShowActions(true);// 鼠标移动速度生效 + // 日志级别 + Debug.on(0); + + // Debug.info("Screen: %s", s); + // String clazz = "testAPI.Test"; + // String imgFolder = "/imgs"; + // String img = "test.png"; + // String inJarFolder = clazz + imgFolder; + // if (ImagePath.add(inJarFolder)) { + // Debug.info("Image Folder in jar at: %s", inJarFolder); + // } else { + // Debug.error("Image Folder in jar not possible: %s", inJarFolder); + // } + // Match target = s.exists(img); + + //sikulix1.1.4特性 + //https://sikulix-2014.readthedocs.io/en/latest/scripting.html#writing-and-redirecting-log-and-debug-messages + // 0和1是关闭,其他数值是缩小为原图的0.xx倍 + //Settings.AlwaysResize = 1; + //备选方案1如果您不需要全部,但仅针对某些图像,则该类Pattern具有一项功能,仅在搜索时缩放给定图像: + //Pattern.resize(); + //备选方案2如果您需要对图像进行更具体的过滤和/或其他修改,则可以使用全局回调功能,如果设置,则允许您返回给定图像的修改版本以进行搜索: + //Settings.ImageCallback; + } + + /** + * 等待当前页面加载完,出现图片后点击。需要截图 + * + * @param imgName 图片名称 + * @throws FindFailed 图片查找失败 + */ + public static void clickImg(String imgName) throws FindFailed { + clickImg(imgName, similarNum, 0, 0); + } + + /** + * 等待当前页面加载完,出现图片后点击。需要截图 + * + * @param imgName 图片名称 + * @param imgSimilar 图片相似度。默认0.85,用默认值传null。 + * @param xOffset x轴偏移,负数向左,正数向右,点击位置水平偏移量,向右移动为正数 + * @param yOffset y轴偏移,负数向上,正数向下,点击位置垂直偏移量,向下移动为正数 + * @throws FindFailed 图片查找失败 + */ + public static void clickImg(String imgName, float imgSimilar, int xOffset, int yOffset) throws FindFailed { + clickImg(imgName, imgSimilar, xOffset, yOffset, false, 5); + } + + /** + * 等待当前页面加载完,出现图片后点击。需要截图 + * + * @param imgName 图片名称 + * @param imgSimilar 图片相似度。默认0.85,用默认值传null。 + * @param xOffset x轴偏移,负数向左,正数向右 点击位置水平偏移量,向右移动为正数 + * @param yOffset y轴偏移,负数向上,正数向下 点击位置垂直偏移量,向下移动为正数 + * @param allowSkip 找不到图片是否可以跳过该步骤,true 可以跳过,false,不可以跳过,执行失败 + * @param waitSecond 等待秒数。 + * @throws FindFailed 图片查找失败 + */ + public static void clickImg(String imgName, float imgSimilar, int xOffset, int yOffset, Boolean allowSkip, + int waitSecond) throws FindFailed { + + // 查找图片并点击 + Region region = findImgOnTheScreen(imgName, imgSimilar, xOffset, yOffset, allowSkip, waitSecond); + if (region != null) { + region.click(); + log.info("click image name : {}", imgName); + } else { + imgNotFind(imgName, allowSkip); + } + } + + + /** + * 等待当前页面加载完,判断是否出现图片 + * + * @param imgName 图片名称 + * @param imgSimilar 图片相似度。默认0.85,用默认值传null。 + * @param waitSecond 等待秒数。 + */ + public static boolean exist(String imgName, float imgSimilar, int waitSecond) { + // 查找图片是否存在 + Region region = findImgOnTheScreen(imgName, imgSimilar, 0, 0, true, waitSecond); + if (region != null) { + log.info("匹配度={},图片:{} 存在", imgSimilar, imgName); + return true; + } else { + log.info("匹配度={},图片:{} 不存在", imgSimilar, imgName); + return false; + } + } + + /** + * 调用sikulix原生方法查找 + */ + private static Region findImgOnTheScreen(String imgName, float imgSimilar, int xOffset, int yOffset, + boolean allowSkip, int waitSecond) { + checkImgName(imgName); + log.info("匹配度={},开始查找图片:{}", imgSimilar, imgName); + + Pattern pattern = new Pattern(); + try { + // 耗时350毫秒左右 + return screen.wait(pattern.setFilename(imgName).similar(imgSimilar), waitSecond).offset(xOffset, yOffset); + } catch (FindFailed e1) { + return null; + } + } + + + /** + * 等待当前页面加载完,出现图片后双击。 + * + * @param imgName 图片名称 + * @param imgSimilar 图片相似度。默认0.85,用默认值传null。 + * @param xOffset x轴偏移,负数向左,正数向右 点击位置水平偏移量,向右移动为正数 + * @param yOffset y轴偏移,负数向上,正数向下 点击位置垂直偏移量,向下移动为正数 + * @param allowSkip 找不到图片是否可以跳过该步骤,true 可以跳过,false,不可以跳过,执行失败 + * @param waitSecond 等待秒数。 + * @throws FindFailed 图片查找失败 + */ + public static void doubleClickImg(String imgName, float imgSimilar, int xOffset, int yOffset, + Boolean allowSkip, int waitSecond) throws FindFailed { + // 查找图片并点击 + Region region = findImgOnTheScreen(imgName, imgSimilar, xOffset, yOffset, allowSkip, waitSecond); + if (region != null) { + region.doubleClick(); + } else { + imgNotFind(imgName, allowSkip); + } + + } + + /** + * 等待页面加载完后查找图片 + * + * @param imgName 图片名称 + * @param imgSimilar 图片相似度 + * @param allowSkip 找不到图片是否可以跳过该步骤,true 可以跳过,false,不可以跳过,执行失败 + * @param waitSecond 等待秒数 + * @throws FindFailed 图片查找失败 + */ + public static void waitImg(String imgName, float imgSimilar, Boolean allowSkip, int waitSecond) throws FindFailed { + sleep(400); + + // 查找图片并点击 + Region region = findImgOnTheScreen(imgName, imgSimilar, 0, 0, allowSkip, waitSecond); + if (region != null) { + log.info("wait image name : {}", imgName); + } else { + imgNotFind(imgName, allowSkip); + } + + } + + + /** + * 双击页面,复制所选文案到粘贴板 + * + * @param imgName 图片名称 + * @param imgSimilar 图片相似度 + * @param xOffset x轴偏移,负数向左,正数向右 + * @param yOffset y轴偏移,负数向上,正数向下 + * @param allowSkip 找不到图片是否可以跳过该步骤,true 可以跳过,false,不可以跳过,执行失败 + * @param waitSecond 等待秒数 + * @return 页面双击后选中的文案 + * @throws FindFailed 图片查找失败 + */ + public static String doubleClickGetInfoIntoClipboard(String imgName, float imgSimilar, int xOffset, + int yOffset, Boolean allowSkip, int waitSecond) throws FindFailed { + + // 查找图片并点击 + Region region = findImgOnTheScreen(imgName, imgSimilar, xOffset, yOffset, allowSkip, waitSecond); + if (region != null) { + region.doubleClick(); + screen.keyDown(Key.CTRL); + screen.type("c"); + screen.keyUp(Key.CTRL); + String xx = App.getClipboard().trim(); + + log.info("从页面获取数据 String from web : {}", xx); + + return xx; + } else { + imgNotFind(imgName, allowSkip); + } + return ""; + } + + /** + * 等待当前页面加载完,出现图片后点击。需要截图 + * + * @param imgName 图片名称 + * @param imgSimilar 图片相似度。默认0.85,用默认值传null。 + * @param waitSecond 出现图片前等待秒数。 + * @param count 闪动次数 + * @throws FindFailed 图片查找失败 + */ + public static void highlight(String imgName, float imgSimilar, int waitSecond, int count) throws FindFailed { + + Region region = findImgOnTheScreen(imgName, imgSimilar, 0, 0, false, waitSecond); + for (int i = 0; i < count; i++) { + if (region != null) { + // 1为闪动的时间间隔,单位秒 + region.highlight(1); + log.info("highlight image name : {}", imgName); + } else { + imgNotFind(imgName, false); + } + } + + } + + /** + * 等待当前页面加载完,出现图片后鼠标滑到指定位置。需要截图 + * + * @param imgName 图片名称 + * @param waitSecond 等待秒数。 + * @param imgSimilar 图片相似度。默认0.85,用默认值传null。 + * @throws FindFailed 图片查找失败 + */ + public static void mouseOver(String imgName, float imgSimilar, int waitSecond) throws FindFailed { + mouseOver(imgName, imgSimilar, 0, 0, true, waitSecond); + } + + /** + * 等待当前页面加载完,出现图片后鼠标滑到指定位置。需要截图 + * + * @param imgName 图片名称 + * @param waitSecond 等待秒数。 + * @param imgSimilar 图片相似度。默认0.85,用默认值传null。 + * @param allowSkip 找不到图片是否可以跳过该步骤,true 可以跳过,false,不可以跳过,执行失败 + * @param xOffset x轴偏移,负数向左,正数向右 点击位置水平偏移量,向右移动为正数 + * @param yOffset y轴偏移,负数向上,正数向下 点击位置垂直偏移量,向下移动为正数 + * @throws FindFailed 图片查找失败 + */ + public static void mouseOver(String imgName, float imgSimilar, int xOffset, int yOffset, Boolean allowSkip, + int waitSecond) throws FindFailed { + + Region region = findImgOnTheScreen(imgName, imgSimilar, xOffset, yOffset, allowSkip, waitSecond); + if (region != null) { + region.mouseMove(region); + } else { + imgNotFind(imgName, allowSkip); + } + + } + + /** + * 弹框 + * + * @param content 弹框内容 + */ + public static void popup(String content) { + Sikulix.popup(content); + } + + private static void checkImgName(String imgName) { + if (StrUtil.isEmpty(imgName)) { + log.error("imgName 不能为null"); + } + } + + /** + * @param imgName 图片名称 + * @param allowSkip 找不到图片是否可以跳过该步骤,true 可以跳过,false,不可以跳过,执行失败 + * @throws FindFailed 图片查找失败 + */ + + private static void imgNotFind(String imgName, boolean allowSkip) throws FindFailed { + if (saveScreenShotWhenFail) { + SikulixScreenShot.screenShot(screen, System.getProperty("user.dir")); + } + + if (!allowSkip) { + throw new FindFailed("找不到匹配内容: " + imgName); + } else { + log.warn("找不到匹配内容: {}", imgName); + } + } + + /** + * 清除输入框中已输入的value值。 + */ + public static void deleteInputedValue() { + screen.keyDown(Key.CTRL); + screen.type("a"); + screen.keyUp(Key.CTRL); + screen.type(Key.DELETE); + log.info("delete input value"); + } + + /** + * 清除输入框中已输入的value值,并输入新数据。 + */ + public static void replaceInputValue(String newValue) { + deleteInputedValue(); + inputValue(newValue); + log.info("replace input value {}", newValue); + } + + /** + * 往输入框中设值。 + * + * @param inputValue 输入内容 + */ + public static void inputValue(String inputValue) { + if (StrUtil.isEmpty(inputValue)) { + log.info("inputValue value is empty"); + return; + } + + Clipboard clipboard; + clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + StringSelection text = new StringSelection(inputValue); + clipboard.setContents(text, null); + + screen.keyDown(Key.CTRL); + screen.type("v"); + screen.keyUp(Key.CTRL); + + log.info("input value : {}", inputValue); + sleep(100); + } + + /** + * 复制已选中的文案到粘贴板 + * + * @return String + */ + public static String getInfoIntoClipboard() { + screen.keyDown(Key.CTRL); + screen.type("c"); + screen.keyUp(Key.CTRL); + String xx = App.getClipboard().trim(); + + log.info("复制已选中的文案到粘贴板 : {}", xx); + + return xx; + } + + /** + * 程序暂停x毫秒 + * + * @param msec 等待毫秒数 + */ + public static void sleep(int msec) { + try { + TimeUnit.MICROSECONDS.sleep(msec); + } catch (InterruptedException e) { + log.error("暂停被打断", e); + Thread.currentThread().interrupt(); + } + log.debug("暂停 : {} 毫秒", msec); + } + + /** + * 关闭当前页面 + */ + public static void closeCurrentPage() { + screen.keyDown(Key.ALT); + screen.keyDown(Key.F4); + + screen.keyUp(Key.F4); + screen.keyUp(Key.ALT); + sleep(1000); + } + + /** + * 模拟滚轴滚动页面向下 + * + * @param wheelNum 鼠标滚轮向下滚动次数,一般的鼠标,滚一次,移动三格;即传1进来会滚3个三格 + * @throws FindFailed 图片查找失败 + */ + public static void wheelDown(int wheelNum) throws FindFailed { + // wheelNum = wheelNum * 3; + + screen.wheel(screen, Button.WHEEL_DOWN, wheelNum); + sleep(1000); + } + + /** + * 模拟滚轴滚动页面向上 + * + * @param wheelNum 滚轴向上滚动圈数 + * @throws FindFailed 图片查找失败 + */ + public static void wheelUp(int wheelNum) throws FindFailed { + // wheelNum = wheelNum * 3; + + screen.wheel(screen, Button.WHEEL_UP, wheelNum); + sleep(1000); + } + + /** + * 模拟页面pageDown + * + * @param downNum 按键次数 + */ + public static void pageDown(int downNum) { + + for (int i = 1; i <= downNum; i++) { + screen.type(Key.PAGE_DOWN); + sleep(400); + } + } + + /** + * 模拟页面pageUp + * + * @param upNum 按键次数 + */ + public static void pageUp(int upNum) { + for (int i = 1; i <= upNum; i++) { + screen.type(Key.PAGE_UP); + sleep(200); + } + } + + /** + * 点tab键n次 + * + * @param clickNum 按键次数 + */ + public static void clickTab(int clickNum) { + for (int i = 1; i <= clickNum; i++) { + screen.type(Key.TAB); + sleep(200); + } + } + + /** + * 最大化软件界面 + * + * @param s Screen + */ + public static void maxwindow(Screen s) { + s.keyDown(Key.WIN); + s.type(Key.UP); + s.keyUp(Key.WIN); + } + + /** + * 按键 + * + * @param keyType 按键类型,对象为Key + */ + public static void keyPress(String keyType) { + keyPress(keyType, 1); + } + + + /** + * 按键 + * + * @param keyType 按键类型,对象为Key + * @param count 次数 + */ + public static void keyPress(String keyType, int count) { + sleep(400); + + if (StrUtil.isEmpty(keyType)) { + log.error("keyType is null , when keyPress"); + return; + } + + for (int i = 0; i < count; i++) { + screen.type(keyType); + sleep(200); + } + sleep(400); + + } + + /** + * 点击链接 + * + * @param driver WebDriver + * @param linkText linkText + */ + public static void clickLinkText(WebDriver driver, String linkText) { + int count = 0; + while (true) { + WebElement element = driver.findElement(By.linkText(linkText)); + if (element != null) { + element.click(); + log.info("click {}", linkText); + break; + } else if (count > 10) { + log.error("The {} not find: ", linkText); + break; + } else { + sleep(2000); + count++; + } + } + + } + +} diff --git a/automation-ui/src/main/java/cn/testnewbie/automation/ui/UIBase.java b/automation-ui/src/main/java/cn/testnewbie/automation/ui/UIBase.java new file mode 100644 index 0000000000000000000000000000000000000000..6151f503c1c4af70f86951bbcbd9917ecabe036c --- /dev/null +++ b/automation-ui/src/main/java/cn/testnewbie/automation/ui/UIBase.java @@ -0,0 +1,102 @@ +package cn.testnewbie.automation.ui; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import io.qameta.allure.Step; +import org.openqa.selenium.WebDriver; +import org.sikuli.script.ImagePath; + + +/** + * UI自动化基类 + * + * @author zhanglixing + */ +public class UIBase { + private static final Log log = LogFactory.get(); + + public static WebDriver driver; + + static { + ImagePath.add(System.getProperty("user.dir")); + ImagePath.add("com.testnewbie.automation.ui.UIBase/browserStatus/"); + } + + /** + * UI自动化用例执行结束,关闭webdriver浏览器 + */ + @Step + public void quit() { + log.info("用例执行结束,关闭浏览器"); + driver.quit();// 关闭浏览器 + } + +} + + +// private String lastLoginUser = ""; +// +// @Given +// public void loginWeb(final @Named("username") String username, final @Named("password") String password) { +// +// // 已登录的直接拿登录后的session_key +// if (lastLoginUser.equals(username)) { +// logger.info("已登录的用户:username=" + username); +// return; +// } +// +// String userdir = System.getProperty("user.dir"); +// logger.info("用户的当前工作目录:" + userdir); +// System.setProperty("webdriver.chrome.driver", userdir + "/webdriver/chromedriver.exe"); +// +// ChromeOptions options = new ChromeOptions(); +// options.addArguments("--disk-cache-dir=" + System.getProperty("user.dir") + "/cache", "--start-maximized"); +// driver = new ChromeDriver(options);// 请安装google浏览器并升级至最新版本,和chromedriver.exe配套。 +// +// // 鼠标移动速度 +// Settings.MoveMouseDelay = 0.25f; +// Settings.setShowActions(true); +// +//// driver.manage().window().maximize(); +// +// try { +// driver.get("http://127.0.0.1:8080/#/login");// 浏览器输入地址 +// +// //登录方式1 +// SikuliXExecutor.clickImg("登录页面.png", 0.65f, 0, 0, false, 10); +// SikuliXExecutor.keyPress(Key.TAB); +// SikuliXExecutor.inputValue(username); +// SikuliXExecutor.keyPress(Key.TAB); +// SikuliXExecutor.inputValue(password); +// SikuliXExecutor.clickImg("登录按钮.png", 0.75f, 0, 0, false, 10); +// +// SikuliXExecutor.sleep(2000); +// SikuliXExecutor.waitImg("chrome网页加载完成.png", 0.70f, false, 10);// 等浏览器加载 +// +// SikuliXExecutor.waitImg("登录成功.png", 0.70f, false, 20);// 等浏览器加载 +// } catch (Exception e) { +// logger.error("登录成功.png找不到,判断为登录失败!", e); +// want.fail("登录成功.png找不到,判断为登录失败!"); +// } +// lastLoginUser = username; +// logger.info("登录完成。"); +// } + + +// /** +// * 退出登录,关闭浏览器 +// */ +// @AfterSuite +// public void logoutWeb() { +// try { +// if (driver != null) { +// //登出 +// SikuliXExecutor.clickImg("登出按钮.png", 0.75f, 0, 0, false, 5); +// SikuliXExecutor.sleep(2000); +// } +// } catch (Exception e) { +// e.printStackTrace(); +// } finally { +// driver.quit(); +// } +// } diff --git a/automation-ui/src/main/java/cn/testnewbie/automation/ui/util/ScreenCaptureVideo.java b/automation-ui/src/main/java/cn/testnewbie/automation/ui/util/ScreenCaptureVideo.java new file mode 100644 index 0000000000000000000000000000000000000000..91e2d50efaedc414539b2578878b3c39ba76adbc --- /dev/null +++ b/automation-ui/src/main/java/cn/testnewbie/automation/ui/util/ScreenCaptureVideo.java @@ -0,0 +1,86 @@ +package cn.testnewbie.automation.ui.util; + +import java.io.File; +import java.io.IOException; + +/** + * 此方法的调用,前提需要安装ffmpeg和Screen Capturer Recorder, + * 此方法在安装ffmpeg-20150519-git-d0ac2f5-win64-static.7z和Setup Screen Capturer + * Recorder v0.12.8 (2).exe后验证正确 + * + * @author zlx + */ +public class ScreenCaptureVideo { + private static String cmd = null; + private static Process proc = null; + + /** + * @param path =D:\\xxxx\\yyyy.flv + * @throws InterruptedException + */ + public static void start(String path) throws InterruptedException { + if (path.isEmpty() || path == null) { + File file = new File("D:\\output.flv"); + if (file.exists()) { + boolean d = file.delete(); + if (d) { + System.out.println("删除成功!"); + } else { + System.out.println("删除失败!"); + } + } + + cmd = "cmd /c start /min ffmpeg -f dshow -i video=\"screen-capture-recorder\" D:\\output.flv "; + } else { + File file = new File(path); + if (file.exists()) { + boolean d = file.delete(); + if (d) { + System.out.println("删除成功!"); + } else { + System.out.println("删除失败!"); + } + } + cmd = "cmd /c start /min ffmpeg -f dshow -i video=\"screen-capture-recorder\" " + path + " "; + } + + try { + proc = Runtime.getRuntime().exec(cmd); + System.out.println("===ScreenCapturereCorderToVideo:start==="); + proc.waitFor(); + int i = proc.exitValue(); + if (i == 0) { + System.out.println("执行完成."); + } else { + System.out.println("执行失败."); + } + proc.destroy(); + proc = null; + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + public static void stop() throws InterruptedException { + + cmd = "cmd /c start /min taskkill /f /IM ffmpeg.exe"; + try { + proc = Runtime.getRuntime().exec(cmd); + System.out.println("===ScreenCapturereCorderToVideo:stop==="); + proc.waitFor(); + int i = proc.exitValue(); + if (i == 0) { + System.out.println("执行完成."); + } else { + System.out.println("执行失败."); + } + proc.destroy(); + proc = null; + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + +} diff --git a/automation-ui/src/main/java/cn/testnewbie/automation/ui/util/SeleniumScreenShot.java b/automation-ui/src/main/java/cn/testnewbie/automation/ui/util/SeleniumScreenShot.java new file mode 100644 index 0000000000000000000000000000000000000000..ef310ea10b6a9ee48e07ab9e10fcd5c930bdab7e --- /dev/null +++ b/automation-ui/src/main/java/cn/testnewbie/automation/ui/util/SeleniumScreenShot.java @@ -0,0 +1,33 @@ +package cn.testnewbie.automation.ui.util; + +import cn.hutool.core.io.FileUtil; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * 截图工具类 + * + * @author zhanglixing + */ +public class SeleniumScreenShot { + + public static int t = 1; + + public static String getDateTime() { + SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd_HHmmss"); + return df.format(new Date()); + } + + public static void screenShot(WebDriver dr, String dir) { + File screenShot = ((TakesScreenshot) dr).getScreenshotAs(OutputType.FILE); + FileUtil.copyFile(screenShot, new File(dir + "/logs/se" + getDateTime() + "_" + t + ".png")); + ++t; + } + + +} diff --git a/automation-ui/src/main/java/cn/testnewbie/automation/ui/util/SikulixScreenShot.java b/automation-ui/src/main/java/cn/testnewbie/automation/ui/util/SikulixScreenShot.java new file mode 100644 index 0000000000000000000000000000000000000000..064351e2fd36c116c4c2d2d7398d7d32df0e4492 --- /dev/null +++ b/automation-ui/src/main/java/cn/testnewbie/automation/ui/util/SikulixScreenShot.java @@ -0,0 +1,67 @@ +package cn.testnewbie.automation.ui.util; + +import org.sikuli.script.Screen; +import org.sikuli.script.ScreenImage; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * 截图工具类 + * + * @author zhanglixing + */ +public class SikulixScreenShot { + + public static int t = 1; + public static String defaultDir = System.getProperty("user.dir"); + + public static String getDateTime() { + SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd_HHmmss"); + return df.format(new Date()); + } + + public static void screenShot() { + screenShot(new Screen()); + } + + public static void screenShot(Screen screen) { + screenShot(screen, defaultDir); + } + + public static void screenShot(Screen screen, String dir) { + try { + ScreenImage simg = screen.getScreen().capture(); + BufferedImage image = simg.getImage(); + File file = new File(dir + "/target/si" + getDateTime() + "_" + t + ".png"); + file.mkdirs(); + ImageIO.write(image, "png", file); + ++t; + } catch (IOException e) { + e.printStackTrace(); + } + } + +// /** +// * 未开发完成 +// * +// * @param s +// * @param dir +// */ +// public static void screenShot(ADBScreen s, String dir) { +// try { +// ScreenImage simg = s.getScreen().capture(); +// BufferedImage image = simg.getImage(); +// ImageIO.write(image, "png", new File(dir + "/logs/si" + getDateTime() + "_" + t + ".jpg")); +// ++t; +// } catch (IOException e) { +// e.printStackTrace(); +// } +// +// } + +} diff --git a/automation-ui/src/main/java/cn/testnewbie/automation/ui/util/VideoRecorder.java b/automation-ui/src/main/java/cn/testnewbie/automation/ui/util/VideoRecorder.java new file mode 100644 index 0000000000000000000000000000000000000000..84163acba6310c6c17562e38d8d31b179a94b64e --- /dev/null +++ b/automation-ui/src/main/java/cn/testnewbie/automation/ui/util/VideoRecorder.java @@ -0,0 +1,195 @@ +package cn.testnewbie.automation.ui.util; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +//import org.bytedeco.ffmpeg.global.avcodec; +//import org.bytedeco.ffmpeg.global.avutil; +//import org.bytedeco.javacv.FFmpegFrameRecorder; +//import org.bytedeco.javacv.OpenCVFrameConverter; +//import org.bytedeco.opencv.opencv_core.IplImage; + +import javax.imageio.ImageIO; +import javax.sound.sampled.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ShortBuffer; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +//import static org.bytedeco.opencv.helper.opencv_imgcodecs.cvLoadImage; + +/** + * https://blog.csdn.net/ccystewart/article/details/105286289 + */ +public class VideoRecorder { + /** + private static final Log logger = LogFactory.get(); + + private ScheduledThreadPoolExecutor screenTimer; + private Rectangle rectangle; + private FFmpegFrameRecorder recorder; + private Robot robot; + private OpenCVFrameConverter.ToIplImage conveter; + private BufferedImage screenCapture; + private ScheduledThreadPoolExecutor exec; + private TargetDataLine line; + private AudioFormat audioFormat; + private DataLine.Info dataLineInfo; + private boolean isHaveDevice; + private String fileName; + private long startTime = 0; + private long videoTS = 0; + private long pauseTime = 0; + private double frameRate = 5; + + //构造器 + public VideoRecorder(String fileName, boolean isHaveDevice) { + init(fileName, isHaveDevice); + } + + void init(String fileName, boolean isHaveDevice) { + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + rectangle = new Rectangle(screenSize.width, screenSize.height); + recorder = new FFmpegFrameRecorder(fileName + ".mp4", screenSize.width, screenSize.height); + recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264); + logger.info("设置视频格式:h264"); + recorder.setFormat("mp4"); + recorder.setSampleRate(44100); + recorder.setFrameRate(frameRate); + + recorder.setVideoQuality(0); + recorder.setVideoOption("crf", "23"); + recorder.setVideoBitrate(1000000); + recorder.setVideoOption("preset", "slow"); + recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); + recorder.setAudioChannels(2); + recorder.setAudioOption("crf", "0"); + recorder.setAudioQuality(0); +// recorder.setAudioCodec(avcodec.AV_CODEC_ID_IMC); + try { + robot = new Robot(); + } catch (AWTException e) { + logger.error("AWTException : ", e); + } + try { + recorder.start(); + } catch (Exception e) { + logger.error(" 录制视频初始化异常 : ", e); + } catch (Error error) { + logger.error("录制视频初始化错误 : ", error); + } + conveter = new OpenCVFrameConverter.ToIplImage(); + this.isHaveDevice = isHaveDevice; + this.fileName = fileName; + } + + public void start() { + + logger.info("录屏开始!start"); + if (startTime == 0) { + startTime = System.currentTimeMillis(); + } + if (pauseTime == 0) { + pauseTime = System.currentTimeMillis(); + } + + if (isHaveDevice) { + new Thread(() -> caputre()).start(); + } + screenTimer = new ScheduledThreadPoolExecutor(1); + screenTimer.scheduleAtFixedRate(() -> { + try { + screenCapture = robot.createScreenCapture(rectangle); + String name = fileName + ".JPEG"; + File f = new File(name); + try { + ImageIO.write(screenCapture, "JPEG", f); + } catch (IOException e) { + logger.error("写文件时发生io异常 : ", e); + } + IplImage image = cvLoadImage(name); + videoTS = 1000 * (System.currentTimeMillis() - startTime - (System.currentTimeMillis() - pauseTime)); + if (videoTS > recorder.getTimestamp()) { + recorder.setTimestamp(videoTS); + } + recorder.record(conveter.convert(image)); + f.delete(); + System.gc(); + } catch (Exception ex) { + logger.error("录屏线程异常 : ", ex); +// MessageResponseHandler.uploadErrorInfo("录屏线程异常", ex); + } + }, (int) (1000 / frameRate), (int) (1000 / frameRate), TimeUnit.MILLISECONDS); + } + + private void caputre() { + audioFormat = new AudioFormat(44100.0F, 16, 2, true, false); + dataLineInfo = new DataLine.Info(TargetDataLine.class, audioFormat); + try { + line = (TargetDataLine) AudioSystem.getLine(dataLineInfo); + line.open(audioFormat); + } catch (LineUnavailableException e1) { + logger.error("准备录制音频时发生异常 : ", e1); +// MessageResponseHandler.uploadErrorInfo("准备录制音频时发生异常", e1); + } + line.start(); + + int sampleRate = (int) audioFormat.getSampleRate(); + int numChannels = audioFormat.getChannels(); + + int audioBufferSize = sampleRate * numChannels; + byte[] audioBytes = new byte[audioBufferSize]; + + exec = new ScheduledThreadPoolExecutor(1); + exec.scheduleAtFixedRate(() -> { + try { + int nBytesRead = line.read(audioBytes, 0, line.available()); + int nSamplesRead = nBytesRead / 2; + short[] samples = new short[nSamplesRead]; + + ByteBuffer.wrap(audioBytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(samples); + ShortBuffer sBuff = ShortBuffer.wrap(samples, 0, nSamplesRead); + + recorder.recordSamples(sampleRate, numChannels, sBuff); + System.gc(); + } catch (Exception e) { + logger.error("录制音频时发生异常 : ", e); +// MessageResponseHandler.uploadErrorInfo("录制音频时发生异常", e); + } + }, (int) (1000 / frameRate), (int) (1000 / frameRate), TimeUnit.MILLISECONDS); + } + + public void stop() { + if (null != screenTimer) { + screenTimer.shutdownNow(); + } + try { + recorder.stop(); + recorder.release(); + recorder.close(); + System.out.println("录屏结束!"); + screenTimer = null; + screenCapture = null; + if (isHaveDevice) { + if (null != exec) { + exec.shutdownNow(); + } + if (null != line) { + line.stop(); + line.close(); + } + dataLineInfo = null; + audioFormat = null; + } + } catch (Exception e) { + logger.error("录屏结束时发生异常 : ", e); +// MessageResponseHandler.uploadErrorInfo("录屏结束时发生异常", e); + } + } + + */ +} diff --git "a/automation-ui/src/main/resources/browserStatus/chrome\347\275\221\351\241\265\345\212\240\350\275\275\345\256\214\346\210\220.png" "b/automation-ui/src/main/resources/browserStatus/chrome\347\275\221\351\241\265\345\212\240\350\275\275\345\256\214\346\210\220.png" new file mode 100644 index 0000000000000000000000000000000000000000..37290c0f0af9dcfb1c220b02c856d3ad294c8b56 Binary files /dev/null and "b/automation-ui/src/main/resources/browserStatus/chrome\347\275\221\351\241\265\345\212\240\350\275\275\345\256\214\346\210\220.png" differ diff --git a/automation-ui/src/test/java/cn/testnewbie/automation/ui/CaptureScreenTest.java b/automation-ui/src/test/java/cn/testnewbie/automation/ui/CaptureScreenTest.java new file mode 100644 index 0000000000000000000000000000000000000000..98328e2e21252218b53538c50ebdbea08cb40b8d --- /dev/null +++ b/automation-ui/src/test/java/cn/testnewbie/automation/ui/CaptureScreenTest.java @@ -0,0 +1,56 @@ +package cn.testnewbie.automation.ui; + +//import org.bytedeco.javacv.CanvasFrame; + +import java.awt.*; +import java.awt.image.BufferedImage; + +/** + * https://blog.csdn.net/eguid_1/article/details/64922443 + */ +public class CaptureScreenTest { +/** + public static void captureScreen() { + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();// 获取当前屏幕大小 + Rectangle rectangle = new Rectangle(screenSize);// 指定捕获屏幕区域大小,这里使用全屏捕获 + //做好自己!--eguid,eguid的博客是:blog.csdn.net/eguid_1 + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();//本地环境 + GraphicsDevice[] gs = ge.getScreenDevices();//获取本地屏幕设备列表 + System.err.println("eguid温馨提示,找到" + gs.length + "个屏幕设备"); + Robot robot = null; + int ret = -1; + for (int index = 0; index < 10; index++) { + GraphicsDevice g = gs[index]; + try { + robot = new Robot(g); + BufferedImage img = robot.createScreenCapture(rectangle); + if (img != null && img.getWidth() > 1) { + ret = index; + break; + } + } catch (AWTException e) { + System.err.println("打开第" + index + "个屏幕设备失败,尝试打开第" + (index + 1) + "个屏幕设备"); + } + } + System.err.println("打开的屏幕序号:" + ret); + CanvasFrame frame = new CanvasFrame("eguid屏幕录制");// javacv提供的图像展现窗口 + int width = 800; + int height = 600; + frame.setBounds((int) (screenSize.getWidth() - width) / 2, (int) (screenSize.getHeight() - height) / 2, width, + height);// 窗口居中 + frame.setCanvasSize(width, height);// 设置CanvasFrame窗口大小 + while (frame.isShowing()) { + BufferedImage image = robot.createScreenCapture(rectangle);// 从当前屏幕中读取的像素图像,该图像不包括鼠标光标 + frame.showImage(image); + + try { + Thread.sleep(45); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + frame.dispose(); + } + */ +} diff --git a/automation-ui/src/test/java/cn/testnewbie/automation/ui/VideoRecorderTest.java b/automation-ui/src/test/java/cn/testnewbie/automation/ui/VideoRecorderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..1f4377c067f5e067d3d4e8e9fdb9e2d732dc14e4 --- /dev/null +++ b/automation-ui/src/test/java/cn/testnewbie/automation/ui/VideoRecorderTest.java @@ -0,0 +1,19 @@ +package cn.testnewbie.automation.ui; + +import org.junit.jupiter.api.Test; + +import java.io.File; + +public class VideoRecorderTest { + private static String defaultFilePath = System.getProperty("user.dir") + File.separator; + + @Test + void recoderTest() throws InterruptedException { +// System.getProperty("user.dir"); +// VideoRecorder videoRecorder = new VideoRecorder(defaultFilePath + "target/screenShot/rec2021", true); +// videoRecorder.start(); +// Thread.sleep(5000); +// videoRecorder.stop(); + } + +} diff --git a/example-ui/pom.xml b/example-ui/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f077df708931e918f02a54135475ba4c12fad551 --- /dev/null +++ b/example-ui/pom.xml @@ -0,0 +1,124 @@ + + + cn.testnewbie + 4.0.0 + example-ui + 0.0.1 + + + 1.8 + UTF-8 + 1.8 + ${maven.compiler.source} + 1.9.1 + 2.13.8 + + + + + + cn.testnewbie.automation + automation-ui + 1.0.2-SNAPSHOT + + + + + + + org.junit + junit-bom + 5.7.1 + pom + import + + + cn.hutool + hutool-all + 5.5.7 + + + org.projectlombok + lombok + 1.18.16 + + + io.qameta.allure + allure-junit5 + ${allure.version} + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + + maven-surefire-plugin + 2.22.2 + + false + + -Dfile.encoding=UTF-8 + -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" + + + ${project.build.directory}/allure-results + + + true + + true + + + SAME_THREAD + + dynamic + + + + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + + + + io.qameta.allure + allure-maven + 2.10.0 + + ${allure.version} + + + https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/${allure.version}/allure-commandline-${allure.version}.zip + + ${project.build.directory}/allure-results + + ${project.build.directory}/allure-report + + + + + + + + diff --git a/example-ui/src/test/java/cn/testnewbie/automation/ui/ocr/OcrTest.java b/example-ui/src/test/java/cn/testnewbie/automation/ui/ocr/OcrTest.java new file mode 100644 index 0000000000000000000000000000000000000000..1ed0d20fe1e24dab5f51d718c51b029c303e5ee1 --- /dev/null +++ b/example-ui/src/test/java/cn/testnewbie/automation/ui/ocr/OcrTest.java @@ -0,0 +1,31 @@ +package cn.testnewbie.automation.ui.ocr; + +import cn.testnewbie.automation.ui.SikuliXExecutor; +import cn.testnewbie.automation.ui.UIBase; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.sikuli.script.FindFailed; +import org.sikuli.script.OCR; + +/** + * java.lang.Error: Invalid memory access + *

+ * Error opening data file C:\Users\Administrator\AppData\Roaming\Sikulix\SikulixTesseract\tessdata/chi_sim.traineddata + * Please make sure the TESSDATA_PREFIX environment variable is set to your "tessdata" directory. + * Failed loading language 'chi_sim' + * Tesseract couldn't load any languages! + */ +@Disabled +public class OcrTest extends UIBase { + + @Test + void ocr() throws FindFailed { +// Settings.OcrDataPath = System.getProperty("user.dir") + "\\src\\test\\resources\\"; + OCR.globalOptions().language("chi_sim"); + + SikuliXExecutor.screen.waitText("帮助", 2).click(); + + SikuliXExecutor.screen.waitText("Help", 2).click(); + SikuliXExecutor.screen.waitText("Window", 2).click(); + } +} diff --git a/example-ui/src/test/java/cn/testnewbie/automation/ui/testbaidu/BaiDuTest.java b/example-ui/src/test/java/cn/testnewbie/automation/ui/testbaidu/BaiDuTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9fe354eb3a9bbecf642ac96c5ec159f6d6897f9a --- /dev/null +++ b/example-ui/src/test/java/cn/testnewbie/automation/ui/testbaidu/BaiDuTest.java @@ -0,0 +1,91 @@ +package cn.testnewbie.automation.ui.testbaidu; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.ui.util.SeleniumScreenShot; +import cn.testnewbie.automation.ui.UIBase; +import io.qameta.allure.Step; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.chrome.ChromeDriver; +import org.sikuli.script.*; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author zhanglixing + */ +@Disabled +public class BaiDuTest extends UIBase { + private static final Log log = LogFactory.get(); + + private Screen s = new Screen(); + + @Test + public void test() throws Exception { + testBaiDu(); + quit(); + } + + + @Step + public void testBaiDu() throws Exception { + log.info("用户的当前工作目录:" + System.getProperty("user.dir")); + + Pattern pattern = new Pattern(); + + try { + System.setProperty("webdriver.chrome.driver", "D:\\chromedriver.exe"); + + // WebDriver webDriver = new InternetExplorerDriver(); + // WebDriver webDriver = new FirefoxDriver(); + // WebDriver webDriver = new ChromeDriver(); + driver = new ChromeDriver(); + Thread.sleep(1000); + driver.manage().window().maximize(); + + // 隐性等待是指当要查找元素,而这个元素没有马上出现时,告诉WebDriver查询Dom一定时间 + // driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); + // Cookie cookie = new Cookie("key", "value"); + // driver.manage().addCookie(cookie); + // driver.save_screenshot("C:\error.jpg") + + // 浏览器输入地址 + driver.get("https://www.baidu.com/"); + + // 点一下输入框 + s.wait(pattern.setFilename("src/test/resources/img/Baidu/输入框.png").similar(0.55),2).offset(0, 0) + .click(); + + // 清除内容并输入 + s.keyDown(Key.CTRL); + s.type("a"); + s.keyUp(Key.CTRL); + s.paste("jtester"); + log.info("粘帖:jtester"); + + // 回车 + s.keyDown(Key.ENTER); + s.keyUp(Key.ENTER); + + // 点一下知道 + s.wait(pattern.setFilename("src/test/resources/img/Baidu/知道.png").similar(0.55),5).offset(0, 0) + .click(); + Thread.sleep(2000); + s.wheel(s, Button.WHEEL_DOWN, 12); + log.info("滑动滚轮"); + + Thread.sleep(3000); + log.info("结束"); + } catch (Exception e) { + // 截图保留现场 + SeleniumScreenShot.screenShot(driver, "d:\\screenshot\\"); + log.error("error", e); + fail("用例失败", e); + } finally { + // 关闭浏览器 + driver.quit(); + } + } + +} diff --git a/example-ui/src/test/java/cn/testnewbie/automation/ui/testbaidu2/BaiDuTest2.java b/example-ui/src/test/java/cn/testnewbie/automation/ui/testbaidu2/BaiDuTest2.java new file mode 100644 index 0000000000000000000000000000000000000000..dadfbbae7eb3d50d88f359c563ce8a7b3508b3c4 --- /dev/null +++ b/example-ui/src/test/java/cn/testnewbie/automation/ui/testbaidu2/BaiDuTest2.java @@ -0,0 +1,160 @@ +package cn.testnewbie.automation.ui.testbaidu2; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.ui.SikuliXExecutor; +import cn.testnewbie.automation.ui.UIBase; +import cn.testnewbie.automation.ui.util.SikulixScreenShot; +import io.qameta.allure.Step; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.sikuli.basics.Settings; +import org.sikuli.script.ImagePath; +import org.sikuli.script.Key; +import org.sikuli.script.Screen; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Jtester UI 测试模版 + * + * @author zhanglixing + */ +@Disabled +public class BaiDuTest2 extends UIBase { + private static final Log log = LogFactory.get(); + + private String lastLoginUser = ""; + + @Test + public void test() { + testStart(); + testBaidu2(); + quit(); + } + + @Step + public void testStart() { + log.info("用户的当前工作目录:" + System.getProperty("user.dir")); + SikulixScreenShot.defaultDir = System.getProperty("user.dir"); + } + + @Step + public void testBaidu2() { + long beginMs = System.currentTimeMillis(); + System.setProperty("webdriver.chrome.driver", "D:\\chromedriver.exe"); + ImagePath.add("src/test/resources/img/Baidu/"); + + try { + // 启用缓存,目录为工作目录下的cache目录;启动时自动最大化driver.manage().window().maximize(); + ChromeOptions options = new ChromeOptions(); + options.addArguments("--disk-cache-dir=" + System.getProperty("user.dir") + "/cache", "--start-maximized"); + driver = new ChromeDriver(options);// 请安装google浏览器并升级至最新版本,和chromedriver.exe配套 + // webDriver = new InternetExplorerDriver(); + // webDriver = new FirefoxDriver(); + + driver.get("https://www.baidu.com/");// 浏览器输入地址 + log.info("开始执行脚本"); +// SikuliXExecutor.popup("开始执行脚本"); + + // SikuliXExecutor.clickImg("fanyi.png", 0.75f, 0, 0, true, 2); + // 找不到可以跳过的图片 + + SikuliXExecutor.clickImg("输入框.png");// 点一下输入框 + // SikuliXExecutor.mouseOver("shurukuang.png", 0.75f, 2);// 鼠标over + // SikuliXExecutor.highlight("shurukuang.png", 0.75f, 2, 3);// 高亮 + + SikuliXExecutor.inputValue("testnewbie");// 输入新的搜索内容 + SikuliXExecutor.keyPress(Key.ENTER); // 回车 + + SikuliXExecutor.waitImg("chrome网页加载完成.png", 0.85f, false, 10);// 等浏览器加载 + + SikuliXExecutor.clickImg("知道.png", 0.85f, 0, 0, false, 2);// 点一下知道 + SikuliXExecutor.waitImg("chrome网页加载完成.png", 0.85f, false, 10);// 等浏览器加载 + + // SikuliXExecutor.wheelDown(s, 9); // 滑动滚轮 + SikuliXExecutor.pageDown(4); // 按翻页键 + + SikuliXExecutor.waitImg("我要提问.jpg", 0.85f, false, 10);// 等浏览器加载 + + SikuliXExecutor.sleep(2000); + + log.info("总耗时" + (System.currentTimeMillis() - beginMs) + "毫秒"); + log.info("结束"); + + } catch (Exception e) { + SikulixScreenShot.screenShot(new Screen(), System.getProperty("user.dir")); + log.error("用例失败:", e); + fail("用例失败:", e);// 报异常说明步骤执行失败 + } finally { + driver.quit(); // 关闭浏览器 + } + } + + public void loginWeb(String username, String password) { + + // 已登录的直接拿登录后的session_key + if (lastLoginUser.equals(username)) { + log.info("已登录的用户:username=" + username); + return; + } + + String userdir = System.getProperty("user.dir"); + log.info("用户的当前工作目录:" + userdir); + System.setProperty("webdriver.chrome.driver", userdir + "/webdriver/chromedriver.exe"); + + ChromeOptions options = new ChromeOptions(); + options.addArguments("--disk-cache-dir=" + System.getProperty("user.dir") + "/cache", "--start-maximized"); + driver = new ChromeDriver(options);// 请安装google浏览器并升级至最新版本,和chromedriver.exe配套 + + // 鼠标移动速度 + Settings.MoveMouseDelay = 0.25f; + Settings.setShowActions(true); + + try { + driver.get("http://49.4.49.173/#/login");// 浏览器输入地址 + + //登录方式1 + SikuliXExecutor.clickImg("登录页面.png", 0.65f, 0, 0, false, 10); + SikuliXExecutor.keyPress(Key.TAB); + SikuliXExecutor.inputValue(username); + SikuliXExecutor.keyPress(Key.TAB); + SikuliXExecutor.inputValue(password); + SikuliXExecutor.clickImg("登录按钮.png", 0.75f, 0, 0, false, 10); + + SikuliXExecutor.sleep(2000); + SikuliXExecutor.waitImg("chrome网页加载完成.png", 0.70f, false, 10);// 等浏览器加载 + + SikuliXExecutor.waitImg("登录成功.png", 0.70f, false, 20);// 等浏览器加载 + } catch (Exception e) { + log.error("登录成功.png找不到,判断为登录失败!", e); + fail("登录成功.png找不到,判断为登录失败!"); + } + lastLoginUser = username; + log.info("登录完成"); + } + + + public void logoutWeb() { + try { + if (driver != null) { + //登出 + SikuliXExecutor.clickImg("登出按钮.png", 0.75f, 0, 0, false, 5); + SikuliXExecutor.sleep(2000); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + driver.quit(); + } + } + +} + +// selenium隐性等待是指当要查找元素,而这个元素没有马上出现时,告诉WebDriver查询Dom一定时间 +// driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); +// 设置Cookie +// Cookie cookie = new Cookie("key", "value"); +// driver.manage().addCookie(cookie); diff --git a/example-ui/src/test/resources/img/Android/op.png b/example-ui/src/test/resources/img/Android/op.png new file mode 100644 index 0000000000000000000000000000000000000000..09e4822711a1a3dee1e2699779f53183d9b35e6e Binary files /dev/null and b/example-ui/src/test/resources/img/Android/op.png differ diff --git "a/example-ui/src/test/resources/img/Android/\344\270\213\345\216\250\346\210\277\345\233\276\346\240\207.png" "b/example-ui/src/test/resources/img/Android/\344\270\213\345\216\250\346\210\277\345\233\276\346\240\207.png" new file mode 100644 index 0000000000000000000000000000000000000000..73f24f68780ef8d78ba0ce9140641acf901305c1 Binary files /dev/null and "b/example-ui/src/test/resources/img/Android/\344\270\213\345\216\250\346\210\277\345\233\276\346\240\207.png" differ diff --git "a/example-ui/src/test/resources/img/Android/\344\270\213\345\216\250\346\210\277\345\233\276\346\240\2072.png" "b/example-ui/src/test/resources/img/Android/\344\270\213\345\216\250\346\210\277\345\233\276\346\240\2072.png" new file mode 100644 index 0000000000000000000000000000000000000000..9b3f1318f2bf33278c4e8469ead4675d4e5cfc9f Binary files /dev/null and "b/example-ui/src/test/resources/img/Android/\344\270\213\345\216\250\346\210\277\345\233\276\346\240\2072.png" differ diff --git "a/example-ui/src/test/resources/img/Android/\345\216\250\346\210\277\345\245\275\347\211\251.png" "b/example-ui/src/test/resources/img/Android/\345\216\250\346\210\277\345\245\275\347\211\251.png" new file mode 100644 index 0000000000000000000000000000000000000000..f6418fbbacdff47541295a5c28cf94af5fe601b8 Binary files /dev/null and "b/example-ui/src/test/resources/img/Android/\345\216\250\346\210\277\345\245\275\347\211\251.png" differ diff --git "a/example-ui/src/test/resources/img/Android/\347\211\251\345\223\2011.png" "b/example-ui/src/test/resources/img/Android/\347\211\251\345\223\2011.png" new file mode 100644 index 0000000000000000000000000000000000000000..15870347751b79efe0a6749f6c541b6fa76748e7 Binary files /dev/null and "b/example-ui/src/test/resources/img/Android/\347\211\251\345\223\2011.png" differ diff --git a/example-ui/src/test/resources/img/Baidu/fanyi.png b/example-ui/src/test/resources/img/Baidu/fanyi.png new file mode 100644 index 0000000000000000000000000000000000000000..f2cb8eabbaf090b33709b58fe473d65903de676d Binary files /dev/null and b/example-ui/src/test/resources/img/Baidu/fanyi.png differ diff --git a/example-ui/src/test/resources/img/Baidu/sousuodaan.png b/example-ui/src/test/resources/img/Baidu/sousuodaan.png new file mode 100644 index 0000000000000000000000000000000000000000..c4c0470e19998b6b9b61b295560fd633081364d2 Binary files /dev/null and b/example-ui/src/test/resources/img/Baidu/sousuodaan.png differ diff --git a/example-ui/src/test/resources/img/Baidu/sousuojieguo.png b/example-ui/src/test/resources/img/Baidu/sousuojieguo.png new file mode 100644 index 0000000000000000000000000000000000000000..e57133c5a88c0b833a14251c5b48b805b317ead9 Binary files /dev/null and b/example-ui/src/test/resources/img/Baidu/sousuojieguo.png differ diff --git "a/example-ui/src/test/resources/img/Baidu/\346\210\221\350\246\201\346\217\220\351\227\256.jpg" "b/example-ui/src/test/resources/img/Baidu/\346\210\221\350\246\201\346\217\220\351\227\256.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..7205f4de8a686964cf83d20724345d855f814e18 Binary files /dev/null and "b/example-ui/src/test/resources/img/Baidu/\346\210\221\350\246\201\346\217\220\351\227\256.jpg" differ diff --git "a/example-ui/src/test/resources/img/Baidu/\347\237\245\351\201\223.png" "b/example-ui/src/test/resources/img/Baidu/\347\237\245\351\201\223.png" new file mode 100644 index 0000000000000000000000000000000000000000..48ee5bd63140f80d822801a924d10b552c774fac Binary files /dev/null and "b/example-ui/src/test/resources/img/Baidu/\347\237\245\351\201\223.png" differ diff --git "a/example-ui/src/test/resources/img/Baidu/\350\276\223\345\205\245\346\241\206.png" "b/example-ui/src/test/resources/img/Baidu/\350\276\223\345\205\245\346\241\206.png" new file mode 100644 index 0000000000000000000000000000000000000000..b7b0ebc60636e7bef5aa5a02aed6ca45bd3f00a1 Binary files /dev/null and "b/example-ui/src/test/resources/img/Baidu/\350\276\223\345\205\245\346\241\206.png" differ diff --git a/example-ui/src/test/resources/tessdata/chi_sim.traineddata b/example-ui/src/test/resources/tessdata/chi_sim.traineddata new file mode 100644 index 0000000000000000000000000000000000000000..0f3378ec47f55b1aa8506611a278f0a7f6627a92 --- /dev/null +++ b/example-ui/src/test/resources/tessdata/chi_sim.traineddata @@ -0,0 +1,1435 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + tessdata/chi_sim.traineddata at master · tesseract-ocr/tessdata + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ Skip to content + + + + + + + + + +
+ +
+ + + + + +
+ + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + +
+ +
+ +
+

+ + + / + + tessdata + + +

+ + +
+ +
    + +
  • + +
    + + + + + + + + Watch + + + + + +
    +
    +

    Notifications

    + +
    + +
    +
    + + + + + + + + +
    + +
    +
    +
    + + +
    +
    + +
    + +
  • + +
  • +
    +
    + + +
    +
    + + +
    + +
  • + +
  • +
    +
    + +
  • +
+ +
+ + +
+ + +
+
+ + + + + +
+ + + + Permalink + + + +
+ +
+
+ + + master + + + +
+
+
+ Switch branches/tags + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + + + + + + + +
+ + +
+
+
+
+ +
+ +
+ + + + Go to file + + +
+ + +
+ +
+ + + +
+ +
+
+ + @stweil + + +
+ + Latest commit + 3737fed + May 10, 2018 + + + + + + History + + +
+
+
Signed-off-by: Stefan Weil <sw@weilnetz.de>
+ +
+ +
+
+ + + 2 + + contributors + + +
+ +

+ Users who have contributed to this file +

+
+ + + + + + + + +
+
+ + + @theraysmith + + @stweil + + + +
+
+ + + + + + +
+ +
+
+ + 42.3 MB +
+ +
+ +
+ Download +
+ +
+ + + + +
+ +
+
+
+ + + +
+
+ View raw +

(Sorry about that, but we can’t show files that are this big right now.)

+
+
+ +
+ + + +
+ + +
+ + +
+
+ + +
+ + + +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + diff --git a/example-ui/start.bat b/example-ui/start.bat new file mode 100644 index 0000000000000000000000000000000000000000..a32a77f35c21aed8f02a8712b5166de1ac8b7b8d --- /dev/null +++ b/example-ui/start.bat @@ -0,0 +1,39 @@ +@rem 执行测试(需要安装maven命令) +call mvn clean test + +echo. +echo ------------------------------------------------------------------------ +echo errorlevel = %errorlevel% +echo ------------------------------------------------------------------------ +echo. + +set "results=.\target\allure-results\" +if not exist "%results%" ( + goto fail +) else ( + goto succeed +) + +:succeed +@rem 显示趋势图 +xcopy .\allure-report\history .\target\allure-results\history /e /Y /I + +@rem 显示环境 +xcopy .\src\test\resources\allure\environment.properties .\target\allure-results\ /e /Y /I + +@rem 分类 +xcopy .\src\test\resources\allure\categories.json .\target\allure-results\ /e /Y /I + +echo. +echo generate allure-report +echo ------------------------------------------------------------------------ +echo. + +@rem 生成报告(需要安装allure命令) +call allure generate target/allure-results/ -o allure-report --clean +goto end + +:fail +echo maven error + +:end diff --git a/example/pom.xml b/example/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0c78cde5c7b0cd0ee6b9bb4585bed649e3350d69 --- /dev/null +++ b/example/pom.xml @@ -0,0 +1,170 @@ + + + + 4.0.0 + cn.testnewbie.automation + example + 0.0.1 + + + 1.8 + UTF-8 + 1.8 + ${maven.compiler.source} + 1.9.1 + 2.13.8 + 1.7.30 + 1.14.4 + + + + + cn.testnewbie.automation + automation-interface + 1.2.11-SNAPSHOT + + + cn.testnewbie.automation + automation-email + 1.0.0 + + + cn.testnewbie.automation + automation-dingding + 1.0.0 + + + mysql + mysql-connector-java + test + 8.0.19 + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + jcl-over-slf4j + ${slf4j.version} + + + org.slf4j + jul-to-slf4j + ${slf4j.version} + + + org.slf4j + log4j-over-slf4j + ${slf4j.version} + + + + + org.apache.poi + poi + 3.17 + + + org.apache.poi + poi-ooxml + 3.17 + + + + + com.alibaba + fastjson + 1.2.75 + + + + + + + + org.junit + junit-bom + 5.7.1 + pom + import + + + io.qameta.allure + allure-junit5 + ${allure.version} + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + + maven-surefire-plugin + 2.22.2 + + false + + -Dfile.encoding=UTF-8 + -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" + + + ${project.build.directory}/allure-results + + + true + + true + + SAME_THREAD + + dynamic + + + + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + + + + io.qameta.allure + allure-maven + 2.10.0 + + ${allure.version} + + + https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/${allure.version}/allure-commandline-${allure.version}.zip + + ${project.build.directory}/allure-results + + + + + + diff --git a/example/src/main/java/com/testexample/annotation/LoginWith.java b/example/src/main/java/com/testexample/annotation/LoginWith.java new file mode 100644 index 0000000000000000000000000000000000000000..65fe864b950d7c92b8493d8d282bb3fe5be5a13f --- /dev/null +++ b/example/src/main/java/com/testexample/annotation/LoginWith.java @@ -0,0 +1,29 @@ +package com.testexample.annotation; + + +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.commons.annotation.Testable; + +import java.lang.annotation.*; + +/** + * 标记在测试类、方法上,表示执行用例前,需要登录 + * 登录的逻辑在 LoginWithExtension.class 中,需要用户自行处理 + * + * @author zhanglx + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Testable +@ExtendWith(LoginWithExtension.class) +public @interface LoginWith { + + /*** 用户名 ***/ + String username() default ""; + + /*** 密码 ***/ + String password() default ""; + + /*** 项目号,如果没有该字段,请删除相关代码 ***/ + String customerNo() default ""; +} diff --git a/example/src/main/java/com/testexample/annotation/LoginWithExtension.java b/example/src/main/java/com/testexample/annotation/LoginWithExtension.java new file mode 100644 index 0000000000000000000000000000000000000000..43bfbf75f8d816b006bd9a9a2c7e4cbd6e22fb5f --- /dev/null +++ b/example/src/main/java/com/testexample/annotation/LoginWithExtension.java @@ -0,0 +1,98 @@ +package com.testexample.annotation; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.Header; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.core.cache.HttpCacheManage; +import com.testexample.common.Constants; +import com.testexample.util.Md5Util; +import org.junit.jupiter.api.Assertions; + +import java.util.HashMap; +import java.util.concurrent.Callable; + +/** + * 该类是一个模版类,需要使用者修改成自己的登录逻辑。 + *

+ * 功能:扫描`测试类`或者`测试方法`上的 LoginWith 注解,使用注解里面的信息,处理登录逻辑。 + * 本例中,登录后获取到的是token,其他接口需要将该token放到请求头里面,作为登录者的标识 + * `测试类`或者`测试类的父类`,必须要有`public HashMap headerMap = new HashMap<>();` + * 调用接口时,headerMap合并其他请求头一起发送到服务器。 + * + * @author zhanglx + */ +public class LoginWithExtension extends HttpCacheManage { + private static final Log log = LogFactory.get(); + + + /** + * 保存或者获取缓存的登录信息时,使用的key + * 如业务系统唯一的`手机号`、`客户号+帐号`等等 + * + * @param loginInfo 注解里面的信息 + * @return 每个帐号密码的唯一标识 + */ + @Override + protected String getUniqueKey(LoginWith loginInfo) { + return loginInfo.customerNo() + loginInfo.username(); + } + + /** + * 登录过程,需要子类自己实现具体逻辑 + * 比如将password加密,然后组装一个json发送给后台请求登录。 + * + * @param loginInfo 注解里面的信息 + * @return 返回整个 HttpResponse + */ + @Override + protected Callable doLogin(LoginWith loginInfo) { + Callable callable = () -> { + + String encryptedPassword = Md5Util.stringToMD5("api" + loginInfo.password()); + String httpBody = "{\"account\":\"" + loginInfo.username() + + "\",\"customerNo\":\"" + loginInfo.customerNo() + + "\",\"password\":\"" + encryptedPassword + "\"}"; + + log.debug("LoginWith--登录:" + httpBody); + HttpResponse httpResponse = HttpRequest.post("http://localhost:9999/api/login") + .header(Header.USER_AGENT, "Hutool http").body(httpBody) + .execute(); + log.debug("LoginWith--请求结果:msg = {}", httpResponse); + + return httpResponse; + }; + return callable; + } + + + /** + * 将登录接口返回的token,放入一个map,用例执行时,会作为http信息头,一起发送给服务器 + * + * @param httpResponse doLogin 的返回值,请自行处理token\cookie等等等 + * @param uniqueKey 登录信息的唯一标识 + * @return 登录信息map + */ + @Override + protected HashMap dealHttpResponse(HttpResponse httpResponse, String uniqueKey) { + HashMap loginInfo = new HashMap<>(); + + String body = httpResponse.body(); + if (body.contains(Constants.TOKEN)) { + JSONObject jsonObject = JSONUtil.parseObj(body); + JSONObject data = (JSONObject) jsonObject.get(Constants.DATA); + String tokenStr = data.getStr(Constants.TOKEN); + Assertions.assertTrue(StrUtil.isNotBlank(tokenStr)); + + loginInfo = new HashMap<>(); + loginInfo.put(Constants.X_TOKEN, tokenStr); + + CACHE.put(uniqueKey, loginInfo); + } + return loginInfo; + } +} diff --git a/example/src/main/java/com/testexample/bean/LoginBean.java b/example/src/main/java/com/testexample/bean/LoginBean.java new file mode 100644 index 0000000000000000000000000000000000000000..a4edb4918942fc1f89366b512949414f0865141e --- /dev/null +++ b/example/src/main/java/com/testexample/bean/LoginBean.java @@ -0,0 +1,17 @@ +package com.testexample.bean; + +import lombok.Data; + +@Data +public class LoginBean { + private String customerNo; + private String account; + private String password; + private String except; + + public String toHttpBody() { + return "{\"customerNo\":\"" + customerNo + '\"' + + ", \"account\":\"" + account + '\"' + + ", \"password\":\"" + password + "\"}"; + } +} diff --git a/example/src/main/java/com/testexample/common/Constants.java b/example/src/main/java/com/testexample/common/Constants.java new file mode 100644 index 0000000000000000000000000000000000000000..7924759bc404ee10f84b479f92f18245bf51d27e --- /dev/null +++ b/example/src/main/java/com/testexample/common/Constants.java @@ -0,0 +1,17 @@ +package com.testexample.common; + +/** + * @author zhanglx + */ +public class Constants { + + public static final String TOKEN = "token"; + public static final String X_TOKEN = "X-Token"; + public static final String DATA = "data"; + public static final String PID = "Pid"; + public static final int userId = 666; + + public static final String SET_COOKIE = "Set-Cookie"; + public static final String COOKIE = "Cookie"; + +} diff --git a/example/src/main/java/com/testexample/util/Md5Util.java b/example/src/main/java/com/testexample/util/Md5Util.java new file mode 100644 index 0000000000000000000000000000000000000000..1f9da4ababd92a069872cadafa9a324d8f80d680 --- /dev/null +++ b/example/src/main/java/com/testexample/util/Md5Util.java @@ -0,0 +1,30 @@ +package com.testexample.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * @author zhanglx + */ +public class Md5Util { + public static String stringToMD5(String plainText) { + byte[] secretBytes; + try { + secretBytes = MessageDigest.getInstance("md5").digest( + plainText.getBytes()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("没有这个md5算法!"); + } + + StringBuffer md5code = new StringBuffer(); + for (int i = 0; i < secretBytes.length; i++) { + int tmp = secretBytes[i]; + if (tmp < 0) + tmp += 256; + if (tmp < 16) + md5code.append("0"); + md5code.append(Integer.toHexString(tmp)); + } + return md5code.toString(); + } +} diff --git a/example/src/test/java/com/testexample/excel/AutoTest.java b/example/src/test/java/com/testexample/excel/AutoTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c902ae106d453de1e7226fb164d5a475273c1651 --- /dev/null +++ b/example/src/test/java/com/testexample/excel/AutoTest.java @@ -0,0 +1,73 @@ +package com.testexample.excel; + +import cn.testnewbie.automation.interfacetest.step.HttpStep; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +//import org.testng.Assert; +//import org.testng.annotations.BeforeClass; +//import org.testng.annotations.DataProvider; +//import org.testng.annotations.Test; + +//TODO + +public class AutoTest extends HttpStep { + + // @DataProvider(name="testData") + public static Object[][] data() throws Exception { + return ExcelUtils.getTestData(Constants.FilePath, Constants.FileSheet); + } + + +// @Test +// @Test(dataProvider="testData",description="测试接口") + public void testApi( + String rowNumber, + String caseRowNumber, + String testCaseName, + String priority, + String apiName, + String url, + String type, + String parmsType, + String parms, + String assertKeyWord + ) throws Exception { + Log.startTestCase(testCaseName); + JSONObject responseObject = null; + if ("post".equalsIgnoreCase(type) || "json".equalsIgnoreCase(parmsType)) { + responseObject = JSON.parseObject(post(url, parms)); + Log.info("Responce: " + responseObject.toString()); + //WriteResponce.write(Constant.ResponseSheet,responseObject.toString(),Integer.parseInt(rowNumber.split("[.]")[0]),0); + System.out.println("response: " + responseObject); + } else { + //TODO + } + + Log.info("断言Response是否与预期结果一致: " + assertKeyWord); + try { + //Assert.assertTrue(responseObject.toString().contains(assertKeyWord)); + Assertions.assertTrue(JSONPathUtil.checkPoint(responseObject.toString(), assertKeyWord)); + } catch (AssertionError error) { + Log.info("断言Response是否与预期结果一致: " + assertKeyWord + " ---> 断言失败"); + ExcelUtils.setCellData(Integer.parseInt(rowNumber.split("[.]")[0]), ExcelUtils.getLastColumnNum(), "Fail"); + Log.info("测试结果成功写入excel数据文件中的测试执行结果列"); + Assertions.fail("断言Response是否与预期结果一致: " + assertKeyWord + " 失败"); + } + + //System.out.println("**** "+Integer.parseInt(rowNumber.split("[.]")[0])); + //ExcelUtils.setCellData(Integer.parseInt(rowNumber.split("[.]")[0]),10,"测试执行成功"); + Log.info("断言Response是否与预期结果一致: " + assertKeyWord + " ---> 断言成功"); + ExcelUtils.setCellData(Integer.parseInt(rowNumber.split("[.]")[0]), ExcelUtils.getLastColumnNum(), "Pass"); + Log.info("测试结果成功写入excel数据文件中的测试执行结果列"); + Log.endTestCase(testCaseName); + } + + + @BeforeAll + public void beforeClass() throws Exception { + ExcelUtils.setExcelFile(Constants.FilePath, Constants.FileSheet); + } +} diff --git a/example/src/test/java/com/testexample/excel/Constants.java b/example/src/test/java/com/testexample/excel/Constants.java new file mode 100644 index 0000000000000000000000000000000000000000..7809650d503063d4cff0fbaa348f5602a9c18890 --- /dev/null +++ b/example/src/test/java/com/testexample/excel/Constants.java @@ -0,0 +1,18 @@ +package com.testexample.excel; + +/** + * @author zhanglx + */ +public class Constants { + + //public static final String URL = "http://mail.qq.com"; + + //测试数据EXCEL路径 + public static final String FilePath = "D:\\接口自动化测试.xlsx"; + + // EXCEL测试数据sheet名称 + public static final String FileSheet = "测试用例"; + + public static final String ResponseSheet = "response"; + +} diff --git a/example/src/test/java/com/testexample/excel/ExcelTest.java b/example/src/test/java/com/testexample/excel/ExcelTest.java new file mode 100644 index 0000000000000000000000000000000000000000..788edfa1c3f5fde2a8727385ed18d32891e0342b --- /dev/null +++ b/example/src/test/java/com/testexample/excel/ExcelTest.java @@ -0,0 +1,15 @@ +package com.testexample.excel; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.poi.excel.ExcelReader; +import cn.hutool.poi.excel.ExcelUtil; +import org.junit.jupiter.api.Test; + +// todo +public class ExcelTest { + + // @Test + void readExcel() { + ExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream("aaa.xlsx")); + } +} diff --git a/example/src/test/java/com/testexample/excel/ExcelUtils.java b/example/src/test/java/com/testexample/excel/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c4479c5368d289e580f48c29150b0f7404c248b --- /dev/null +++ b/example/src/test/java/com/testexample/excel/ExcelUtils.java @@ -0,0 +1,142 @@ +package com.testexample.excel; + +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFCell; +import org.apache.poi.xssf.usermodel.XSSFRow; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class ExcelUtils { + + private static XSSFSheet sheet; + private static XSSFWorkbook workbook; + private static XSSFCell cell; + private static XSSFRow row; + + //指定要操作的excel文件的路径及sheet名称 + public static void setExcelFile(String path,String sheetName) throws Exception{ + try { + FileInputStream file = new FileInputStream(path); + workbook = new XSSFWorkbook(file); + sheet = workbook.getSheet(sheetName); + } catch (Exception e) { + e.printStackTrace(); + } + } + + //读取excel文件指定单元格数据(此方法只针对.xlsx后辍的Excel文件) + public static String getCellData(int rowNum,int colNum) throws Exception{ + try { + //获取指定单元格对象 + cell = sheet.getRow(rowNum).getCell(colNum); + //获取单元格的内容 + //如果为字符串类型,使用getStringCellValue()方法获取单元格内容,如果为数字类型,则用getNumericCellValue()获取单元格内容 + String cellData = cell.getStringCellValue(); + return cellData; + } catch (Exception e) { + return ""; + } + } + + //在EXCEL的执行单元格中写入数据(此方法只针对.xlsx后辍的Excel文件) rowNum 行号,colNum 列号 + public static void setCellData(int rowNum,int colNum,String Result) throws Exception{ + try { + //获取行对象 + row = sheet.getRow(rowNum); + //如果单元格为空,则返回null + cell = row.getCell(colNum); + if(cell == null){ + cell=row.createCell(colNum); + cell.setCellValue(Result); + }else{ + cell.setCellValue(Result); + } + FileOutputStream out = new FileOutputStream(Constants.FilePath); + //将内容写入excel中 + workbook.write(out); + out.flush(); + out.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + //从EXCEL文件中获取测试数据 + public static Object[][] getTestData(String excelFilePath,String sheetName) throws IOException { + //声明一个file文件对象 + File file = new File(excelFilePath); + //创建一个输入流 + FileInputStream in = new FileInputStream(file); + //声明workbook对象 + Workbook workbook = null; + //判断文件扩展名 + String fileExtensionName = excelFilePath.substring(excelFilePath.indexOf(".")); + if(fileExtensionName.equals(".xlsx")){ + workbook = new XSSFWorkbook(in); + }else { + workbook = new HSSFWorkbook(in); + } + + //获取sheet对象 + Sheet sheet = workbook.getSheet(sheetName); + //获取sheet中数据的行数,行号从0始 + int rowCount = sheet.getLastRowNum()-sheet.getFirstRowNum(); + + List records = new ArrayList(); + //读取数据(省略第一行表头) + for(int i=1; i>>>>>>>>>> "+ row.getLastCellNum()); + //声明一个数组存每行的测试数据,excel最后两列不需传值 + String[] fields = new String[row.getLastCellNum()-2]; + //excel倒数第二列为Y,表示数据行要被测试脚本执行,否则不执行 + if(row.getCell(row.getLastCellNum()-2).getStringCellValue().equals("Y")){ + //if(row.getCell(10).getStringCellValue().equals("Y")){ + for(int j=0; j>>>>>>>>>>[k]); + }*/ + //System.out.println("********:"+sheet.getRow(0).getLastCellNum()); + records.add(fields); + } + } + //将list转为Object二维数据 + Object[][] results = new Object[records.size()][]; + //设置二维数据每行的值,每行是一个object对象 + for(int i=0; i map = new HashMap<>(); + for (int i = 0; i < data.length; i++) { + map.put(data[i].split("=")[0], data[i].split("=")[1]); + System.out.println("检查点"+ (i+1) +"返回的数据:" + JSONPath.read(json, data[i].split("=")[0])); + System.out.println("检查点"+ (i+1) +"断言的数据:" + map.get(data[i].split("=")[0])); + + //判断检查点数据与返回的json数据是否一致 + if (JSONPath.read(json, data[i].split("=")[0]) instanceof String) { + if (JSONPath.read(json, data[i].split("=")[0]).equals(map.get(data[i].split("=")[0]))) { + //System.out.println("Pass A"); + flag = true; + } else { + //System.out.println("Fail A"); + flag = false; + break; + } + } else { // Object转String + if ((JSONPath.read(json, data[i].split("=")[0]).toString()).equals((map.get(data[i].split("=")[0])))) { + //System.out.println("Pass B"); + flag = true; + } else { + //System.out.println("Fail B"); + flag = false; + break; + } + } + } + if (flag) { + System.out.println("【测试执行结果:通过】"); + } else { + System.out.println("【测试执行结果:失败】"); + } + } + + /** + * 预期结果校验 + * @param response + * @param assertKeyWord + * @return + */ + public static Boolean checkPoint(String response,String assertKeyWord){ + //分隔检查点 + String[] data = assertKeyWord.split(";"); + // 定义测试结果的标记 + Boolean flag = false; + + //遍历数组,获取每一个检查点在json中对应的数据,存在map中 + Map map = new HashMap<>(); + for (int i = 0; i < data.length; i++) { + map.put(data[i].split("=")[0], data[i].split("=")[1]); + System.out.println("检查点"+ (i+1) +"返回的数据:" + JSONPath.read(response, data[i].split("=")[0])); + System.out.println("检查点"+ (i+1) +"断言的数据:" + map.get(data[i].split("=")[0])); + + Log.info("检查点"+ (i+1) +"返回的数据:" + JSONPath.read(response, data[i].split("=")[0])); + Log.info("检查点"+ (i+1) +"断言的数据:" + map.get(data[i].split("=")[0])); + + //判断检查点数据与返回的json数据是否一致 + if (JSONPath.read(response, data[i].split("=")[0]) instanceof String) { + if (JSONPath.read(response, data[i].split("=")[0]).equals(map.get(data[i].split("=")[0]))) { + //System.out.println("Pass A"); + flag = true; + } else { + //System.out.println("Fail A"); + flag = false; + break; + } + } else { // Object转String + if ((JSONPath.read(response, data[i].split("=")[0]).toString()).equals((map.get(data[i].split("=")[0])))) { + flag = true; + } else { + flag = false; + break; + } + } + } + if (flag) { + return true; + } else { + return false; + } + } +} diff --git a/example/src/test/java/com/testexample/excel/Log.java b/example/src/test/java/com/testexample/excel/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..10c4212f43c0fb7353e255927d322ae671e21029 --- /dev/null +++ b/example/src/test/java/com/testexample/excel/Log.java @@ -0,0 +1,41 @@ +package com.testexample.excel; + +import org.apache.log4j.Logger; + +public class Log { + + // 初始化Log4j日志 + private static Logger Log = Logger.getLogger(com.testexample.excel.Log.class.getName()); + + // 打印测试用例开头的日志 + public static void startTestCase(String sTestCaseName) { + Log.info("------------------ 测试用例【"+ sTestCaseName + "】" + "开始执行 ------------------"); + } + + //打印测试用例结束的日志 + public static void endTestCase(String sTestCaseName) { + Log.info("------------------ 测试用例【"+ sTestCaseName + "】" + "测试执行结束 ------------------"); + + } + + public static void info(String message) { + Log.info(message); + } + + public static void warn(String message) { + Log.warn(message); + } + + public static void error(String message) { + Log.error(message); + } + + public static void fatal(String message) { + Log.fatal(message); + } + + public static void debug(String message) { + Log.debug(message); + } + +} diff --git a/example/src/test/java/com/testexample/interfacetest/LoginTest.java b/example/src/test/java/com/testexample/interfacetest/LoginTest.java new file mode 100644 index 0000000000000000000000000000000000000000..67275d8a93f4c82d3ec93f47e47cdb5074b19b42 --- /dev/null +++ b/example/src/test/java/com/testexample/interfacetest/LoginTest.java @@ -0,0 +1,87 @@ +package com.testexample.interfacetest; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.interfacetest.step.HttpStep; +import com.testexample.util.Md5Util; +import io.qameta.allure.Description; +import io.qameta.allure.Step; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.opentest4j.AssertionFailedError; + +import java.util.HashMap; + +import static cn.testnewbie.automation.core.AssertUtil.assertByJSONPath; +import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT; + +@Disabled +public class LoginTest extends HttpStep { + private static final Log log = LogFactory.get(); + + static { + System.out.println("本测试类 LoginTest ,需要开启`project-demo`服务才能正常执行通过..."); + } + + + // @ParameterizedTest(name = "{displayName} [{argumentsWithNames}]") + @ParameterizedTest(name = "{displayName} {index}") + @CsvSource({"0001, apitest1, 123456, token", + "0001, apitest2, 123456, \"status\":true", + "0002, apitest3, 123456, 账户已被禁用,请联系管理员", + "0002, apitest4, 123456, \"status\":false"}) // 执行2次 + @DisplayName("登录测试") + @Description("并发执行") + @Execution(CONCURRENT) + void loginTest(String customerNo, String account, String password, String except) { + String httpResponse = httpLogin(customerNo, account, password); + checkHttpResponse(except, httpResponse); + } + + @Test + void loginTest2() { + String httpResponse = httpLogin("0001", "apitest", "123456"); + assertByJSONPath("$.status", true, httpResponse); + } + + @Test + void loginTest3() { + String httpResponse = httpLogin("0001", "apitest", "123456"); +// Assertions.assertAll( +// () -> assertByJSONPath("$.status", "true", httpResponse), // 类型对不上,本case会失败 +// () -> assertByJSONPath("$.data.message", "登录成功", httpResponse) // 不存在的字段,本case会失败 +// ); + Assertions.assertThrows(AssertionFailedError.class, () -> assertByJSONPath("$.status", "true", httpResponse));// 类型对不上,本case会失败 + Assertions.assertThrows(AssertionFailedError.class, () -> assertByJSONPath("$.data.message", "登录成功", httpResponse));// 不存在的字段,本case会失败 + } + + + @RepeatedTest(3) + void loginTest4() { + String httpResponse = httpLogin("0001", "apitest", "123456"); + HashMap except = new HashMap(); + except.put("$.status", true); + except.put("$.code", null); + except.put("$.data.lastProjectId", 1001); + except.put("$.data.projects[0].projectId", 1001); + assertByJSONPath(except, httpResponse); + } + + + @Step("登录") + public String httpLogin(String customerNo, String account, String password) { + String encryptedPassword = Md5Util.stringToMD5("api" + password); + String httpBody = "{\"account\":\"" + account + "\",\"customerNo\":\"" + customerNo + "\",\"password\":\"" + encryptedPassword + "\"}"; + + log.info("登录:" + httpBody); + return post("http://localhost:9999/api/login", httpBody); + } + + + @AfterAll + static void dummy() { + System.out.println("-----AfterAll-----"); + } +} diff --git a/example/src/test/java/com/testexample/interfacetest/LoginTestUseCsvBeanSource.java b/example/src/test/java/com/testexample/interfacetest/LoginTestUseCsvBeanSource.java new file mode 100644 index 0000000000000000000000000000000000000000..5ecf00299ff2cbef29d0974b833f888277d56b6c --- /dev/null +++ b/example/src/test/java/com/testexample/interfacetest/LoginTestUseCsvBeanSource.java @@ -0,0 +1,68 @@ +package com.testexample.interfacetest; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.interfacetest.step.HttpStep; +import cn.testnewbie.automation.core.annotation.CsvBeanArgument; +import cn.testnewbie.automation.core.annotation.CsvBeanSource; +import com.testexample.bean.LoginBean; +import com.testexample.util.Md5Util; +import io.qameta.allure.Step; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.converter.JavaTimeConversionPattern; +import org.junit.jupiter.params.provider.ValueSource; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT; + +@Execution(CONCURRENT) +public class LoginTestUseCsvBeanSource extends HttpStep { + private static final Log log = LogFactory.get(); + + static { + System.out.println("本测试类 LoginTestUseCsvBeanSource ,需要开启`project-demo`服务才能正常执行通过..."); + } + + @ParameterizedTest(name = "{displayName} [{argumentsWithNames}]") + @CsvBeanSource({"customerNo=0001, account=apitest1, password=123456, except=token", + "customerNo=0001, account=apitest2, password=123456, except=\"status\":true", + "customerNo=0002, account=apitest3, password=123456, except=账户已被禁用,请联系管理员", + "customerNo=0002, account=apitest4, password=123456, except=\"status\":false"}) // 执行2次 + @DisplayName("登录测试") + void csvLoginTest(@CsvBeanArgument LoginBean loginBean) { + String httpResponse = httpLogin(loginBean); + checkHttpResponse(loginBean.getExcept(), httpResponse); + } + +// @ParameterizedTest(name = "{displayName} [{argumentsWithNames}]") +// @CsvFileSource(files = {"src/test/resources/testcase/login.csv"}, numLinesToSkip = 1) +// @DisplayName("登录测试") +// void csvLoginTest2(@CsvBeanArgument LoginBean loginBean) { +// String httpResponse = httpLogin(loginBean); +// log.info("登录返回:" + httpResponse); +// checkHttpResponse(loginBean.getExcept(), httpResponse); +// } + + + @Step("登录") + public String httpLogin(LoginBean loginBean) { + String encryptedPassword = Md5Util.stringToMD5("api" + loginBean.getPassword()); + loginBean.setPassword(encryptedPassword); + + String httpBody = loginBean.toHttpBody(); + log.info("登录:" + httpBody); + return post("http://localhost:9999/api/login", httpBody); + } + + + @ParameterizedTest + @ValueSource(strings = {"01.01.2017", "31.12.2017"}) + void testWithExplicitJavaTimeConverter(@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) { + assertEquals(2017, argument.getYear()); + } +} diff --git a/example/src/test/java/com/testexample/interfacetest/LoginWithOnClassTest.java b/example/src/test/java/com/testexample/interfacetest/LoginWithOnClassTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a9575781f8d816f57ac63a8812d632ce157e7f09 --- /dev/null +++ b/example/src/test/java/com/testexample/interfacetest/LoginWithOnClassTest.java @@ -0,0 +1,48 @@ +package com.testexample.interfacetest; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.core.AssertUtil; +import cn.testnewbie.automation.interfacetest.step.HttpStep; +import com.testexample.annotation.LoginWith; +import com.testexample.common.Constants; +import io.qameta.allure.Allure; +import io.qameta.allure.Step; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@Execution(ExecutionMode.CONCURRENT) +@LoginWith(username = "apitest1", password = "123456", customerNo = "0001") +public class LoginWithOnClassTest extends HttpStep { + private static final Log log = LogFactory.get(); + + static { + System.out.println("本测试类 LoginWithOnClassTest ,需要开启`project-demo`服务才能正常执行通过..."); + } + + @ParameterizedTest(name = "[{argumentsWithNames}]") + @CsvSource({"7", "3", "2"}) + public void getUserInfo1(int userId) { + String httpResponse = getUserInfo(userId); + AssertUtil.assertByJSONPath("$.status", true, httpResponse); + } + + @Test + public void getUserInfo2() { + getUserInfo(Constants.userId); + } + + + @Step("获取用户信息") + String getUserInfo(int userId) { + String httpResponse = + post("http://localhost:9999/api/getUserInfo", "{\"id\": " + userId + "}"); + Allure.attachment("获取用户信息返回", httpResponse); + return httpResponse; + } + + +} diff --git a/example/src/test/java/com/testexample/interfacetest/LoginWithOnMethodTest.java b/example/src/test/java/com/testexample/interfacetest/LoginWithOnMethodTest.java new file mode 100644 index 0000000000000000000000000000000000000000..53b22d3b7af896ecc86be6f7c0fa234f6c27b85a --- /dev/null +++ b/example/src/test/java/com/testexample/interfacetest/LoginWithOnMethodTest.java @@ -0,0 +1,51 @@ +package com.testexample.interfacetest; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.core.AssertUtil; +import cn.testnewbie.automation.interfacetest.step.HttpStep; +import com.testexample.annotation.LoginWith; +import com.testexample.common.Constants; +import io.qameta.allure.Allure; +import io.qameta.allure.Step; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@Execution(ExecutionMode.CONCURRENT) +public class LoginWithOnMethodTest extends HttpStep { + private static final Log log = LogFactory.get(); + + static { + System.out.println("本测试类 LoginWithOnMethodTest ,需要开启`project-demo`服务才能正常执行通过..."); + } + + @LoginWith(username = "apitest1", password = "123456", customerNo = "0001") + @ParameterizedTest(name = "[{argumentsWithNames}]") + @CsvSource({"7", "3", "2"}) + public void getUserInfo1(int userId) { + String httpResponse = getUserInfo(userId); + AssertUtil.assertByJSONPath("$.status", true, httpResponse); + } + + @Test + @LoginWith(username = "apitest2", password = "123456", customerNo = "0001") + public void getUserInfo2() { + String httpResponse = getUserInfo(Constants.userId); + AssertUtil.assertByJSONPath("$.status", true, httpResponse); + } + + + @Step("获取用户信息") + String getUserInfo(int userId) { + String httpResponse = + post("http://localhost:9999/api/getUserInfo", + "{\"id\": " + userId + "}"); + Allure.attachment("获取用户信息返回", httpResponse); + return httpResponse; + } + + +} diff --git a/example/src/test/java/com/testexample/interfacetest/LoginWithSimpleTest.java b/example/src/test/java/com/testexample/interfacetest/LoginWithSimpleTest.java new file mode 100644 index 0000000000000000000000000000000000000000..24ab7a83d6fefbb60521d964016705b6ccd36bb0 --- /dev/null +++ b/example/src/test/java/com/testexample/interfacetest/LoginWithSimpleTest.java @@ -0,0 +1,32 @@ +package com.testexample.interfacetest; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import com.testexample.annotation.LoginWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.HashMap; + +@Execution(ExecutionMode.CONCURRENT) +@LoginWith(username = "apitest1", password = "123456", customerNo = "0001") +public class LoginWithSimpleTest { + private static final Log log = LogFactory.get(); + + static { + System.out.println("本测试类 LoginWithSimpleTest ,需要开启`project-demo`服务才能正常执行通过..."); + } + + + public HashMap headerMap = new HashMap<>(); + + @ParameterizedTest(name = "[{argumentsWithNames}]") + @CsvSource({"7", "3", "2"}) + public void loginWithOnClass(int userId) { + System.out.println("userId = " + userId + "; headerMap = " + headerMap.toString()); + } + + +} diff --git a/example/src/test/java/com/testexample/otherdemo/alluredemo/allure/AllureFixtureTest.java b/example/src/test/java/com/testexample/otherdemo/alluredemo/allure/AllureFixtureTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7c22feafc01ec431f7595cf1e434c4ba05b5e8d9 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/alluredemo/allure/AllureFixtureTest.java @@ -0,0 +1,44 @@ +package com.testexample.otherdemo.alluredemo.allure; + +import org.junit.jupiter.api.*; + +import static io.qameta.allure.Allure.step; +import static io.qameta.allure.Allure.suite; + +public class AllureFixtureTest { + + @BeforeAll + public static void beforeAll() { + step("Step inside beforeAll"); + } + + @BeforeEach + public void beforeEach() { + step("Step inside beforeEach"); + } + + @Test + public void allureFixtureTest() { + suite("allure suite 1"); + step("Step inside allureFixtureTest"); + step("lambda Step", () -> { + System.out.println("lambda..."); + }); + } + + @Test + public void allureFixtureTest2() { + step("Step inside allureFixtureTest2"); + } + + @AfterEach + public void afterEach() { + step("Step inside afterEach"); + } + + @AfterAll + public static void afterAll() { + step("Step inside afterAll"); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/alluredemo/allure/AllureParameterizedTest.java b/example/src/test/java/com/testexample/otherdemo/alluredemo/allure/AllureParameterizedTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7d397e66d48bdac6e5cc36db12b7f2a7ce15517b --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/alluredemo/allure/AllureParameterizedTest.java @@ -0,0 +1,57 @@ +package com.testexample.otherdemo.alluredemo.allure; + +import io.qameta.allure.Description; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static io.qameta.allure.Allure.parameter; +import static io.qameta.allure.Allure.step; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AllureParameterizedTest { + + @ParameterizedTest(name = "{displayName} [{argumentsWithNames}]") + @ValueSource(strings = {"First Name", "Second Name"}) // 执行2次 + @DisplayName("用例名称") + @Description("allure参数化测试--描述") + public void allureParameterizedTest(String testParam1) { + parameter("参数", testParam1); + step("Step inside parameterized test"); +// step("Test parameter: " + testParam1); + step("Test parameter: ", () -> { + System.out.println(testParam1); + }); + } + + @Test + public void allureFakeParameterizedTest() { + parameter("fakeParam","fakeValue"); + step("Step inside fake parameterized test"); + } + + + @ParameterizedTest + @MethodSource("stringIntAndListProvider") + void testWithMultiArgMethodSource(String str, int num, List list) { + assertEquals(3, str.length()); + assertTrue(num >=1 && num <=2); + assertEquals(2, list.size()); + } + + static Stream stringIntAndListProvider() { + return Stream.of( + Arguments.of("foo", 1, Arrays.asList("a", "b")), + Arguments.of("bar", 2, Arrays.asList("x", "y")) + ); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/alluredemo/allure/AllureSimpleTest.java b/example/src/test/java/com/testexample/otherdemo/alluredemo/allure/AllureSimpleTest.java new file mode 100644 index 0000000000000000000000000000000000000000..462aac73daeecebd3efda059adc1219a5d68825e --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/alluredemo/allure/AllureSimpleTest.java @@ -0,0 +1,31 @@ +package com.testexample.otherdemo.alluredemo.allure; + +import io.qameta.allure.Step; +import io.qameta.allure.model.Status; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.qameta.allure.Allure.attachment; +import static io.qameta.allure.Allure.step; + +public class AllureSimpleTest { + + @Test + @DisplayName("allureSimpleTest displayName") + public void allureSimpleTest() { + step("Simple step"); + step("Simple step with status", Status.FAILED); + step("Simple lambda step", () -> { + step("Simple step inside lambda step"); + }); + simpleTestMethod("method parameter"); + attachment("附件名称","附件内容"); + } + + @Step("Simple test method with step annotation") + public void simpleTestMethod(String param) { + step("Method parameter: " + param); + step("Simple step inside test method"); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/alluredemo/allure/AllureTest.java b/example/src/test/java/com/testexample/otherdemo/alluredemo/allure/AllureTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2ec808ad8a8219bab5d4733fabfd88d35399cba7 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/alluredemo/allure/AllureTest.java @@ -0,0 +1,42 @@ +package com.testexample.otherdemo.alluredemo.allure; + +import io.qameta.allure.*; +import org.junit.jupiter.api.Test; + +import static io.qameta.allure.Allure.suite; + +@Epic("这是一个Epic") +@Feature("这是一个Feature") +public class AllureTest { + + @Test + @Description("第一个测试") + @Story("这是一个Story") + public void StepTest() { + suite("allure suite 1"); + step1(); + step2(); + } + + @Step("这是创建部门第一步") + public void step1() { + System.out.println("步骤1"); + } + + @Step("这是创建部门第二步") + public void step2() { + System.out.println("步骤2"); + } + +// @Step("Type {user.name} / {user.password}.") +// public void loginWith(User user) { +// ... +// } + + @Test + @Attachment + public String StepTest3() { + System.out.println("步骤3"); + return "步骤3"; + } +} diff --git a/example/src/test/java/com/testexample/otherdemo/alluredemo/allure/JunitDemoTest.java b/example/src/test/java/com/testexample/otherdemo/alluredemo/allure/JunitDemoTest.java new file mode 100644 index 0000000000000000000000000000000000000000..8b2f3a2b312f1c3d75705919b4a290bf0a230a11 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/alluredemo/allure/JunitDemoTest.java @@ -0,0 +1,101 @@ +package com.testexample.otherdemo.alluredemo.allure; + +import cn.testnewbie.automation.core.util.GuiCamera; +import io.qameta.allure.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +//import io.github.bonigarcia.wdm.ChromeDriverManager; +//import org.openqa.selenium.OutputType; +//import org.openqa.selenium.TakesScreenshot; +//import org.openqa.selenium.WebDriver; +//import org.openqa.selenium.chrome.ChromeDriver; +//import static org.hamcrest.CoreMatchers.is; +//import static org.hamcrest.MatcherAssert.assertThat; + + +@Epic("极光浏览器打开操作") +@Feature("对极光首页进行截图") +public class JunitDemoTest { +// static WebDriver driver; + + @BeforeAll + public static void setUp() throws Exception { +// ChromeDriverManager.getInstance().setup(); +// driver = new ChromeDriver(); + } + + @Test + @Issue("open-1") + @Description("打开浏览器,获取项目的title,对比结果") + public void openhome() { +// driver.get("https://www.jiguang.cn/"); +// String title = driver.getTitle(); +// System.out.println(title); +// assertEquals("首页 - 极光", title); + makeScreenShot(); +// driver.close(); + } + + @Step("带截图的case") + public void makeScreenShot() { +// return ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES); + GuiCamera cam1 = new GuiCamera(null, "target/screenShot"); + String fileName; + try { + fileName = cam1.screenShot(); + } catch (Exception e) { + fail("截图失败"); + return; + } + + Path content = Paths.get(fileName); + try (InputStream is = Files.newInputStream(content)) { + Allure.attachment("进行截图1", is); + Allure.addAttachment("进行截图2", "image/png", + new FileInputStream(fileName), ".png"); + } catch (IOException e) { + e.printStackTrace(); + fail("截图失败"); + } + } + + @Test + public void simpleTestWithAttachments() throws Exception { + assertEquals(2, 2); + makeAttach(); + } + + @Attachment + public String makeAttach() { + return "yeah, 2 is 2"; + } + + @Description("Test shows CSV attachment") + @Test + public void csvAttachmentTest() throws Exception { + saveCsvAttachment(); + } + + @Issue("123") + @Attachment(value = "Sample csv attachment", type = "text/csv") + public byte[] saveCsvAttachment() throws URISyntaxException, IOException { + URL resource = getClass().getClassLoader().getResource("testcase/sample.csv"); + if (resource == null) { + fail("不能打开资源文件 'sample.csv'"); + } + return Files.readAllBytes(Paths.get(resource.toURI())); + } +} diff --git a/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureAnnotationStepTest.java b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureAnnotationStepTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0b3cd7045070765f9812d3e33c105a965ce347b1 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureAnnotationStepTest.java @@ -0,0 +1,32 @@ +package com.testexample.otherdemo.alluredemo.allureexamples; + +import io.qameta.allure.Step; +import org.junit.jupiter.api.Test; + +import static io.qameta.allure.Allure.step; + +public class AllureAnnotationStepTest { + + @Test + public void allureStepAnnotationTest() { + simpleTestMethod(); + parametrizedTestMethod("method parameter"); + parametrizedWithFieldTestMethod("field parameter"); + } + + @Step("Parametrized test method with step annotation'") + public void parametrizedTestMethod(String param) { + step("Method parameter: " + param); + } + + @Step("Parametrized test method with fields: '{param}'") + public void parametrizedWithFieldTestMethod(String param) { + + } + + @Step("Simple test method with step annotation") + public void simpleTestMethod() { + step("Simple step inside test method"); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureAttachmentTest.java b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureAttachmentTest.java new file mode 100644 index 0000000000000000000000000000000000000000..99734ed89135c7f8cba9789743494abbe11ac885 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureAttachmentTest.java @@ -0,0 +1,58 @@ +package com.testexample.otherdemo.alluredemo.allureexamples; + +import cn.testnewbie.automation.core.util.InputStreamToBytes; +import io.qameta.allure.Attachment; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static io.qameta.allure.Allure.addAttachment; +import static io.qameta.allure.Allure.attachment; + +//import org.apache.commons.io.IOUtils; + +public class AllureAttachmentTest { + + @Test + public void allureSimpleAttachmentTest() { + attachment("file1.txt", "Text inside file"); + addAttachment("file2.txt", "Text inside file"); + addAttachment("file3.txt", "text/plain", "Text inside file"); + addAttachment("file4.txt", "text/plain", "Text inside file", "txt"); + + InputStream stream = ClassLoader.getSystemResourceAsStream("testcase/content.xml"); + attachment("content1.xml", stream); + + stream = ClassLoader.getSystemResourceAsStream("testcase/content.xml"); + addAttachment("content2.xml", stream); + + stream = ClassLoader.getSystemResourceAsStream("testcase/content.xml"); + addAttachment("content3.xml", "text/xml", stream, "xml"); + } + + @Test + public void allureAttachmentAnnotationTest() { + attachFile("test.txt", "text inside file"); + attachFile("index.html", "

text inside html

"); + attachXmlFile(); + } + + @Attachment(value = "Attachment name: {filename}") + public String attachFile(String filename, String content) { + return content; + } + + @Attachment(value = "XML_file", type = "text/xml", fileExtension = "xml") + public byte[] attachXmlFile() { + try { + final InputStream stream = ClassLoader.getSystemResourceAsStream("testcase/content.xml"); +// return IOUtils.toString(stream, StandardCharsets.UTF_8).getBytes(); + return InputStreamToBytes.convertToText(stream, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureBehaviorsMappingTest.java b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureBehaviorsMappingTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d1ed833c455314e91aa68651168ad46e6f2b4116 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureBehaviorsMappingTest.java @@ -0,0 +1,51 @@ +package com.testexample.otherdemo.alluredemo.allureexamples; + +import io.qameta.allure.*; +import org.junit.jupiter.api.Test; + +import static io.qameta.allure.Allure.*; + +@Epics(@Epic("Epic from class annotation")) +@Features(@Feature("Feature from class annotation")) +@Stories({@Story("Story from class annotation"), @Story("Story2 from class annotation")}) +public class AllureBehaviorsMappingTest { + + @Test + public void allureLambdaSuiteTest() { + suite("Suite from lambda annotation"); + } + + @Epic("Epic from test annotation") + @Feature("Feature from test annotation") + @Story("Story from test annotation") + @Test + public void allureBehaviorsMappingTest() { + + } + + @Test + public void allureLambdaBehaviorsMappingTest() { + epic("Epic from lambda method"); + feature("Feature from lambda method"); + story("Story from lambda method"); + } + + @Epic("Epic2 from test annotation") + @Test + public void allureEpicMappingTest() { + epic("Epic2 from lambda method"); + } + + @Feature("Feature2 from test annotation") + @Test + public void allureFeatureMappingTest() { + feature("Feature2 from lambda method"); + } + + @Story("Story2 from test annotation") + @Test + public void allureStoryMappingTest() { + story("Story2 from lambda method"); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureLabelsTest.java b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureLabelsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d83ffa6f41bc0f85f4344ba7dc50f15383647e39 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureLabelsTest.java @@ -0,0 +1,27 @@ +package com.testexample.otherdemo.alluredemo.allureexamples; + +import org.junit.jupiter.api.Test; + +import static io.qameta.allure.Allure.label; + +public class AllureLabelsTest { + + /** + * Feature of Allure EE and Allure 3 + */ + @Test + public void allureLabelTest() { + //Custom Fields + label("microservice", "Report"); + label("env", "Stage"); + + //Test Layer + label("layer", "Selenium"); + label("layer", "API"); + + //Members + label("author", "eroshenkoam"); + label("author", "simple-elf"); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureLambdaStepTest.java b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureLambdaStepTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a5d6de9295d911ab953de6f6c5ee5b9e3905ee32 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureLambdaStepTest.java @@ -0,0 +1,34 @@ +package com.testexample.otherdemo.alluredemo.allureexamples; + +import io.qameta.allure.model.Status; +import org.junit.jupiter.api.Test; + +import static io.qameta.allure.Allure.step; + +public class AllureLambdaStepTest { + + @Test + public void allureSimpleStepTest() { + step("Simple step"); + step("Simple step with failed status", Status.FAILED); + step("Simple step with skipped status", Status.SKIPPED); + step("Simple step with broken status", Status.BROKEN); + step("Simple step with passed status", Status.PASSED); + } + + @Test + public void allureLambdaStepTest() { + step("Simple lambda step", () -> { + step("Simple step inside lambda step"); + }); + + step("Lambda step with parameter", (step) -> { + step.parameter("param", "value"); + }); + + step("Old step name", (step) -> { + step.name("Dynamic step name"); + }); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureLinksTest.java b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureLinksTest.java new file mode 100644 index 0000000000000000000000000000000000000000..af755a6ec961ee3b90f3b2a4b5c7ccdafed88111 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureLinksTest.java @@ -0,0 +1,53 @@ +package com.testexample.otherdemo.alluredemo.allureexamples; + +import io.qameta.allure.*; +import org.junit.jupiter.api.Test; + +import static io.qameta.allure.Allure.*; + +public class AllureLinksTest { + + @Link("https://docs.qameta.io") // empty URL + @Test + public void allureOneParamLinkTest() { + link("https://docs.qameta.io"); // empty name + } + + @Links({@Link(name = "Allure Docs (@Link)", url = "https://docs.qameta.io"), @Link(name = "GitHub (@Link)", url = "https://github.com/allure-examples/")}) + @Test + public void allureAnnotationLinkTest() { + + } + + @Test + public void allureLambdaLinkTest() { + link("Allure Docs (link)", "https://docs.qameta.io"); + + // link types: tms issue custom + link("GitHub Issue (link)", "issue", "https://github.com/allure-examples/allure-examples/issues/12"); + link("GitHub Tms (link)", "tms", "https://github.com/allure-examples/allure-examples/blob/master/allure-java-commons/README.md"); + } + + @Issues(@Issue("https://github.com/allure-examples/allure-examples/issues/12")) + @Test + public void allureAnnotationIssueTest() { + + } + + @Test + public void allureLambdaIssueTest() { + issue("GitHub Issue (issue)", "https://github.com/allure-examples/allure-examples/issues/12"); + } + + @TmsLinks(@TmsLink("https://github.com/allure-examples/allure-examples/blob/master/allure-java-commons/README.md")) + @Test + public void allureAnnotationTMSTest() { + + } + + @Test + public void allureLambdaTMSTest() { + tms("GitHub Tms (tms)", "https://github.com/allure-examples/allure-examples/blob/master/allure-java-commons/README.md"); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureSuiteTest.java b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureSuiteTest.java new file mode 100644 index 0000000000000000000000000000000000000000..604472395f077776a4d774b544ce14a0f5bed57b --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureSuiteTest.java @@ -0,0 +1,19 @@ +package com.testexample.otherdemo.alluredemo.allureexamples; + +import org.junit.jupiter.api.Test; + +import static io.qameta.allure.Allure.suite; + +public class AllureSuiteTest { + + @Test + public void allureSuiteTest() { + suite("allure suite 1"); + } + + @Test + public void allureSuite2Test() { + suite("allure suite 2"); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureTestDescriptionTest.java b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureTestDescriptionTest.java new file mode 100644 index 0000000000000000000000000000000000000000..89b673d33de5915122823a9bcc8bfb1f5161c81d --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureTestDescriptionTest.java @@ -0,0 +1,20 @@ +package com.testexample.otherdemo.alluredemo.allureexamples; + +import org.junit.jupiter.api.Test; + +import static io.qameta.allure.Allure.description; +import static io.qameta.allure.Allure.descriptionHtml; + +public class AllureTestDescriptionTest { + + @Test + public void allureSimpleDescriptionTest() { + description("simple test description, \n" + "new line ignored"); + } + + @Test + public void allureHTMLDescriptionTest() { + descriptionHtml("

HTML

test description
new line
"); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureTestParametersTest.java b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureTestParametersTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b763e84837ee064594e04cb8a041085b5bfba46c --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/alluredemo/allureexamples/AllureTestParametersTest.java @@ -0,0 +1,21 @@ +package com.testexample.otherdemo.alluredemo.allureexamples; + +import org.junit.jupiter.api.Test; + +import static io.qameta.allure.Allure.parameter; + +public class AllureTestParametersTest { + + @Test + public void allureOneTestParameterTest() { + parameter("test param", "value"); + } + + @Test + public void allureTestParametersTest() { + parameter("test param1", "value1"); + parameter("test param2", "value2"); + parameter("test param3", "value3"); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/core/AssertJ.java b/example/src/test/java/com/testexample/otherdemo/core/AssertJ.java new file mode 100644 index 0000000000000000000000000000000000000000..63a5f0abd78376aa26eb43b7cadb79b63e0b3d84 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/core/AssertJ.java @@ -0,0 +1,96 @@ +package com.testexample.otherdemo.core; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.core.common.TNConstants; +import cn.testnewbie.automation.core.config.PropertyMgr; +import cn.testnewbie.automation.core.db.IDataBase; +import cn.testnewbie.automation.core.util.StopWatchUtil; +import com.mysql.cj.jdbc.MysqlDataSource; +import org.assertj.db.type.Request; +import org.assertj.db.type.Source; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.assertj.db.api.Assertions.assertThat; + + +@Disabled +public class AssertJ implements IDataBase { + private static final Log log = LogFactory.get(); + StopWatchUtil stopWatchUtil = new StopWatchUtil("DataBaseTest"); + + @Test + void assert_j() { + String url = PropertyMgr.getString(TNConstants.URL); + String username = PropertyMgr.getString(TNConstants.USERNAME); + String password = PropertyMgr.getString(TNConstants.PASSWORD); + Source source = new Source(url, username, password); + Request request = new Request(source, "SELECT name,method,url FROM au_step_http"); + + assertThat(request) + .column("method") + .hasValues("POST", "GET"); + + assertThat(request) + .row().hasValues("www.baidu.com", "POST", "http://127.0.0.1:8090/demo/addDemo") + .row().hasValues("/demo/?id=15", "GET", "http://127.0.0.1:8090/demo/queryDemo"); + + } + + //db.dataSource + @Test + void assert_j2() { + + Request request = new Request(db.getDataSource(), + "SELECT name,method,url FROM au_step_http where id = ? ", + new Object[]{"1"}); + + stopWatchUtil.goOn("assertThat1"); + assertThat(request) + .column("method") + .hasValues("POST"); +// ------------------------------------------------ + stopWatchUtil.goOn("assertThat2"); + assertThat(request) + .row().hasValues("www.baidu.com", "POST", "http://127.0.0.1:8090/demo/addDemo"); +// ------------------------------------------------ + stopWatchUtil.goOn("new Request 2 "); // ------------ + request = new Request(db.getDataSource(), "SELECT name,status,description FROM au_step_jdbc"); + assertThat(request) + .column("description") + .hasValues("描述"); + stopWatchUtil.stop(); // ------------ + } + + + // MysqlDataSource + @Test + void assert_j3() { + stopWatchUtil.goOn("PropertyMgr"); + String url = PropertyMgr.getString(TNConstants.URL); + String username = PropertyMgr.getString(TNConstants.USERNAME); + String password = PropertyMgr.getString(TNConstants.PASSWORD); + + stopWatchUtil.goOn("MysqlDataSource"); + MysqlDataSource source2 = new MysqlDataSource(); + source2.setUrl(url); + source2.setUser(username); + source2.setPassword(password); + + stopWatchUtil.goOn("new Request"); + Request request = new Request(source2, + "SELECT name,method,url FROM au_step_http where id = ? ", + new Object[]{"1"}); + + stopWatchUtil.goOn("assertThat1"); // ------------ + assertThat(request) + .column("method") + .hasValues("POST"); + + stopWatchUtil.goOn("assertThat2"); // ------------ + assertThat(request) + .row().hasValues("www.baidu.com", "POST", "http://127.0.0.1:8090/demo/addDemo"); + stopWatchUtil.stop(); // ------------ + } +} diff --git a/example/src/test/java/com/testexample/otherdemo/core/DataBaseTest.java b/example/src/test/java/com/testexample/otherdemo/core/DataBaseTest.java new file mode 100644 index 0000000000000000000000000000000000000000..bd14e13b6ba13eacd0e790c0bb883ed9438d36d2 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/core/DataBaseTest.java @@ -0,0 +1,34 @@ +package com.testexample.otherdemo.core; + +import cn.hutool.json.JSONUtil; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.core.db.IDataBase; +import cn.testnewbie.automation.core.util.StopWatchUtil; +import org.json.JSONException; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import java.sql.SQLException; +import java.util.List; + + +@Disabled +public class DataBaseTest implements IDataBase { + private static final Log log = LogFactory.get(); + StopWatchUtil stopWatchUtil = new StopWatchUtil("DataBaseTest"); + + @Test + void executeQuery() throws SQLException, JSONException { +// db.getConnection(); + List list = db.executeQuery("SELECT name,method,url FROM au_step_http"); + log.info(list.toString()); + + String expected = "[{\"name\":\"/demo/?id=15\", \"method\":\"GET\", \"url\":\"http://127.0.0.1:8090/demo/queryDemo\"},{\"name\":\"www.baidu.com\", \"method\":\"POST\", \"url\":\"http://127.0.0.1:8090/demo/addDemo\"}]"; + String actual = JSONUtil.toJsonStr(list); + log.info(actual); + JSONAssert.assertEquals(expected, actual, false); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/jsonassert/JSONAssertTest.java b/example/src/test/java/com/testexample/otherdemo/jsonassert/JSONAssertTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f85be595192bb12867b848746fb606f75452f38c --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/jsonassert/JSONAssertTest.java @@ -0,0 +1,80 @@ +package com.testexample.otherdemo.jsonassert; + +import org.json.JSONException; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +/** + * http://jsonassert.skyscreamer.org/cookbook.html + */ +//@Disabled +public class JSONAssertTest { + + @Test + void test2() throws JSONException { + String expected = "{id:1,name:\"Joe\",friends:[{id:2,name:\"Pat\",pets:[\"dog\"]},{id:3,name:\"Sue\",pets:[\"bird\",\"fish\"]}],pets:[]}"; + String actual = "{id:1,name:\"Joe\",friends:[{id:2,name:\"Pat\",pets:[\"dog\"]},{id:3,name:\"Sue\",pets:[\"cat\",\"fish\"]}],pets:[]}"; + JSONAssert.assertEquals(expected, actual, false); + } + + @Test + void test3() throws JSONException { + // If you enable strictMode, then extended fields fail: + String result = "{id:1,name:\"Juergen\"}"; + JSONAssert.assertEquals("{id:1}", result, false); // Pass + JSONAssert.assertEquals("{id:1}", result, true); // Fail + } + + @Test + void test4() throws JSONException { + // Strict or not, field order does not matter: + String result = "{id:1,name:\"Juergen\"}"; + JSONAssert.assertEquals("{name:\"Juergen\",id:1}", result, true); // Pass + // Because application interfaces are naturally extended as they mature, + // it is recommended that you default to leaving strict mode off, except in particular cases. + } + + @Test + void test5() throws JSONException { + // Arrays rules are different. If sequence is important, you can enable strict mode: + String result = "[1,2,3,4,5]"; + JSONAssert.assertEquals("[1,2,3,4,5]", result, true); // Pass + JSONAssert.assertEquals("[5,3,2,1,4]", result, true); // Fail + + // When strict mode is off, arrays can be in any order: + String result2 = "[1,2,3,4,5]"; + JSONAssert.assertEquals("[5,3,2,1,4]", result2, false); // Pass + } + + @Test + void test6() throws JSONException { + // Strict or not, arrays must match. They can't be "extended" like object fields can: + String result3 = "[1,2,3,4,5]"; + JSONAssert.assertEquals("[1,2,3,4,5]", result3, false); // Pass + JSONAssert.assertEquals("[1,2,3]", result3, false); // Fail + JSONAssert.assertEquals("[1,2,3,4,5,6]", result3, false); // Fail + + // You can test arrays of arrays, loose/strict ordering constraints apply at all levels: + String result4 = "{id:1,stuff:[[1,2],[2,3],[],[3,4]]}"; + JSONAssert.assertEquals("{id:1,stuff:[[1,2],[2,3],[],[3,4]]}", result4, true); // Pass + JSONAssert.assertEquals("{id:1,stuff:[[4,3],[3,2],[],[1,2]]}", result4, false); // Pass + } + + @Test + void test7() throws JSONException { + // You can test arrays of arrays, loose/strict ordering constraints apply at all levels: + String result = "{id:1,name:\"Joe\",friends:[{id:2,name:\"Pat\",pets:[\"dog\"]},{id:3,name:\"Sue\",pets:[\"bird\",\"fish\"]}],pets:[]}"; + JSONAssert.assertEquals("{id:1,name:\"Joe\",friends:[{id:2,name:\"Pat\",pets:[\"dog\"]},{id:3,name:\"Sue\",pets:[\"bird\",\"fish\"]}],pets:[]}", result, true); // Pass + JSONAssert.assertEquals("{name:\"Joe\",friends:[{id:3,name:\"Sue\",pets:[\"fish\",\"bird\"]},{id:2,name:\"Pat\",pets:[\"dog\"]}],pets:[],id:1}", result, false); // Pass + } + + @Test + void test8() throws JSONException { + // As you can see, tests work against any level of depth: + String result2 = "{a:{b:{c:{d:{e:{f:{g:{h:{i:{j:{k:{l:{m:{n:{o:{p:\"blah\"}}}}}}}}}}}}}}}}"; + JSONAssert.assertEquals("{a:{b:{c:{d:{e:{f:{g:{h:{i:{j:{k:{l:{m:{n:{o:{p:\"blah\"}}}}}}}}}}}}}}}}", + result2, true); // Pass + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/junit5demo/CalcLngLatTest.java b/example/src/test/java/com/testexample/otherdemo/junit5demo/CalcLngLatTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3d881b1640a5316475b228da145bffce1699bbb3 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/junit5demo/CalcLngLatTest.java @@ -0,0 +1,39 @@ +package com.testexample.otherdemo.junit5demo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; + +import java.util.HashMap; + + +public class CalcLngLatTest { + public static HashMap headerMap = new HashMap(); + public static double startLng = 120.203696000010; +// public static double incrementLng = 0.000015000001;// 南北 + public static double incrementLng = 0.000600000001; // 东西 + + public static double startLat = 30.18130000001; +// public static double incrementLat = 0.000600000000; // 南北 + public static double incrementLat = 0.000001000001; // 东西 + + @DisplayName("add") + @RepeatedTest(22) + void add() { + double step = startLng + incrementLng; + System.out.print(step); + startLng = step; + } + + @DisplayName("subtract") + @RepeatedTest(20) + void subtract() { + double step = startLat + incrementLat; + System.out.print(step); + startLat = step; + } + + static { + startLng = startLng - incrementLng; + startLat = startLat + incrementLat; + } +} diff --git a/example/src/test/java/com/testexample/otherdemo/junit5demo/Calculator.java b/example/src/test/java/com/testexample/otherdemo/junit5demo/Calculator.java new file mode 100644 index 0000000000000000000000000000000000000000..a99b3ba3f21910c1aec5f893b99f7685bc46e588 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/junit5demo/Calculator.java @@ -0,0 +1,19 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package com.testexample.otherdemo.junit5demo; + +public class Calculator { + + public int add(int a, int b) { + return a + b; + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/junit5demo/CalculatorTests.java b/example/src/test/java/com/testexample/otherdemo/junit5demo/CalculatorTests.java new file mode 100644 index 0000000000000000000000000000000000000000..0baf3b470ae43e997808353e7c3b1be296d13adb --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/junit5demo/CalculatorTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package com.testexample.otherdemo.junit5demo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.converter.JavaTimeConversionPattern; +import org.junit.jupiter.params.provider.CsvFileSource; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +//@ExtendWith({DBExtension.class}) +class CalculatorTests { + + @Test + @DisplayName("1 + 1 = 2") + void addsTwoNumbers() { + Calculator calculator = new Calculator(); + assertEquals(2, calculator.add(1, 1), "1 + 1 should equal 2"); + } + + @ParameterizedTest(name = "{0} + {1} = {2}") + @CsvSource({ + "0, 1, 1", + "1, 2, 3", + "49, 51, 100", + "1, 100, 101" + }) + void add(int first, int second, int expectedResult) { + Calculator calculator = new Calculator(); + assertEquals(expectedResult, calculator.add(first, second), + () -> first + " + " + second + " should equal " + expectedResult); + } + + @ParameterizedTest(name = "{0} + {1} = {2}") + @CsvFileSource(files = {"src/test/resources/testcase/calculator.csv"}, numLinesToSkip = 1) + void add2(int first, int second, int expectedResult) { + Calculator calculator = new Calculator(); + assertEquals(expectedResult, calculator.add(first, second), + () -> first + " + " + second + " should equal " + expectedResult); + } + + @ParameterizedTest + @ValueSource(strings = {"01.01.2017", "31.12.2017"}) + void testWithExplicitJavaTimeConverter(@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) { + System.out.println("argument = " + argument); + assertEquals(2017, argument.getYear()); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/junit5demo/DynamicTestsDemoTest.java b/example/src/test/java/com/testexample/otherdemo/junit5demo/DynamicTestsDemoTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e2e3379938507dae15879579427c92c3727a62f2 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/junit5demo/DynamicTestsDemoTest.java @@ -0,0 +1,111 @@ +package com.testexample.otherdemo.junit5demo; + +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.function.ThrowingConsumer; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.Random; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.DynamicContainer.dynamicContainer; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +class DynamicTestsDemoTest { + + // This will result in a JUnitException! + // must return a single org.junit.jupiter.api.DynamicNode or a Stream, Collection, Iterable, + // Iterator, or array of org.junit.jupiter.api.DynamicNode. +// @TestFactory +// List dynamicTestsWithInvalidReturnType() { +// return Arrays.asList("Hello", "1"); +// } + + @TestFactory + Collection dynamicTestsFromCollection() { + return Arrays.asList( + dynamicTest("1st dynamic test", () -> assertTrue(true)), + dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2)) + ); + } + + @TestFactory + Iterable dynamicTestsFromIterable() { + return Arrays.asList( + dynamicTest("3rd dynamic test", () -> assertTrue(true)), + dynamicTest("4th dynamic test", () -> assertEquals(4, 2 * 2)) + ); + } + + @TestFactory + Iterator dynamicTestsFromIterator() { + return Arrays.asList( + dynamicTest("5th dynamic test", () -> assertTrue(true)), + dynamicTest("6th dynamic test", () -> assertEquals(4, 2 * 2)) + ).iterator(); + } + + @TestFactory + Stream dynamicTestsFromStream() { + return Stream.of("A", "B", "C") + .map(str -> dynamicTest("test" + str, () -> { /* ... */ })); + } + + @TestFactory + Stream dynamicTestsFromIntStream() { + // Generates tests for the first 10 even integers. + return IntStream.iterate(0, n -> n + 2).limit(10) + .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0))); + } + + @TestFactory + Stream generateRandomNumberOfTests() { + + // Generates random positive integers between 0 and 100 until + // a number evenly divisible by 7 is encountered. + Iterator inputGenerator = new Iterator() { + + Random random = new Random(); + int current; + + @Override + public boolean hasNext() { + current = random.nextInt(100); + return current % 7 != 0; + } + + @Override + public Integer next() { + return current; + } + }; + + // Generates display names like: input:5, input:37, input:85, etc. + Function displayNameGenerator = (input) -> "input:" + input; + + // Executes tests based on the current input value. + ThrowingConsumer testExecutor = (input) -> assertTrue(input % 7 != 0); + + // Returns a stream of dynamic tests. + return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor); + } + + @TestFactory + Stream dynamicTestsWithContainers() { + return Stream.of("A", "B", "C") + .map(input -> dynamicContainer("Container " + input, Stream.of( + dynamicTest("not null", () -> assertNotNull(input)), + dynamicContainer("properties", Stream.of( + dynamicTest("length > 0", () -> assertTrue(input.length() > 0)), + dynamicTest("not empty", () -> assertFalse(input.isEmpty())) + )) + ))); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/junit5demo/ExecutionTest.java b/example/src/test/java/com/testexample/otherdemo/junit5demo/ExecutionTest.java new file mode 100644 index 0000000000000000000000000000000000000000..055312ccb8a159d78c4e64b03d2cda5ef2e7d255 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/junit5demo/ExecutionTest.java @@ -0,0 +1,30 @@ +package com.testexample.otherdemo.junit5demo; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.parallel.Execution; + +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT; + + +//@Execution(CONCURRENT) +@Disabled +public class ExecutionTest { + + @RepeatedTest(4) + @Execution(CONCURRENT) + void exec1() throws InterruptedException { + TimeUnit.SECONDS.sleep(1); + System.out.println("exec1"); + } + + + @RepeatedTest(4) + void exec3() throws InterruptedException { + TimeUnit.SECONDS.sleep(1); + System.out.println("exec1"); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/junit5demo/OrderTest.java b/example/src/test/java/com/testexample/otherdemo/junit5demo/OrderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ad6926d524d682d1d13491a6ef58c2c9a8bc1340 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/junit5demo/OrderTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package com.testexample.otherdemo.junit5demo; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) // 按照Order注解排序 +//@TestMethodOrder(MethodOrderer.Random.class) // 随机 +//@TestMethodOrder(MethodOrderer.MethodName.class) // 方法名,默认 +class OrderTest { + + + @Test + @Order(1) + void bb() { + } + + @Test + @Order(2) + void dd() { + } + + @Test + @Order(3) + void ab() { + } + + @Test + @Order(4) + void aa() { + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/junit5demo/SeverityTest.java b/example/src/test/java/com/testexample/otherdemo/junit5demo/SeverityTest.java new file mode 100644 index 0000000000000000000000000000000000000000..24b521663e5f53390c551e1155a0e0a6d310b535 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/junit5demo/SeverityTest.java @@ -0,0 +1,39 @@ +package com.testexample.otherdemo.junit5demo; + +import io.qameta.allure.Severity; +import io.qameta.allure.SeverityLevel; +import org.junit.jupiter.api.Test; + +public class SeverityTest { + + @Test + @Severity(SeverityLevel.TRIVIAL) + void execTRIVIAL() { + System.out.println("TRIVIAL"); + } + + + @Test + @Severity(SeverityLevel.MINOR) + void execMINOR() { + System.out.println("MINOR"); + } + + @Test + @Severity(SeverityLevel.NORMAL) + void execNORMAL() { + System.out.println("NORMAL"); + } + + @Test + @Severity(SeverityLevel.CRITICAL) + void execCRITICAL() { + System.out.println("CRITICAL"); + } + + @Test + @Severity(SeverityLevel.BLOCKER) + void execBLOCKER() { + System.out.println("BLOCKER"); + } +} diff --git a/example/src/test/java/com/testexample/otherdemo/other/ExtensionTest.java b/example/src/test/java/com/testexample/otherdemo/other/ExtensionTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7adbac75565d49294b054323c0988dbf4de8cc3d --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/other/ExtensionTest.java @@ -0,0 +1,69 @@ +package com.testexample.otherdemo.other; + +import cn.testnewbie.automation.core.util.InputStreamToBytes; +import cn.testnewbie.automation.interfacetest.junit5.DBExtension; +import cn.testnewbie.automation.interfacetest.junit5.Junit5Extension; +import io.qameta.allure.Attachment; +import io.qameta.allure.Severity; +import io.qameta.allure.SeverityLevel; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +@ExtendWith({DBExtension.class, Junit5Extension.class}) +public class ExtensionTest { + + @Test + @Severity(SeverityLevel.CRITICAL) + void calc1() { + int i = 1 + 1; + System.out.println("calc1 i = " + i); + assertEquals(2, i); + } + + @RepeatedTest(3) + void calc2() { + int i = 1 + 2; + System.out.println("calc2 i = " + i); + assertEquals(3, i); + } + + + @RepeatedTest(1) + @Disabled + void calc3() { + int i = 1 + 3; + System.out.println("calc3 i = " + i); + assertEquals(3, i); + } + + @Test + @Disabled + void calc4() { + int i = 1 + 4; + System.out.println("calc4 i = " + i); + i = 5 / 0; + } + + @Test + @Severity(SeverityLevel.MINOR) + void screenshot5() throws Exception { + saveScreenshot(); + performedActions("Attachment content"); + } + + @Attachment(value = "截图--", type = "image/png") + public byte[] saveScreenshot() throws Exception { + return InputStreamToBytes.screenshot(); + } + + @Attachment + public String performedActions(String actionSequence) { + return actionSequence; + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/other/HuToolLogTest.java b/example/src/test/java/com/testexample/otherdemo/other/HuToolLogTest.java new file mode 100644 index 0000000000000000000000000000000000000000..4c7f2e9d6aae2ec6ec293974364e6e8b4903cf58 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/other/HuToolLogTest.java @@ -0,0 +1,22 @@ +package com.testexample.otherdemo.other; + + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.hutool.log.level.Level; +import org.junit.jupiter.api.Test; + +public class HuToolLogTest { + private static final Log log = LogFactory.get(); + + @Test + void logTest() { + + log.debug("This is {} log", Level.DEBUG); + log.info("This is {} log", Level.INFO); + log.warn("This is {} log", Level.WARN); + + Exception e = new Exception("test Exception"); + log.error(e, "This is {} log", Level.ERROR); + } +} diff --git a/example/src/test/java/com/testexample/otherdemo/other/SlfLogTest.java b/example/src/test/java/com/testexample/otherdemo/other/SlfLogTest.java new file mode 100644 index 0000000000000000000000000000000000000000..631e01a911683d926bd48d335f65b613803f0dca --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/other/SlfLogTest.java @@ -0,0 +1,25 @@ +package com.testexample.otherdemo.other; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.hutool.log.level.Level; +import org.junit.jupiter.api.Test; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import org.slf4j.event.Level; + +public class SlfLogTest { + // private static final Logger log = LoggerFactory.getLogger(SlfLogTest.class); + private static final Log log = LogFactory.get(); + + @Test + void logTest() { + + log.debug("This is {} log", Level.DEBUG); + log.info("This is {} log", Level.INFO); + log.warn("This is {} log", Level.WARN); + + Exception e = new Exception("test Exception"); + log.error("This is ERROR log", e); + } +} diff --git a/example/src/test/java/com/testexample/otherdemo/util/AssertUtilTest.java b/example/src/test/java/com/testexample/otherdemo/util/AssertUtilTest.java new file mode 100644 index 0000000000000000000000000000000000000000..fad580e1486e515c33a3a6712154f75f406c761f --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/util/AssertUtilTest.java @@ -0,0 +1,50 @@ +package com.testexample.otherdemo.util; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.testnewbie.automation.core.AssertUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; + +import static cn.testnewbie.automation.core.AssertUtil.assertByJSONPath; + +@DisplayName("AssertUtil工具类测试") +public class AssertUtilTest { + private static final Log log = LogFactory.get(); + String json = "{\"status\":true,\"data\":{\"userName\":\"apitest接口测试账户\",\"token\":\"Z07OeMMwAMLLs18Pg\",\"lastProjectId\":1001,\"projects\":[{\"projectId\":1001,\"projectName\":\"道路\",\"longitude\":120.202027,\"latitude\":30.185405},{\"projectId\":1002,\"projectName\":\"工厂\",\"longitude\":120.201344,\"latitude\":30.185628}]},\"message\":null,\"code\":null}"; + + @Test + void jsonAssertTest1() { + assertByJSONPath("$.data.userName", "apitest接口测试账户", json); + } + + @Test + void jsonAssertTest2() { + HashMap except = new HashMap<>(); + except.put("$.status", true); + except.put("$.code", null); + except.put("$.data.lastProjectId", 1001); + except.put("$.data.projects[1].projectId", 1002); + assertByJSONPath(except, json); + } + + @Test + @Disabled + void jsonAssertTest3() { + assertByJSONPath("$.data.message", "登录成功", json); // 不存在的字段,本case会失败 + } + + @Test + void jsonAssertTest4() { + AssertUtil.assertAll( + () -> assertByJSONPath("$.status", true, json), + () -> assertByJSONPath("$.code", null, json), + () -> assertByJSONPath("$.data.lastProjectId", 1002, json), + () -> assertByJSONPath("$.data.projects[1].projectId", 1002, json) + ); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/util/GuiCameraTest.java b/example/src/test/java/com/testexample/otherdemo/util/GuiCameraTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0c59f62f884f206a7ec363aca05d781b8372cf17 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/util/GuiCameraTest.java @@ -0,0 +1,48 @@ +package com.testexample.otherdemo.util; + +import cn.testnewbie.automation.core.util.GuiCamera; +import io.qameta.allure.Allure; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileInputStream; + +@DisplayName("GuiCamera工具类测试") +public class GuiCameraTest { + + @Test + @Disabled +// @DisabledOnOs(OS.WINDOWS) + void screenshot() throws Exception { + GuiCamera cam1 = new GuiCamera(); + String fileName = cam1.screenShot(); + Assertions.assertTrue(new File(fileName).exists()); + Allure.attachment("截图测试1", fileName); + } + + @Test + @Disabled + void screenshot2() throws Exception { + GuiCamera cam1 = new GuiCamera(System.getProperty("user.dir") + File.separator); + String fileName = cam1.screenShot(cam1.getDateTime(), "jpg"); + Assertions.assertTrue(new File(fileName).exists()); + Allure.attachment("截图测试2", fileName); + } + + @Test + void screenshot3() throws Exception { + GuiCamera cam1 = new GuiCamera(null, "target/screenShot"); + String fileName = cam1.screenShot(cam1.getDateTime(), "jpg"); + Assertions.assertTrue(new File(fileName).exists()); + + Allure.attachment("文件名", fileName); + + Allure.addAttachment("截图测试3", "image/png", + new FileInputStream(fileName), ".png"); + } + + +} diff --git a/example/src/test/java/com/testexample/otherdemo/util/JUnit5_StandardSoftAssertions_Examples.java b/example/src/test/java/com/testexample/otherdemo/util/JUnit5_StandardSoftAssertions_Examples.java new file mode 100644 index 0000000000000000000000000000000000000000..8493db3629e18f6644a3a03fbf581c71ec345b8d --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/util/JUnit5_StandardSoftAssertions_Examples.java @@ -0,0 +1,44 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + *

+ * Copyright 2012-2016 the original author or authors. + */ +package com.testexample.otherdemo.util; + +import org.assertj.core.api.SoftAssertions; +import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.ArrayList; + +@ExtendWith(SoftAssertionsExtension.class) +public class JUnit5_StandardSoftAssertions_Examples { + + @Test + public void successful_junit_standard_soft_assertion_example(SoftAssertions softly) { + softly.assertThat("Frodo").isEqualTo("Frodo"); + softly.assertThat(33).isEqualTo(33); + softly.assertThat(new ArrayList<>()).contains("111"); + } + + // comment the @Disabled to see the test failing with all the assertion error and not only the first one. + @Test + @Disabled + public void failing_junit_standard_soft_assertions_example(SoftAssertions softly) { + // basic object to test + String name = "Michael Jordan - Bulls"; + + // use our own soft assertions based on JUnit rule + softly.assertThat(name).startsWith("Mike").contains("Lakers").endsWith("Chicago"); + } + +} diff --git a/example/src/test/java/com/testexample/otherdemo/util/SimpleAgentTest.java b/example/src/test/java/com/testexample/otherdemo/util/SimpleAgentTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f447f89f6ca4a54666489be24a1344d500f7d5b4 --- /dev/null +++ b/example/src/test/java/com/testexample/otherdemo/util/SimpleAgentTest.java @@ -0,0 +1,9 @@ +package com.testexample.otherdemo.util; + +public class SimpleAgentTest { + + public static void main(String[] args) { + + } + +} diff --git a/example/src/test/resources/allure/allure.properties b/example/src/test/resources/allure/allure.properties new file mode 100644 index 0000000000000000000000000000000000000000..206859214cbe917b96c87adda3894f3afd5d2028 --- /dev/null +++ b/example/src/test/resources/allure/allure.properties @@ -0,0 +1,3 @@ +allure.link.mylink.pattern={} +allure.link.issue.pattern={} +allure.link.tms.pattern={} \ No newline at end of file diff --git a/example/src/test/resources/allure/categories.json b/example/src/test/resources/allure/categories.json new file mode 100644 index 0000000000000000000000000000000000000000..fe34def0b2d05cdb47fd25c28117d622178366c7 --- /dev/null +++ b/example/src/test/resources/allure/categories.json @@ -0,0 +1,23 @@ +[ + { + "name": "通过的case", + "matchedStatuses": ["passed"] + }, + { + "name": "失败的case", + "matchedStatuses": ["failed"] + }, + { + "name": "跳过的case", + "matchedStatuses": ["skipped"] + }, + { + "name": "异常的case", + "matchedStatuses": ["broken", "unknown"] + }, + { + "name": "找不到文件的case", + "matchedStatuses": ["broken"], + "traceRegex": ".*FileNotFoundException.*" + } +] diff --git a/example/src/test/resources/allure/environment.properties b/example/src/test/resources/allure/environment.properties new file mode 100644 index 0000000000000000000000000000000000000000..54e72f574ffe14ab800b2ef4e1cc372bcf9a57eb --- /dev/null +++ b/example/src/test/resources/allure/environment.properties @@ -0,0 +1,3 @@ +Browser=Chrome +Browser.Version=63.0 +Stand=Production diff --git a/example/src/test/resources/categories.json b/example/src/test/resources/categories.json new file mode 100644 index 0000000000000000000000000000000000000000..fe34def0b2d05cdb47fd25c28117d622178366c7 --- /dev/null +++ b/example/src/test/resources/categories.json @@ -0,0 +1,23 @@ +[ + { + "name": "通过的case", + "matchedStatuses": ["passed"] + }, + { + "name": "失败的case", + "matchedStatuses": ["failed"] + }, + { + "name": "跳过的case", + "matchedStatuses": ["skipped"] + }, + { + "name": "异常的case", + "matchedStatuses": ["broken", "unknown"] + }, + { + "name": "找不到文件的case", + "matchedStatuses": ["broken"], + "traceRegex": ".*FileNotFoundException.*" + } +] diff --git a/example/src/test/resources/logback.xml b/example/src/test/resources/logback.xml new file mode 100644 index 0000000000000000000000000000000000000000..e95af2137f4b5bfd86fe82f81dad9210d1a9a8fa --- /dev/null +++ b/example/src/test/resources/logback.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + + + + + + + ${FILE_PATH}/log/test-newbie.log + + ${FILE_LOG_PATTERN} + + + ${FILE_PATH}/log/test-newbie-%d{yyyy-MM-dd}.%i.log + 30 + + 16MB + + + + + + + + + ${FILE_PATH}/log/test-newbie.error.log + + + ERROR + ACCEPT + DENY + + + + ${FILE_PATH}/log/test-newbie-error-%d{yyyy-MM-dd}.%i.log + + 30 + + 16MB + + + + ${FILE_LOG_PATTERN} + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/src/test/resources/testcase/calculator.csv b/example/src/test/resources/testcase/calculator.csv new file mode 100644 index 0000000000000000000000000000000000000000..0c3bc74f880edd8dceb7b950e969bf38a9bafba6 --- /dev/null +++ b/example/src/test/resources/testcase/calculator.csv @@ -0,0 +1,3 @@ +a,b,c +19,21,40 +55,56,111 diff --git a/example/src/test/resources/testcase/content.xml b/example/src/test/resources/testcase/content.xml new file mode 100644 index 0000000000000000000000000000000000000000..bab812d63b43ea025eaf022ef64d2b6d545b414a --- /dev/null +++ b/example/src/test/resources/testcase/content.xml @@ -0,0 +1,2 @@ + +content \ No newline at end of file diff --git a/example/src/test/resources/testcase/login.csv b/example/src/test/resources/testcase/login.csv new file mode 100644 index 0000000000000000000000000000000000000000..9edd1ac5bcf9820fe5276314e2bcd30d5d9041eb --- /dev/null +++ b/example/src/test/resources/testcase/login.csv @@ -0,0 +1,5 @@ +customerNo,username,password,except +0001,apitest1,123456,token +0001,apitest2,123456,\"status\":true +0002,apitest3,123456,账户已被禁用,请联系管理员 +0002,apitest4,123456,\"status\":false diff --git a/example/src/test/resources/testcase/page.html b/example/src/test/resources/testcase/page.html new file mode 100644 index 0000000000000000000000000000000000000000..703f41101f73ea1ae0f78dd0b491deceb49476e8 --- /dev/null +++ b/example/src/test/resources/testcase/page.html @@ -0,0 +1 @@ +

Page html file

\ No newline at end of file diff --git a/example/src/test/resources/testcase/sample.csv b/example/src/test/resources/testcase/sample.csv new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/example/src/test/resources/testnewbie.properties b/example/src/test/resources/testnewbie.properties new file mode 100644 index 0000000000000000000000000000000000000000..25c9d4d4796e921211fb56cd28575c26348a26ad --- /dev/null +++ b/example/src/test/resources/testnewbie.properties @@ -0,0 +1,62 @@ +logging.config = classpath:logback-spring.xml + +### ------------------- 数据库配置 ------------------- +#连接基本属性 +driverClassName=com.mysql.cj.jdbc.Driver +url=jdbc:mysql://localhost:3306/testnewbie?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true +username=root +password=123456 + +#-------------连接池大小和连接超时参数-------------------------------- +#初始化连接数量:连接池启动时创建的初始化连接数量 +#默认为0 +initialSize=0 + +#最大活动连接数量:连接池在同一时间能够分配的最大活动连接的数量, 如果设置为负数则表示不限制 +#默认为8 +maxTotal=8 + +#最大空闲连接:连接池中容许保持空闲状态的最大连接数量,超过的空闲连接将被释放,如果设置为负数表示不限制 +#默认为8 +maxIdle=8 + +#最小空闲连接:连接池中容许保持空闲状态的最小连接数量,低于这个数量将创建新的连接,如果设置为0则不创建 +#注意:timeBetweenEvictionRunsMillis为正数时,这个参数才能生效。 +#默认为0 +minIdle=0 + +#最大等待时间 +#当没有可用连接时,连接池等待连接被归还的最大时间(以毫秒计数),超过时间则抛出异常,如果设置为<=0表示无限等待 +#默认-1 +maxWaitMillis=-1 + + +### ------------------- 邮件配置 ------------------- +#是否发送邮件 +isSendMail=false +#服务器 +mailHost=smtp.exmail.qq.com +#端口号 +mailPort=25 +#邮箱账号 +mailUsername=xxxxxxx@xxxx.com +#邮箱授权码 +mailPassword=******* +#时间延迟 +mailTimeout=5000 +#发送人 +mailFrom=xxxxxxx@xxxx.com +#邮件主题 +subject=自动化测试报告 +#接收人 +mailTo=xxxxxxx@xxxx.com + +### ------------------- 钉钉群消息配置 ------------------- +#是否发送邮件 +isSendDingDing=false +#钉钉群自定义机器人地址 +dingDingWebhookUrl=https://oapi.dingtalk.com/robot/send?access_token=xxxxxxx +#获取到的加签key(不选就没有) +secret= +#通过手机号码指定“被@人列表” +mobiles=153XXXXXXXX,153XXXXXXXX \ No newline at end of file diff --git a/example/start.bat b/example/start.bat new file mode 100644 index 0000000000000000000000000000000000000000..4460120813b4f61b39f162b1f12daba662b3a9f9 --- /dev/null +++ b/example/start.bat @@ -0,0 +1,42 @@ +@rem 执行测试(需要安装maven命令) +call mvn clean test + +echo. +echo ------------------------------------------------------------------------ +echo errorlevel = %errorlevel% +echo ------------------------------------------------------------------------ +echo. + +set "results=.\target\allure-results\" +if not exist "%results%" ( + goto fail +) else ( + goto succeed +) + +:succeed +@rem 显示趋势图 +xcopy .\allure-report\history .\target\allure-results\history /e /Y /I + +@rem 显示环境 +xcopy .\src\test\resources\allure\environment.properties .\target\allure-results\ /e /Y /I + +@rem 分类 +xcopy .\src\test\resources\allure\categories.json .\target\allure-results\ /e /Y /I + +echo. +echo generate allure-report +echo ------------------------------------------------------------------------ +echo. + +@rem 生成报告(需要安装allure命令) +call allure generate target/allure-results/ -o allure-report --clean +@rem 打开报告(需要安装allure命令) +call allure open allure-report +goto end + +:fail +echo maven error + +:end + diff --git a/maven-install.bat b/maven-install.bat new file mode 100644 index 0000000000000000000000000000000000000000..dae9e1fd57000da3251ae5d762cbf9dd842133dc --- /dev/null +++ b/maven-install.bat @@ -0,0 +1,8 @@ + +mvn clean install -pl ^ +automation-core,^ +automation-interface,^ +automation-ui,^ +automation-email,^ +automation-dingding,^ +automation-agent -am diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c87139bed7ddc84d43d38678a32fad1c8f91d602 --- /dev/null +++ b/pom.xml @@ -0,0 +1,95 @@ + + + + 4.0.0 + cn.testnewbie.automation + automation-parent + 1.0.0 + pom + + automation-core + automation-interface + automation-ui + + automation-email + automation-dingding + automation-agent + + example + example-ui + project-demo + + + 1.8 + UTF-8 + 1.8 + ${maven.compiler.source} + 1.9.1 + 2.13.8 + 1.0.13 + + + + + + org.junit + junit-bom + 5.7.1 + pom + import + + + org.javassist + javassist + 3.25.0-GA + + + cn.hutool + hutool-all + 5.5.7 + + + org.projectlombok + lombok + 1.18.16 + + + io.qameta.allure + allure-junit5 + ${allure.version} + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + ch.qos.logback + logback-core + ${logback.version} + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + + + diff --git a/project-demo/HELP.md b/project-demo/HELP.md new file mode 100644 index 0000000000000000000000000000000000000000..73d3a9a4dad87d48cd318fa01bf70369a75abf1c --- /dev/null +++ b/project-demo/HELP.md @@ -0,0 +1,4 @@ + +# 项目功能 +提供接口给测试用例调用 + diff --git a/project-demo/pom.xml b/project-demo/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9a91dc00e00f342e5a89ac307380350190b6a3ba --- /dev/null +++ b/project-demo/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.5.RELEASE + + + com.testexample + project-demo + 0.0.1-SNAPSHOT + project-demo + 提供接口给测试用例调用,接口都是伪实现的。 + + 1.8 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + junit-jupiter + org.junit.jupiter + + + junit-vintage-engine + org.junit.vintage + + + junit-jupiter-api + org.junit.jupiter + + + + + com.alibaba + fastjson + 1.2.75 + + + org.apache.commons + commons-lang3 + + + junit-jupiter + org.junit.jupiter + + + + + + + org.junit + junit-bom + 5.7.1 + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/project-demo/src/main/java/com/testexample/ProjectDemoApplication.java b/project-demo/src/main/java/com/testexample/ProjectDemoApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..4fa49544aed945560a11d35a05fab0417c9ac977 --- /dev/null +++ b/project-demo/src/main/java/com/testexample/ProjectDemoApplication.java @@ -0,0 +1,16 @@ +package com.testexample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author zhanglx + */ +@SpringBootApplication(scanBasePackages = {"com.testexample"}) +public class ProjectDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(ProjectDemoApplication.class, args); + } + +} diff --git a/project-demo/src/main/java/com/testexample/common/BaseController.java b/project-demo/src/main/java/com/testexample/common/BaseController.java new file mode 100644 index 0000000000000000000000000000000000000000..ac74a4f848e2d03fa0c455fb18650fc4995ff520 --- /dev/null +++ b/project-demo/src/main/java/com/testexample/common/BaseController.java @@ -0,0 +1,59 @@ +package com.testexample.common; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageConversionException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author zhanglx + * @date 2018/12/25 1:00 + */ +public class BaseController { + Logger logger = LoggerFactory.getLogger(BaseController.class); + + public static final String CONTENT_TYPE_FORMED = "application/x-www-form-urlencoded"; + + /** + * 定义exceptionHandler解决未被controller层吸收的exception + * + * @param request + * @param ex + * @return + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.OK) + @ResponseBody + public Object handlerException(HttpServletRequest request, Exception ex) { + logger.error("调用异常:", ex); + + Integer errCode = null; + String errMsg = ""; + + if (ex instanceof BusinessException) { + BusinessException businessException = (BusinessException) ex; + errCode = businessException.getErrCode(); + errMsg = businessException.getErrMsg(); + } else if (ex instanceof MethodArgumentNotValidException) { + MethodArgumentNotValidException exception = (MethodArgumentNotValidException) ex; + errMsg = exception.getBindingResult().getFieldError().getDefaultMessage(); + errCode = EmBusinessError.UNKNOWN_ERROR.getErrCode(); + } else if (ex instanceof HttpMessageConversionException) { + errCode = EmBusinessError.UNKNOWN_ERROR.getErrCode(); + errMsg = EmBusinessError.PARAMETER_VALIDATION_ERROR.getErrMsg(); + } else { + errCode = EmBusinessError.UNKNOWN_ERROR.getErrCode(); + errMsg = EmBusinessError.UNKNOWN_ERROR.getErrMsg(); + } + logger.error(errCode + errMsg); + + return CommonReturnType.create(errCode, errMsg); + } +} diff --git a/project-demo/src/main/java/com/testexample/common/BusinessException.java b/project-demo/src/main/java/com/testexample/common/BusinessException.java new file mode 100644 index 0000000000000000000000000000000000000000..8b9d9afaaf8d10b2fda7ed7ff9ce3921d26dedf5 --- /dev/null +++ b/project-demo/src/main/java/com/testexample/common/BusinessException.java @@ -0,0 +1,50 @@ +package com.testexample.common; + +/** + * 包装器业务 业务异常类实现 + * + * @author zhanglx + * @date 2018/12/25 0:08 + */ +public class BusinessException extends Exception implements CommonError { + + private CommonError commonError; + + /** + * 直接接受EmBusinessError的传参,用户构造业务异常 + * + * @param commonError + */ + public BusinessException(CommonError commonError) { + super(); + this.commonError = commonError; + } + + /** + * 接受自定义errMsg的方式构造业务异常 + * + * @param commonError + * @param errMsg + */ + public BusinessException(CommonError commonError, String errMsg) { + super(errMsg); + this.commonError = commonError; + this.commonError.setErrMsg(errMsg); + } + + @Override + public int getErrCode() { + return this.commonError.getErrCode(); + } + + @Override + public String getErrMsg() { + return this.commonError.getErrMsg(); + } + + @Override + public CommonError setErrMsg(String errMsg) { + this.commonError.setErrMsg(errMsg); + return this; + } +} diff --git a/project-demo/src/main/java/com/testexample/common/CommonError.java b/project-demo/src/main/java/com/testexample/common/CommonError.java new file mode 100644 index 0000000000000000000000000000000000000000..9a504cc35d6d763ccb4b1202c0772509555cadd3 --- /dev/null +++ b/project-demo/src/main/java/com/testexample/common/CommonError.java @@ -0,0 +1,30 @@ +package com.testexample.common; + +/** + * @author zhanglx + * @date 2018/12/24 23:55 + */ +public interface CommonError { + /** + * 错误码 + * + * @return + */ + public int getErrCode(); + + /** + * 错误提示 + * + * @return + */ + public String getErrMsg(); + + /** + * 设置错误提示 + * + * @param errMsg + * @return + */ + public CommonError setErrMsg(String errMsg); + +} diff --git a/project-demo/src/main/java/com/testexample/common/CommonReturnType.java b/project-demo/src/main/java/com/testexample/common/CommonReturnType.java new file mode 100644 index 0000000000000000000000000000000000000000..0ce56c7ededf3d9842203d90cf65b685a05f5f3f --- /dev/null +++ b/project-demo/src/main/java/com/testexample/common/CommonReturnType.java @@ -0,0 +1,84 @@ +package com.testexample.common; + +/** + * @author zhanglx + * @date 2018/12/24 20:11 + */ +public class CommonReturnType { + /** + * 表明对应请求的返回处理结果为“success”或“fail” + */ + private boolean status; + + /** + * 错误码 + */ + private Integer code; + /** + * 信息 + */ + private String message; + + /** + * 若status=success,则data内返回前端需要的json数据 + * 若status=fail,则data内使用通用的错误码格式 + */ + private Object data; + + /** + * 定义个通用的创建方法 + * + * @param result + * @return + */ + public static CommonReturnType create(Object result) { + return create(result, true); + } + + public static CommonReturnType create(Object result, boolean status) { + CommonReturnType type = new CommonReturnType(); + type.setData(result); + type.setStatus(status); + return type; + } + + public static CommonReturnType create(int code, String message) { + CommonReturnType type = new CommonReturnType(); + type.setStatus(false); + type.setCode(code); + type.setMessage(message); + return type; + } + + public boolean isStatus() { + return status; + } + + public void setStatus(boolean status) { + this.status = status; + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } +} diff --git a/project-demo/src/main/java/com/testexample/common/Constants.java b/project-demo/src/main/java/com/testexample/common/Constants.java new file mode 100644 index 0000000000000000000000000000000000000000..cc891b1173c985f6fd60ceaa63876c13609b6fb5 --- /dev/null +++ b/project-demo/src/main/java/com/testexample/common/Constants.java @@ -0,0 +1,18 @@ +package com.testexample.common; + +/** + * @author zhanglx + */ +public class Constants { + + public static final String HELLO = "Hello world"; + public static final String CUSTOMER_NO = "0001"; + public static final String TOKEN = "token"; + public static final String X_TOKEN = "X-Token"; + public static final String USER_NAME_SUFFIX = "接口测试账户"; + + public static final String SUCCESS_TOKEN = "Z07OeMMwAMLLs18Pg"; + public static final String LOGIN_USER_INFO = "{\"userId\": 666,\"userName\":\"apitest接口测试账户\",\"token\":\"Z07OeMMwAMLLs18Pg\",\"lastProjectId\":1001,\"projects\":[{\"projectId\":1001,\"projectName\":\"道路\",\"longitude\":120.202027,\"latitude\":30.185405},{\"projectId\":1002,\"projectName\":\"工厂\",\"longitude\":120.201344,\"latitude\":30.185628}]}"; + public static final String USER_INFO = "{\"userName\":\"apitest接口测试账户\",\"lastProjectId\":1001,\"projects\":[{\"projectId\":1001,\"projectName\":\"道路\",\"longitude\":120.202027,\"latitude\":30.185405},{\"projectId\":1002,\"projectName\":\"工厂\",\"longitude\":120.201344,\"latitude\":30.185628}]}"; + +} diff --git a/project-demo/src/main/java/com/testexample/common/EmBusinessError.java b/project-demo/src/main/java/com/testexample/common/EmBusinessError.java new file mode 100644 index 0000000000000000000000000000000000000000..44d22fa352ad22de5c179b51ea32fd530a01f98e --- /dev/null +++ b/project-demo/src/main/java/com/testexample/common/EmBusinessError.java @@ -0,0 +1,69 @@ +package com.testexample.common; + +/** + * @author zhanglx + * @date 2018/12/24 23:58 + */ +public enum EmBusinessError implements CommonError { + + /** + * 系统错误 + */ + SYSTEM_ERROR(10000, "系统错误"), + /** + * 通用的错误类型10001 + */ + PARAMETER_VALIDATION_ERROR(10001, "参数错误"), + UNKNOWN_ERROR(10002, "未知错误"), + + /** + * 20000开头为用户信息相关错误定义 + */ + USER_NOT_EXIT(20001, "用户不存在"), + USER_NOT_DISABLED(20002, "账户已被禁用,请联系管理员"), + USER_NOT_LOGIN(20003, "用户未登录"), + LOGIN_EXPIRED(20004, "登录超时"), + + + NO_DATA(30001, "暂无数据"), + /** + * 30000开头为socket失败错误定义 + */ + SOCKET_ERROR(30100, "建议网络通信时异常"), + TCP_ERROR(30101, "TCP连接错误,请检查服务器ip、端口是否正确,服务器正常"), + PORT_ERROR(30102, "端口被占用"); + + + private EmBusinessError(int errCode, String errMsg) { + this.errCode = errCode; + this.errMsg = errMsg; + } + + private int errCode; + private String errMsg; + + @Override + public int getErrCode() { + return this.errCode; + } + + @Override + public String getErrMsg() { + return this.errMsg; + } + + @Override + public CommonError setErrMsg(String errMsg) { + this.errMsg = errMsg; + return this; + } + + public static final Boolean isExist(String message) { + for (EmBusinessError anEnum : EmBusinessError.values()) { + if (anEnum.errMsg.equals(message)) { + return Boolean.TRUE; + } + } + return Boolean.FALSE; + } +} diff --git a/project-demo/src/main/java/com/testexample/controller/DemoController.java b/project-demo/src/main/java/com/testexample/controller/DemoController.java new file mode 100644 index 0000000000000000000000000000000000000000..709571f61d0dc2dfba73806c5a239b6620d1c64d --- /dev/null +++ b/project-demo/src/main/java/com/testexample/controller/DemoController.java @@ -0,0 +1,47 @@ +package com.testexample.controller; + +import com.testexample.common.BaseController; +import com.testexample.common.BusinessException; +import com.testexample.common.CommonReturnType; +import com.testexample.model.BaseBO; +import com.testexample.model.LoginBO; +import com.testexample.service.DemoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +/** + * @author zhanglx + */ +@CrossOrigin +@RestController() +@RequestMapping("/api") +public class DemoController extends BaseController { + + @Autowired + private DemoService demoService; + + @RequestMapping("/greeting") + String greeting() { + return demoService.greeting(); + } + + @RequestMapping("/getLocalIps") + public CommonReturnType getLocalIps() { + return CommonReturnType.create(demoService.getLocalIps()); + } + + @RequestMapping("/login") + public CommonReturnType login(@RequestBody LoginBO loginBO) throws BusinessException { + return CommonReturnType.create(demoService.login(loginBO)); + } + + @RequestMapping("/getUserInfo") + public CommonReturnType getUserInfo(@RequestBody BaseBO baseBO) throws BusinessException { + return CommonReturnType.create(demoService.getUserInfo(baseBO.getId())); + } + +} diff --git a/project-demo/src/main/java/com/testexample/model/BaseBO.java b/project-demo/src/main/java/com/testexample/model/BaseBO.java new file mode 100644 index 0000000000000000000000000000000000000000..24274b35d8fb8596e11f3349bd02335fcf9b215f --- /dev/null +++ b/project-demo/src/main/java/com/testexample/model/BaseBO.java @@ -0,0 +1,23 @@ +package com.testexample.model; + +/** + * @author zhanglx + */ +public class BaseBO { + Integer id; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Override + public String toString() { + return "BaseBO{" + + "id=" + id + + '}'; + } +} diff --git a/project-demo/src/main/java/com/testexample/model/LocalAddress.java b/project-demo/src/main/java/com/testexample/model/LocalAddress.java new file mode 100644 index 0000000000000000000000000000000000000000..7693b7fe2c4bec240ae463f950fe8aaf384c90cd --- /dev/null +++ b/project-demo/src/main/java/com/testexample/model/LocalAddress.java @@ -0,0 +1,14 @@ +package com.testexample.model; + +import lombok.Data; + +/** + * @author zhanglx + */ +@Data +public class LocalAddress { + public String address; + public String name; + public String mask; + public String broadcastIp; +} diff --git a/project-demo/src/main/java/com/testexample/model/LoginBO.java b/project-demo/src/main/java/com/testexample/model/LoginBO.java new file mode 100644 index 0000000000000000000000000000000000000000..3384a27231508bcde3e33092ed3082ae2e66e3b8 --- /dev/null +++ b/project-demo/src/main/java/com/testexample/model/LoginBO.java @@ -0,0 +1,50 @@ +package com.testexample.model; + +import javax.validation.constraints.NotNull; + +/** + * @author zhanglx + */ +public class LoginBO { + @NotNull(message = "customerNo不能为空") + private String customerNo; + + @NotNull(message = "account不能为空") + private String account; + + @NotNull(message = "password不能为空") + private String password; + + public String getCustomerNo() { + return customerNo; + } + + public void setCustomerNo(String customerNo) { + this.customerNo = customerNo; + } + + public String getAccount() { + return account; + } + + public void setAccount(String account) { + this.account = account; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public String toString() { + return "LoginBO{" + + "customerNo='" + customerNo + '\'' + + ", account='" + account + '\'' + + ", password='" + password + '\'' + + '}'; + } +} diff --git a/project-demo/src/main/java/com/testexample/model/Project.java b/project-demo/src/main/java/com/testexample/model/Project.java new file mode 100644 index 0000000000000000000000000000000000000000..3812642473feff6eac72280857b7c5a4568236cf --- /dev/null +++ b/project-demo/src/main/java/com/testexample/model/Project.java @@ -0,0 +1,56 @@ +package com.testexample.model; + + +import java.io.Serializable; + +/** + * @author zhanglx + */ +public class Project implements Serializable { + + private int projectId; + private String projectName; + private double longitude; + private double latitude; + + public void setProjectId(int projectId) { + this.projectId = projectId; + } + + public int getProjectId() { + return projectId; + } + + public void setProjectName(String projectName) { + this.projectName = projectName; + } + + public String getProjectName() { + return projectName; + } + + public void setLongitude(double longitude) { + this.longitude = longitude; + } + + public double getLongitude() { + return longitude; + } + + public void setLatitude(double latitude) { + this.latitude = latitude; + } + + public double getLatitude() { + return latitude; + } + + + public Project(int projectId, String projectName, double longitude, double latitude) { + this.projectId = projectId; + this.projectName = projectName; + this.longitude = longitude; + this.latitude = latitude; + } + +} diff --git a/project-demo/src/main/java/com/testexample/model/User.java b/project-demo/src/main/java/com/testexample/model/User.java new file mode 100644 index 0000000000000000000000000000000000000000..d3d1b58ea8af8132ea7a2fc18aa93fd9eb4db8ff --- /dev/null +++ b/project-demo/src/main/java/com/testexample/model/User.java @@ -0,0 +1,75 @@ + +package com.testexample.model; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.List; + + +/** + * @author zhanglx + */ +@Component +public class User { + + private int userId; + + private String userName; + + private String token; + + private int lastProjectId; + + private List projects; + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public int getLastProjectId() { + return lastProjectId; + } + + public void setLastProjectId(int lastProjectId) { + this.lastProjectId = lastProjectId; + } + + public List getProjects() { + return projects; + } + + public void setProjects(List projects) { + this.projects = projects; + } + + @Override + public String toString() { + return "User{" + + "userName='" + userName + '\'' + + ", token='" + token + '\'' + + ", lastProjectId=" + lastProjectId + + ", projects=" + projects + + '}'; + } +} diff --git a/project-demo/src/main/java/com/testexample/service/DemoService.java b/project-demo/src/main/java/com/testexample/service/DemoService.java new file mode 100644 index 0000000000000000000000000000000000000000..fcf69c511a5b15f22bde31f8549f9fa3f6b60edc --- /dev/null +++ b/project-demo/src/main/java/com/testexample/service/DemoService.java @@ -0,0 +1,43 @@ +package com.testexample.service; + +import com.testexample.model.LocalAddress; +import com.testexample.model.LoginBO; +import com.testexample.model.User; +import com.testexample.common.BusinessException; + +import java.util.List; + +/** + * @author zhanglx + */ +public interface DemoService { + + /** + * 返回字符串 + * + * @return + */ + String greeting(); + + /** + * 返回本地主机地址 + * + * @return + */ + List getLocalIps(); + + /** + * 模拟登录 + * @param loginBO + * @return + * @throws BusinessException + */ + User login(LoginBO loginBO) throws BusinessException; + + /** + * 模拟获取用户信息,需要登录 + * @return + * @throws BusinessException + */ + User getUserInfo(Integer userId) throws BusinessException; +} diff --git a/project-demo/src/main/java/com/testexample/service/impl/DemoServiceImpl.java b/project-demo/src/main/java/com/testexample/service/impl/DemoServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..9390b7d259630eeae50d39d8ad41d8b33bc19c40 --- /dev/null +++ b/project-demo/src/main/java/com/testexample/service/impl/DemoServiceImpl.java @@ -0,0 +1,107 @@ +package com.testexample.service.impl; + +import com.testexample.common.BusinessException; +import com.testexample.common.Constants; +import com.testexample.common.EmBusinessError; +import com.testexample.model.LocalAddress; +import com.testexample.model.LoginBO; +import com.testexample.model.Project; +import com.testexample.model.User; +import com.testexample.service.DemoService; +import com.testexample.util.LocalHostUtil; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author zhanglx + */ +@Service +public class DemoServiceImpl implements DemoService { + Logger logger = LoggerFactory.getLogger(DemoServiceImpl.class); + + AtomicInteger loginCount = new AtomicInteger(); + + ConcurrentHashMap loginUser = new ConcurrentHashMap<>(); + + @Override + public User login(LoginBO loginBO) throws BusinessException { + logger.info("登录接口被调用:" + loginCount.incrementAndGet() + "次,account = " + loginBO.getAccount()); + if (!StringUtils.equals(loginBO.getCustomerNo(), Constants.CUSTOMER_NO)) { + throw new BusinessException(EmBusinessError.USER_NOT_DISABLED); + } + + String token = UUID.randomUUID().toString(); + + User user = new User(); + user.setToken(token); + user.setUserName(loginBO.getAccount() + Constants.USER_NAME_SUFFIX); + user.setLastProjectId(1001); + + Project project1 = new Project(1001, "道路", 120.202027, 30.185405); + Project project2 = new Project(1002, "工厂", 120.201344, 30.185628); + ArrayList projects = new ArrayList<>(); + projects.add(project1); + projects.add(project2); + user.setProjects(projects); + + + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + loginUser.put(token, user); + return user; + } + + @Override + public User getUserInfo(Integer userId) throws BusinessException { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()) + .getRequest(); + String token = request.getHeader(Constants.X_TOKEN); + + if (token == null) { + throw new BusinessException(EmBusinessError.USER_NOT_LOGIN); + } + User user = loginUser.get(token); + if (user == null) { + throw new BusinessException(EmBusinessError.LOGIN_EXPIRED); + } + + user.setUserId(userId); + user.setToken(null); + return user; + } + + + @Override + public String greeting() { + return Constants.HELLO; + } + + + @Override + public List getLocalIps() { + try { + return LocalHostUtil.getLocalIps(); + } catch (SocketException e) { + logger.error("获取服务器本地网络信息失败。", e); + return null; + } + } + +} diff --git a/project-demo/src/main/java/com/testexample/util/JSONUtils.java b/project-demo/src/main/java/com/testexample/util/JSONUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..1fa2216f850c5222db738441250dea6efcd2d9b9 --- /dev/null +++ b/project-demo/src/main/java/com/testexample/util/JSONUtils.java @@ -0,0 +1,96 @@ +package com.testexample.util; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; + +/** + * Json工具类 + */ +public final class JSONUtils { + + /** + * Json反序列化至指定类型 + * + * @param json json字符串 + * @param type 指定数据类型 + */ + public static T parse(String json, Type type) { + if (StringUtils.isEmpty(json)) { + return null; + } + return JSONObject.parseObject(json, type); + } + + /** + * Json反序列化至指定类型,该方法用于支持泛型的复杂数据类型 + * + * @param json json字符串 + * @param type 指定数据类型 + */ + public static T parse(String json, TypeReference type) { + if (StringUtils.isEmpty(json)) { + return null; + } + return JSONObject.parseObject(json, type); + } + + + /** + * Json序列化 + * + * @param obj 待序列化对象 + */ + public static String toJSONString(Object obj) { + if (obj == null) { + return null; + } + return JSONObject.toJSONString(obj); + } + + + + /** + * Json反序列化集合 + * + * @param json 待反序列化字符串 + * @param clazz 集合中的数据类型 + * @return Json对象 + */ + public static List toList(String json, Class clazz) { + return JSON.parseArray(json, clazz); + } + + /** + * json对象转换为List + * + * @param objData + * @param clazz List中的实体对象的Class + * @return 对象集合 + */ + public static List toList(Object objData, Class clazz) { + if (objData == null) { + return null; + } + String jsonDataStr = toJSONString(objData); + if (StringUtils.isEmpty(jsonDataStr)) { + return null; + } + return toList(jsonDataStr, clazz); + } + + + /** + * @param json + * @return + */ + public static Map stringToMap(String json) { + return JSONObject.parseObject(json); + } +} + diff --git a/project-demo/src/main/java/com/testexample/util/LocalHostUtil.java b/project-demo/src/main/java/com/testexample/util/LocalHostUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..e5321811a89a0e73023b21212823c523e6f21d40 --- /dev/null +++ b/project-demo/src/main/java/com/testexample/util/LocalHostUtil.java @@ -0,0 +1,258 @@ +package com.testexample.util; + +import com.testexample.model.LocalAddress; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.*; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 本地主机工具类 + * + * @author zhanglx + * @since 2019年11月13日09:04:36 + */ +public class LocalHostUtil { + static Logger logger = LoggerFactory.getLogger(LocalHostUtil.class); + + /** + * 获取主机名称 + * + * @return + * @throws UnknownHostException + */ + public static String getHostName() throws UnknownHostException { + return InetAddress.getLocalHost().getHostName(); + } + + /** + * 获取系统首选IP + * + * @return + * @throws UnknownHostException + */ + @SuppressWarnings("AlibabaLowerCamelCaseVariableNaming") + public static String getLocalIp() throws UnknownHostException { + return InetAddress.getLocalHost().getHostAddress(); + } + + /** + * 获取所有网卡IP,排除回文地址、虚拟地址 + * + * @return + * @throws SocketException + */ + public static List getLocalIps() throws SocketException { + long total = System.currentTimeMillis(); + List localIps = new ArrayList<>(); + + Enumeration enumeration = NetworkInterface.getNetworkInterfaces(); + + logger.info("获取所有网卡IP,步骤1耗时:" + (System.currentTimeMillis() - total)); + while (enumeration.hasMoreElements()) { + NetworkInterface intf = enumeration.nextElement(); + if (intf.isLoopback() || intf.isVirtual()) { + continue; + } + // 获取此网络接口的全部或部分 + List list = intf.getInterfaceAddresses(); + if (list.size() > 0) { + // list里面第一个是IPv4的子网掩码 + InterfaceAddress intAddr = list.get(0); + InetAddress addr = intAddr.getAddress(); + if (addr.isLoopbackAddress() || !addr.isSiteLocalAddress() || addr.isAnyLocalAddress()) { + continue; + } + // 子网掩码的二进制1的个数 + int mask = intAddr.getNetworkPrefixLength(); + StringBuilder maskStr = new StringBuilder(); + int[] maskIp = new int[4]; + for (int i = 0; i < maskIp.length; i++) { + maskIp[i] = (mask >= 8) ? 255 : (mask > 0 ? (mask & 0xff) : 0); + mask -= 8; + maskStr.append(maskIp[i]); + if (i < maskIp.length - 1) { + maskStr.append("."); + } + } + + LocalAddress localAddress = new LocalAddress(); + localAddress.address = addr.getHostAddress(); + localAddress.name = addr.getHostAddress(); + localAddress.mask = maskStr.toString(); + localAddress.broadcastIp = intAddr.getBroadcast().getHostAddress(); + localIps.add(localAddress); + } + } + logger.info("获取所有网卡IP,总耗时:" + (System.currentTimeMillis() - total)); + return localIps; + } + + + public static final String WINDOWS = "windows"; + + /** + * 判断操作系统是否是Windows + * + * @return + */ + public static boolean isWindowsSystem() { + boolean isWindows = false; + String osName = System.getProperty("os.name"); + if (osName.toLowerCase().indexOf(WINDOWS) > -1) { + isWindows = true; + } + return isWindows; + } + + /** + * 检查IP地址是否被占用 + * + * @param userIds + * @param ip + * @return + */ + public static boolean isUsedIp(String ip) { + + // 获取当前程序的运行进对象 + Runtime runtime = Runtime.getRuntime(); + Process process; //声明处理类对象 + String line; //返回行信息 + InputStream is; //输入流 + InputStreamReader isr;// 字节流 + BufferedReader br; + boolean flag = false; + + try { + if (isWindowsSystem()) { + process = runtime.exec("ping " + ip); + // 实例化输入流 + is = process.getInputStream(); + // 把输入流转换成字节流 + isr = new InputStreamReader(is, "gbk"); + } else { + process = runtime.exec("ping -w 3 " + ip); + is = process.getInputStream(); + isr = new InputStreamReader(is); + } + // 从字节中读取文本 + br = new BufferedReader(isr); + + while ((line = br.readLine()) != null) { + logger.info(line); + if (line.toUpperCase().contains("TTL")) { + flag = true; + break; + } + } + is.close(); + isr.close(); + br.close(); + if (flag) { + logger.info("ping通 ... 新ip地址已被占用"); + logger.info("警告:新ip地址已被占用"); + } else { + logger.info("ping不通..."); + logger.info("新ip地址ping不通,发送配置命令。"); + } + } catch (IOException e) { + logger.error("checkIP error:", e); + runtime.exit(1); + } + return flag; + } + + + public static final int NETWORK_LENGTH = 4; + + /** + * 检查IP地址是否正常 + * + * @param networkString + * @return + */ + public static boolean isBadNetworkString(String networkString) { + if (StringUtils.isEmpty(networkString)) { + return true; + } + + String[] networkStrings = networkString.split("\\."); + if (networkStrings.length != NETWORK_LENGTH) { + return true; + } + + for (String string : networkStrings) { + int value = Integer.parseInt(string); + if (value > 256 || value < 0) { + return true; + } + } + return false; + } + + + /** + * 检查IP地址是否正常 + * + * @param networkString + * @return + */ + public static boolean isBadNetworkString2(String networkString) { + String pattern = "((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})(\\.((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})){3}"; + Pattern r = Pattern.compile(pattern); + Matcher m = r.matcher(networkString); + return !m.matches(); + } + + /** + * 将port字符串转为 int + * + * @param portString + * @return + */ + public static int getPort(String portString) { + if (!StringUtils.isEmpty(portString)) { + int portInt = Integer.parseInt(portString); + if (isBadPort(portInt)) { + return 0; + } else { + return portInt; + } + } + return 0; + } + + + public static final int MAX_PORT = 65535; + public static final int MIN_PORT = 1; + + public static boolean isBadPort(int port) { + if (port > MAX_PORT || port < MIN_PORT) { + return true; + } + return false; + } + + + public static void main(String[] args) { + try { + System.out.println("主机是否为Windows系统:" + LocalHostUtil.isWindowsSystem()); + System.out.println("主机名称:" + LocalHostUtil.getHostName()); + System.out.println("系统首选IP:" + LocalHostUtil.getLocalIp()); + System.out.println("系统所有IP:" + LocalHostUtil.getLocalIps()); + } catch (UnknownHostException e) { + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/project-demo/src/main/resources/application.yml b/project-demo/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..3c51134740c1a94ed9b749f5db29bf3a862ac767 --- /dev/null +++ b/project-demo/src/main/resources/application.yml @@ -0,0 +1,2 @@ +server: + port: 9999 diff --git a/project-demo/src/test/java/com/testexample/ProjectDemoApplicationTests.java b/project-demo/src/test/java/com/testexample/ProjectDemoApplicationTests.java new file mode 100644 index 0000000000000000000000000000000000000000..d8afb3db7d1eda595c7886fc5b7c8389531ef50c --- /dev/null +++ b/project-demo/src/test/java/com/testexample/ProjectDemoApplicationTests.java @@ -0,0 +1,13 @@ +package com.testexample; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ProjectDemoApplicationTests { + + @Test + void contextLoads() { + } + +}