├── res
├── build.jpg
├── email.jpg
├── report1.png
├── report2.png
├── report3.png
├── report4.png
├── structure.jpg
├── send_email.jpg
├── file_structure.png
├── build.xml
└── jmeter-results-detail-report_21.xsl
├── testCase
└── baidu
│ ├── config_default.txt
│ ├── email_default.txt
│ └── build.xml
├── config.py
├── client
├── config.conf
└── client.py
├── schedule.py
├── config.conf
├── LICENSE
├── beanshell
├── logger.py
├── sendEmail.py
├── README.md
├── server.py
└── testing.py
/res/build.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/build.jpg
--------------------------------------------------------------------------------
/res/email.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/email.jpg
--------------------------------------------------------------------------------
/testCase/baidu/config_default.txt:
--------------------------------------------------------------------------------
1 | ip = 127.0.0.1 // 测试环境IP
2 | port = 9999 // 服务端口
--------------------------------------------------------------------------------
/res/report1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/report1.png
--------------------------------------------------------------------------------
/res/report2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/report2.png
--------------------------------------------------------------------------------
/res/report3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/report3.png
--------------------------------------------------------------------------------
/res/report4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/report4.png
--------------------------------------------------------------------------------
/res/structure.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/structure.jpg
--------------------------------------------------------------------------------
/res/send_email.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/send_email.jpg
--------------------------------------------------------------------------------
/res/file_structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leeyoshinari/ATI_Jmeter/HEAD/res/file_structure.png
--------------------------------------------------------------------------------
/testCase/baidu/email_default.txt:
--------------------------------------------------------------------------------
1 | {
2 | "subject": "百度接口自动化测试报告",
3 | "receiveName": "baidu_all",
4 | "receiveEmail": "aaa@baidu.com,bbb@baidu.com"
5 | }
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # Author: leeyoshinari
4 | import configparser
5 |
6 |
7 | class Config(object):
8 | """读取配置文件"""
9 | def __init__(self):
10 | self.cfg = configparser.ConfigParser()
11 | self.cfg.read('config.conf', encoding='utf-8')
12 |
13 | def getConfig(self, key):
14 | return self.cfg.get('default', key, fallback=None)
15 |
16 | def __del__(self):
17 | pass
18 |
--------------------------------------------------------------------------------
/client/config.conf:
--------------------------------------------------------------------------------
1 | [default]
2 | # 服务端IP和端口
3 | IP = 127.0.0.1
4 | PORT = 12020
5 |
6 | # 日志级别
7 | log_level = INFO
8 | # 日志路径
9 | log_path = logs
10 |
11 | # 端口重启后,是否自动执行测试任务,0为不执行,1为执行
12 | is_start = 1
13 | # 待测系统名称,和测试用例路径下的文件夹名称保持一直,例如testCase路径下的baidu文件夹
14 | # 如有多个独立的待测系统,则用逗号隔开;例如:baidu,tencent。如没用待测系统,可配置为空
15 | # 用逗号隔开的系统名的顺序必须和监听端口的顺序保持一致
16 | server_name = baidu,tencent
17 | # 待测系统启用的端口,通过判断该端口的重启自动执行测试任务。如不需要监听端口,可配置为空
18 | # 如需监听多个端口,用逗号隔开;例如:8001,8002,8003
19 | # 监听端口的顺序和 server_name 的顺序保持一直
20 | # 如果监听的是多个端口,每个端口的重启会触发执行对应的测试任务
21 | server_port = 8001,8002
22 | # 周期性执行测试任务时间间隔,单位:秒。如不需要周期性执行测试任务,需注释该配置项
23 | interval = 3600
24 | # 每天(工作日)定时执行测试任务的时间。如不需要定时执行测试任务,需注释该配置项
25 | timing = 20:20
26 |
--------------------------------------------------------------------------------
/schedule.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # Author: leeyoshinari
4 | import queue
5 | from threading import Thread
6 | from testing import Testing
7 |
8 |
9 | class Scheduler(object):
10 | def __init__(self):
11 | self.testing = Testing()
12 | self.test_task = queue.Queue() # 创建队列
13 |
14 | t = Thread(target=self.worker)
15 | t.start()
16 |
17 | @property
18 | def task(self):
19 | return None
20 |
21 | @task.setter
22 | def task(self, value):
23 | self.test_task.put((self.testing.run, value))
24 |
25 | def worker(self):
26 | """
27 | 从队列中获取测试任务,并开始执行
28 | :return:
29 | """
30 | while True:
31 | func, param = self.test_task.get()
32 | func(param)
33 | self.test_task.task_done()
34 |
--------------------------------------------------------------------------------
/config.conf:
--------------------------------------------------------------------------------
1 | [default]
2 | # 开启服务的IP和端口
3 | host = 127.0.0.1
4 | port = 12020
5 |
6 | # 自动化测试用例执行时间间隔,单位:秒
7 | interval = 1800
8 |
9 | # Jmeter脚本测试用例存放路径
10 | case_path = /home/ATI_Jmeter/testCase
11 | # 测试报告保存路径
12 | report_path = /home/ATI_Jmeter/reports
13 | # 测试记录保存地址,保存开始测试时间,测试任务名称,执行用例数,成功率
14 | record_name = record.txt
15 |
16 | # 如果接口自动化脚本维护在git上,则每次执行测试任务前,自动拉取git上的脚本
17 | # 是否自动从git拉去脚本到本地,0为不用git,1为用git
18 | is_git = 0
19 | # git本地仓库路径
20 | git_path = /home/ATI_Jmeter
21 |
22 | # 是否发送邮件,0为总不发送,1为总是发送,2为仅失败发送,3为仅成功发送
23 | is_email = 0
24 | # SMTP
25 | smtp = smtp.qq.com
26 | # 发件人邮箱
27 | sender_email = aaa@qq.com
28 | # 发件人邮箱登录密码,在`sendEmail.py`文件中第74行设置
29 | # password = 123456
30 | # 发件人的名字
31 | sender_name = aaa
32 | #
33 | # 收件人名字及邮箱地址,对应的是每个系统的文件夹中的email_default.txt文件,该文件为默认文件,实际使用时会复制并重命名成email.txt
34 | # email_default.txt文件中包括邮件主题、收件人姓名和收件人地址;
35 | #
36 |
37 | # 日志级别
38 | log_level = INFO
39 | # 日志路径
40 | log_path = logs
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 leeyoshinari
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/beanshell:
--------------------------------------------------------------------------------
1 | import java.io.*;
2 | import java.util.*;
3 | import org.apache.jmeter.services.FileServer;
4 |
5 | public Map getEnvConf(String filePath) throws IOException {
6 | File file = new File(filePath);
7 | FileReader fileReader = new FileReader(file);
8 | InputStreamReader isr = new InputStreamReader(new FileInputStream(file), "UTF-8");
9 | BufferedReader reader = new BufferedReader(isr);
10 | String txt = "";
11 | int lines = 0;
12 | String content = "";
13 | Map confMap = new HashMap();
14 | while ((txt = reader.readLine()) != null) {
15 | txt = txt.trim();
16 | String[] item = txt.split("=");
17 | String[] itemEnd = item[1].split("//");
18 | confMap.put(item[0].trim(), itemEnd[0].trim());
19 | }
20 | System.out.println(confMap);
21 | reader.close();
22 | fileReader.close();
23 | return confMap;
24 | }
25 |
26 | String baseDir=FileServer.getFileServer().getBaseDir();
27 | String confFileName = "config.txt";
28 | String filePath = baseDir + System.getProperty("file.separator") + confFileName;
29 | Map envConfMap = getEnvConf(filePath);
30 | for(String key : envConfMap.keySet()){
31 | vars.put(key, envConfMap.get(key));
32 | }
33 |
--------------------------------------------------------------------------------
/logger.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding:utf-8 -*-
3 | # Author: leeyoshinari
4 |
5 | import os
6 | import time
7 | import traceback
8 | import logging.handlers
9 | from config import Config
10 |
11 | cfg = Config()
12 | LEVEL = cfg.getConfig('log_level')
13 | log_path = cfg.getConfig('log_path')
14 |
15 | if not os.path.exists(log_path):
16 | os.mkdir(log_path)
17 |
18 | log_level = {
19 | 'DEBUG': logging.DEBUG,
20 | 'INFO': logging.INFO,
21 | 'WARNING': logging.WARNING,
22 | 'ERROR': logging.ERROR,
23 | 'CRITICAL': logging.CRITICAL
24 | }
25 |
26 | logger = logging.getLogger()
27 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s[line:%(lineno)d] - %(message)s')
28 | logger.setLevel(level=log_level.get(LEVEL))
29 |
30 | current_day = time.strftime('%Y-%m-%d')
31 | log_name = os.path.join(log_path, current_day + '.log')
32 |
33 | file_handler = logging.handlers.RotatingFileHandler(filename=log_name, maxBytes=10 * 1024 * 1024, backupCount=7)
34 | # file_handler = logging.StreamHandler()
35 |
36 | file_handler.setFormatter(formatter)
37 | logger.addHandler(file_handler)
38 |
39 |
40 | def handle_exception(errors=(Exception,), is_return=False, default_value=None):
41 | """
42 | Handle exception, throw an exception, or return a value.
43 | :param errors: Exception type
44 | :param is_return: Whether to return 'default_value'. Default False, if exception, don't throw an exception, but return a value.
45 | :param default_value: If 'is_return' is True, return 'default_value'.
46 | :return: 'default_value'
47 | """
48 | def decorator(func):
49 | def decorator1(*args, **kwargs):
50 | if is_return:
51 | try:
52 | return func(*args, **kwargs)
53 | except errors:
54 | logger.error(traceback.format_exc())
55 | return default_value
56 | else:
57 | try:
58 | return func(*args, **kwargs)
59 | except errors:
60 | raise
61 |
62 | return decorator1
63 | return decorator
64 |
--------------------------------------------------------------------------------
/res/build.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | 整体架构图:
6 | 
7 |
8 | 功能:
9 | 1. 使用Jmeter维护接口测试用例;
10 | 2. 使用Ant执行测试任务,并生成测试报告;
11 | 3. 使用Python完成邮件发送及任务调度;
12 | 4. 通过get请求触发任务执行,调度方式灵活;
13 | 5. 具有定时功能,可周期性或者定时执行测试任务;
14 | 6. 通过监控端口,当服务重启后,可自动执行测试任务;
15 | 7. 支持自动从git拉取最新版本;
16 |
17 | 实现:
18 | 1. 使用Ant执行Jmeter脚本,并生成测试报告;
19 | 2. 考虑到邮件正文内容可读性,定制化修改测试报告模板;
20 | 3. 使用正则表达式提取测试报告中的信息,重新组合成邮件正文;
21 | 4. 通过get请求触发测试任务的执行;
22 | 5. 通过多线程和队列的方式执行测试任务;
23 | 6. 使用aiohttp框架启动后台服务,将测试报告加入到静态资源中,可通过链接访问;
24 | 7. 每次执行测试任务前,自动从git拉取最新版本;如git pull时需要登录,需要提前配置免登录;
25 |
26 | 生成的测试报告:
27 | 1. Ant生成的测试报告,长这个样子
28 | 
29 | 2. 邮件收到的测试报告,长这个样子
30 | 
31 |
32 | ### 部署
33 | 1、Jmeter和Ant部署参考网上教程,主要介绍测试报告模板修改和build.xml文件
34 | > 测试报告模板是在jmeter自带模板的基础上修改的,主要修改详见`res`文件夹中的`report`截图,说明如下,其他小的修改这里不赘述,可使用文档比较工具自行比较查看,也可[在线文档比较](http://www.jq22.com/textDifference) ;
35 | >> (1) 截图中标注的修改1和修改5,因为默认模板带有2个png的静态文件,生成测试报告时必须带上这2个静态文件,否则测试报告页面不好看,因此,需要去掉这2个静态文件;
36 | >> (2) 截图中标注的修改2,测试报告一般重点关注测试失败的用例,因此,需要把测试失败的用例展示在前面;
37 | >> (3) 截图中标注的修改3,把标题改成中文,因为测试报告会发给较多的人;如果你在外企工作,可以改成英文,但是相对应的脚本中的正则表达式也需要修改;
38 | >> (4) 截图中标注的修改4,添加了一个空的`span`标签,用于添加自定义的内容,提高发送的测试报告邮件的可读性;
39 | >> (5) 其他未标注出来的修改点,主要是默认模板没有我想看到的数据,把一些没有展示的数据展示出来,把一些“没用的”数据隐藏起来,以及一些样式的修改;
40 |
41 | > build.xml文件如下,具体配置已详细说明。强调:为了方便测试报告统一管理,也为了能够自动发送邮件,所有系统的build.xml中的测试报告路径必须是同一个文件夹
42 | 
43 |
44 | 2、克隆repository
45 | ```git clone https://github.com/leeyoshinari/ATI_Jmeter.git```
46 |
47 | 3、用例编写
48 | 在编写JMeter用例时,需要读取配置文件config_default.txt中的变量,然后将变量传递到JMeter中。
49 | 读取变量的方法是在beanshell中写Java脚本完成的,具体Java脚本详见`beanshell`中的脚本。
50 |
51 | 4、测试用例放置
52 | > (1)所有测试用例放在一个统一的文件夹中,例如`testCase`文件夹;
53 | > (2)针对不同系统的不同测试用例,可单独再放入一个文件夹中管理,例如:百度的测试用例放在`baidu`中、百度的BVT测试用例放在`baidu_bvt`中、腾讯的测试用例放在`tencent`中;
54 | > (3)每个系统的测试用例文件夹中,都需要放一个配置好的`build.xml`文件;注意:所有系统的测试报告路径必须是同一个文件夹;
55 | > (4)测试用例文件夹具体结构如下:
56 | > 
57 |
58 | 强烈建议文件夹及文件名称使用英文
59 | 为什么要按照上面的要求放置测试用例?这样放置方便执行测试任务,通过get或post请求`http://ip:port/run?systemName=baidu`就可以执行百度的测试用例。
60 | get请求,jmeter参数和邮件信息为默认值,post请求传的参数可以覆盖默认值。
61 | post请求传参示例:`{"params": {"ip": "127.0.0.1", "port": "9998"}, "email": {"subject": "腾讯接口自动化测试报告", "receiveName": "tencent_all", "receiveEmail": "aaa@baidu.com"}}`
62 |
63 | 5、修改配置文件config.conf
64 | > (1)线程池大小,建议设置1就够了;如确实调度较多测试用例的执行,可酌情增加;
65 | > (2)测试用例路径和测试报告路径,建议使用绝对路径;其中测试报告路径应和`build.xml`文件中的路径保持一致;
66 | > (3)如接口自动化脚本维护在git上,可配置git本地仓库路径,每次执行任务前,自动从git上拉取最新版本,默认拉取主分支;前提是已经clone到本地了;
67 | > (4)邮件发送配置,请确认SMTP服务配置正确;邮箱登录密码配置,请在`sendEmail.py`文件中第70行设置,如果密码不想让其他人看到,请将该py文件进行编译,或者直接将这个repository打包,具体打包方法,请往下看;
68 |
69 | 6、运行
70 | > Linux:
71 | > ```nohup python3 server.py &```
72 | > Windows
73 | > ```python server.py```
74 |
75 | 7、打包
76 | 经过前5步,如果该repository可以启动,且执行测试任务成功,则可以进行打包,使用pyinstaller进行打包。
77 | pyinstaller安装自行查找教程,须确保安装正确,否则打包会报错,下面直接进行打包:
78 | > (1)进入ATI_Jmeter文件夹,执行命令:
79 | > ```shell
80 | > pyinstaller -F server.py -p schedule.py -p logger.py -p config.py -p sendEmail.py -p testing.py --hidden-import logger --hidden-import schedule --hidden-import config --hidden-import sendEmail --hidden-import testing
81 | > ```
82 | > (2)打包完成后,在当前路径下会生成dist文件夹,进入dist文件夹即可找到可执行文件server;
83 | > (3)将配置文件config.conf拷贝到dist文件夹下,并修改配置文件;
84 | > (4)如需要部署在其他服务器上,可将dist整个文件夹拷贝到其他服务器,启动server
85 | > ```nohup ./server &```
86 |
87 | 8、CI/CD,以Jenkins为例,在Jenkins构建后操作中增加一个get请求,请求的url为`http://IP:PORT/run?systemName=系统名称`,此处系统名称应和testCase用例文件夹中的对应的系统名称保持一致。
88 |
89 | 9、如果你所在的项目还没有用到CI/CD,或者项目本身有较多配置项,每次手动更改配置重启项目后,也想自动执行测试任务;亦或是你不想配置CI/CD,则需要执行客户端;
90 | 进入client文件夹,将脚本和配置文件拷贝到项目所在的服务器上,运行即可,也可以按照步骤6的方式进行打包。
91 |
92 | 修改配置文件config.conf:
93 | > (1)系统名称必须和测试用例文件夹中的名称保持一致,例如可配置成`baidu`;如需测试多个系统,名字用英文逗号`,`隔开;
94 | > (2)系统端口号即系统占用的端口号;如需监控多个系统的端口,端口用英文逗号`,`隔开;
95 | 注意:如测试多个系统,系统名称的排序和系统端口的排序必须保持一致
96 |
97 | ### 注意
98 | 1、如需部署client,部署的服务器必须支持`netstat`命令,以便根据端口号查进程号;仅支持Linux系统;
99 | 2、已经测试的版本:Jmeter-5.2.1、Ant-1.10.7
100 | 3、另外还有一个项目,使用Python编写的接口自动化测试框架,用Excel维护测试用例,感兴趣的话[可以点我](https://github.com/leeyoshinari/ATI) 。
101 |
102 | ### Requirements
103 | 1. aiohttp>=3.6.2
104 | 2. GitPython>=3.1.2
105 | 3. requests
106 | 4. Python 3.7+
107 |
--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # Author: leeyoshinari
4 | import os
5 | import glob
6 | import json
7 | import asyncio
8 | from aiohttp import web
9 | from schedule import Scheduler
10 | from sendEmail import sendMsg
11 | from logger import logger, cfg
12 |
13 |
14 | schedule = Scheduler()
15 | report_path = cfg.getConfig('report_path')
16 | IP = cfg.getConfig('host')
17 | PORT = cfg.getConfig('port')
18 | if not os.path.exists(report_path):
19 | os.mkdir(report_path)
20 |
21 |
22 | async def get_list(request):
23 | """
24 | 获取当前测试用例目录下的系统名,及执行测试任务需要的get请求的url
25 | """
26 | case_path = cfg.getConfig('case_path')
27 | max_num = 0
28 | for i in os.listdir(case_path):
29 | if max_num < len(i):
30 | max_num = len(i)
31 | dirs = [d+' '*(max_num-len(d))+'\t\t'+'http://'+IP+':'+PORT+'/run?systemName='+d for d in os.listdir(case_path) if os.path.isdir(os.path.join(case_path, d))]
32 | return web.Response(body='\n'.join(dirs))
33 |
34 |
35 | async def run(request):
36 | """
37 | run接口,把测试任务放入队列中
38 | :param request:
39 | :return:
40 | """
41 | if request.method == 'GET':
42 | paths = {"method": "GET"}
43 | system_name = request.query.get('systemName') # 待执行测试的系统根路径
44 | case_path = os.path.join(cfg.getConfig('case_path'), system_name) # 测试用例路径
45 | paths.update({"system_name": system_name})
46 | paths.update({"case_path": case_path}) # 测试用例路径
47 | paths.update({"record_path": os.path.join(case_path, cfg.getConfig('record_name'))}) # 测试结果记录路径
48 | paths.update({"build_file": os.path.join(cfg.getConfig('case_path'), system_name, 'build.xml')}) # build.xml路径
49 | paths.update({"email_file": os.path.join(case_path, 'email_default.txt')}) # 邮件默认配置文件
50 | paths.update({"config_file": os.path.join(case_path, 'config_default.txt')}) # jmx执行的配置文件对应的默认配置文件
51 | paths.update({"new_email_file": os.path.join(case_path, 'email.txt')}) # 邮件配置文件
52 | paths.update({"new_config_file": os.path.join(case_path, 'config.txt')}) # jmx执行的配置文件
53 | if os.path.exists(case_path):
54 | if not os.path.exists(paths["record_path"]):
55 | f = open(paths["record_path"], 'a')
56 | f.close()
57 |
58 | if os.path.exists(paths["build_file"]):
59 | schedule.task = paths
60 | return web.Response(
61 | body=json.dumps({'code': 1, 'message': '操作成功,测试任务正在准备执行', 'data': None}, ensure_ascii=False))
62 | else:
63 | return web.Response(body=json.dumps({'code': 0, 'message': 'build.xml文件不存在,测试任务执行失败', 'data': None},
64 | ensure_ascii=False))
65 | else:
66 | return web.Response(body=json.dumps({
67 | 'code': 0, 'message': '未找到与系统名称对应的脚本,请确认系统名称是否正确,脚本是否存在!', 'data': None}, ensure_ascii=False))
68 | else:
69 | post_data = await request.json()
70 | paths = {"method": "POST"}
71 | system_name = request.query.get('systemName') # 待执行测试的系统根路径
72 | case_path = os.path.join(cfg.getConfig('case_path'), system_name) # 测试用例路径
73 | paths.update({"system_name": system_name})
74 | paths.update({"case_path": case_path}) # 测试用例路径
75 | paths.update({"record_path": os.path.join(case_path, cfg.getConfig('record_name'))}) # 测试结果记录路径
76 | paths.update({"build_file": os.path.join(cfg.getConfig('case_path'), system_name, 'build.xml')}) # build.xml路径
77 | paths.update({"email_file": os.path.join(case_path, 'email_default.txt')}) # 邮件默认配置文件
78 | paths.update({"config_file": os.path.join(case_path, 'config_default.txt')}) # jmx执行的配置文件对应的默认配置文件
79 | paths.update({"new_email_file": os.path.join(case_path, 'email.txt')}) # 邮件配置文件
80 | paths.update({"new_config_file": os.path.join(case_path, 'config.txt')}) # jmx执行的配置文件
81 | paths.update({"post_data": post_data})
82 |
83 | if os.path.exists(case_path):
84 | if not os.path.exists(paths["record_path"]):
85 | f = open(paths["record_path"], 'a')
86 | f.close()
87 |
88 | if os.path.exists(paths["build_file"]):
89 | schedule.task = paths
90 | return web.Response(
91 | body=json.dumps({'code': 1, 'message': '操作成功,测试任务正在准备执行', 'data': None}, ensure_ascii=False))
92 | else:
93 | return web.Response(body=json.dumps({'code': 0, 'message': 'build.xml文件不存在,测试任务执行失败', 'data': None},
94 | ensure_ascii=False))
95 | else:
96 | return web.Response(body=json.dumps({
97 | 'code': 0, 'message': '未找到与系统名称对应的脚本,请确认系统名称是否正确,脚本是否存在!', 'data': None}, ensure_ascii=False))
98 |
99 | async def sendEmail(request):
100 | """
101 | get请求,用于发送邮件,用于客户端异常时发送邮件提醒
102 | :param request:
103 | :return:
104 | """
105 | name = request.match_info['name']
106 | port = request.match_info['port']
107 | ind = request.match_info['ind']
108 | IP = request.match_info['IP']
109 | email_file = glob.glob(os.path.join(cfg.getConfig('case_path'), name, 'email_*.txt'))
110 | if len(email_file) == 0:
111 | return web.Response(body=json.dumps({'code': 0, 'message': '没有设置收件人邮箱地址的txt文件,测试任务执行失败', 'data': None}, ensure_ascii=False))
112 | elif len(email_file) > 1:
113 | return web.Response(body=json.dumps({'code': 0, 'message': '应该只有一个收件人邮箱地址的txt文件,但是找到了多个,测试任务执行失败', 'data': None}, ensure_ascii=False))
114 |
115 | if int(ind) == 1:
116 | msg = f'{IP} 服务器上的 {port} 端口已经停了,无法执行 {name} 的接口自动化测试,请及时检查端口状态'
117 | else:
118 | msg = f'{IP} 服务器上的 {name} 接口自动化测试执行异常,请检查测试用例,或手动执行get请求 http://{IP}:{PORT}/run?systemName={name} '
119 | html = f'
此邮件自动发出,请勿回复。
' 122 | try: 123 | sendMsg(html, email_file[0], is_path=False) 124 | return web.Response(body=json.dumps({'code': 1, 'message': '邮件提醒发送成功!', 'data': None}, ensure_ascii=False)) 125 | except Exception as err: 126 | return web.Response(body=json.dumps({'code': 0, 'message': err, 'data': None}, ensure_ascii=False)) 127 | 128 | 129 | async def main(): 130 | app = web.Application() 131 | app.router.add_static('/testReport/', path=report_path) 132 | 133 | app.router.add_route('GET', '/', get_list) 134 | app.router.add_route('*', '/run', run) 135 | # app.router.add_route('GET', '/sendEmail/{name}/{port}/{ind}/{IP}', sendEmail) 136 | 137 | app_runner = web.AppRunner(app) 138 | await app_runner.setup() 139 | site = web.TCPSite(app_runner, IP, int(PORT)) 140 | await site.start() 141 | 142 | 143 | loop = asyncio.get_event_loop() 144 | loop.run_until_complete(main()) 145 | loop.run_forever() 146 | -------------------------------------------------------------------------------- /testing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Author: leeyoshinari 4 | import os 5 | import re 6 | import time 7 | import shutil 8 | from threading import Thread 9 | from git import Repo 10 | from sendEmail import sendMsg 11 | from logger import logger, cfg, handle_exception 12 | 13 | 14 | class Testing(object): 15 | def __init__(self): 16 | self.interval = int(cfg.getConfig('interval')) 17 | self.tasking = [] 18 | 19 | t = Thread(target=self.lookup) 20 | t.start() 21 | 22 | def run(self, paths): 23 | """ 24 | 执行测试任务 25 | :param paths: 字典 26 | :return: 27 | """ 28 | try: 29 | if int(cfg.getConfig('is_git')): 30 | logger.info('准备从git上拉取最新版本') 31 | repo = Repo(cfg.getConfig('git_path')) 32 | remote = repo.remote() 33 | remote.pull() 34 | logger.info('从git上拉取版本成功') 35 | except Exception as err: 36 | logger.error(err) 37 | 38 | build_path = paths["build_file"] 39 | logger.info(f'开始执行测试任务{build_path}') 40 | 41 | try: 42 | if paths["method"] == "GET": 43 | shutil.copy(paths["config_file"], paths["new_config_file"]) # 复制,用于jmx执行 44 | else: 45 | if paths["post_data"].get('params'): 46 | replace_config(paths["config_file"], paths["post_data"]['params'], paths["new_config_file"]) 47 | 48 | report_name = f'{paths["system_name"]}-{int(time.time() * 1000)}' 49 | _ = os.popen('nohup ant -f {} -DReportName={} &'.format(build_path, report_name)) # 执行测试 50 | paths.update({"file_name": report_name}) 51 | paths.update({"start_time": time.time()}) 52 | self.tasking.append(paths) 53 | time.sleep(3) 54 | except Exception as err: 55 | logger.error(err) 56 | 57 | @handle_exception(is_return=True, default_value=False) 58 | def post_deal(self, paths): 59 | msg = self.parse_html(paths["file_name"] + '.html', paths["case_path"]) # 重组html 60 | 61 | sendMsg(msg['fail_case'], paths, failure_num=msg['failure_num']) # 发送邮件 62 | 63 | string = f"{paths['start_time']},{paths['build_file']},{msg['total_num']},{msg['failure_num']}\n" 64 | logger.info(f'写测试记录到本地, {string}') 65 | with open(paths["record_path"], 'a', encoding='utf-8') as f: 66 | f.write(string) 67 | 68 | logger.info('测试完成') 69 | return True 70 | 71 | @handle_exception() 72 | def parse_html(self, file_name, case_path): 73 | """ 74 | 提取自动生成的测试报告中的一些信息,重组测试报告用于邮件发送 75 | :param case_path: 测试用例路径 76 | :param file_name: 测试报告名称 77 | :return: 78 | """ 79 | all_case = os.path.join(cfg.getConfig('report_path'), file_name) # 完整的测试报告路径 80 | fail_case = os.path.join(cfg.getConfig('report_path'), 'send_' + file_name) # 处理好用于邮件发送的测试报告路径 81 | logger.info('开始处理html测试报告{}'.format(all_case)) 82 | with open(all_case, 'r', encoding='utf-8') as f: 83 | htmls = f.readlines() 84 | 85 | html = '' 86 | for line in htmls: 87 | html += line.strip() 88 | 89 | # 提取用例总数,成功率数据 90 | case_num = re.findall('响应时间最大值.*| 累计执行次数' \ 113 | f' | 累计执行用例数 | 累计执行失败用例数 | 执行成功率 |
|---|---|---|---|
| {len(total_num)} | {sum(total_num)} | ' \ 115 | f'{sum(failure_num)} | {ratio}% |
此邮件自动发出,请勿回复。