赞
踩
代码仓库地址:https://gitee.com/submi_to/python_test_development__tools.git
DIFF的应用场景:
手工case、接口测试、接口自动化、UI自动化全做了,但是线上怎么还是老有问题反馈?
服务端系统交接,功能业务逻辑不变,开发语言由Java变为Go语言?又该如何开展测试?
某部门要撤掉,但某些系统保留,突然交接过来,交接期而且很短,对业务代码不熟悉?如何保证质量?
测试环境好好的,部署上线出现问题,如何证明不是漏测?
??
??
等等
首先,我们在开发一个工具之前要考虑它解决了哪些工作中的痛点。在日常的工作中,有一些模块等需要进行重构。本来是成都研发中心的项目,现在要交接到我们北京总部研发团队。然后交接日期是2个月,交接分3次完成,其中交接中还有需求需要开发。
第一次,成都研发出设计、研发方案设计、文档、提供各种支持让我们了解整个系统的组件、架构、数据表结构、关联,我们北京研发团队参与共建代码的评审,有问题一起关注。
第二次,北京侧出设计、研发方案、数据库表设计、文档、进行研发、测试,由武汉研发把关评审,最后武汉研发在上线日前进行主要功能核心功能的回归把关。
第三次,北京侧出设计、进行研发,武汉评审把关,由北京侧全权负责整个项目的开发、测试、上线流程。以及上线后的问题解决处理。他们负责解答支持。
至此以后,成都研发将全部抽出,不在参与该项目的任何工作。也正是在这次交接过程中,应成都要求去做了一个这么一个小工具。自我在工作中并未让事业部去进行实际应用,主要还是依赖接口自动化,只针对一类接口进行不同环境的相同入参请求,通过对比不同环境的返回值来验证本次重构交接是否对该类接口影响作用并不明显。
下面就进入正题,简单讲解一下我用3个小时写的一个小工具。还不是很完善,比如条件判断,注释,异常捕获,日志规范等~,但是先将简单的构思架构分享给大家,希望大家能够长江后浪拍前浪,继续发挥拓展增加数据驱动、容错、前端页面等等。
一、环境文件配置及读取
在所有的框架设计前我总是先会写一个获取项目路径的方法,getBasePath.py
- import os
-
-
- class GetBasePath:
- """
- """
-
- def get_base_path(self):
-
- base_path = os.path.dirname(__file__)
- return base_path
-
-
- if __name__ == '__main__':
- print(GetBasePath().get_base_path())
因为我是要对比两个或者多个环境,相同传参下的响应。故而需要一个配置环境的配置文件(我们的测试环境和生产环境的区别,除了请求的域名不一样,一个指向测试一个指向线上),所以在config下创建一个config.ini文件,内容如下:
有了配置文件,继而需要配置文件的读取,创建一个getConfigInfo.py,代码如下:
假如我们的配置文件如上图,section就相当于[evn],option就相当于等号坐边的值,不懂的可以百度一下Python读取ini配置文件,文章介绍挺多的
- import os
- import configparser
- from getBasePath import GetBasePath
-
- base_path = GetBasePath().get_base_path()
- config_path = os.path.join(base_path, 'config', 'config.ini')
-
-
- class GetConfigInfo:
-
- # 初始化
- def __init__(self, section, option):
- self.section = section
- self.option = option
-
- # 通过section 和option拿到对应值,若有不懂可以搜一下Python读取配置文件ini
- def get_value(self):
- conf = configparser.ConfigParser()
- conf.read(config_path)
- value = conf.get(self.section, self.option)
- return value
-
-
- if __name__ == '__main__':
- config = GetConfigInfo('env', 'preissue')
- print(config.get_value())
运行一下这个文件,我们可以看到,通过写的方法拿到了preissue的值为:http://www.baidu.com
二、我们既然要写测试工具,就需要维护用例脚本。我这简单的写到了Excel中,创建testCase目录,其下创建case.xls
有了用例的Excel,我们需要对Excel进行读写操作,创建reWtExcel.py
- import os
- import xlrd
- from xlutils.copy import copy
- from getBasePath import GetBasePath
-
- xl_top_path = os.path.join(GetBasePath().get_base_path(), 'testCase')
-
-
- class GetExcelInfo:
-
- def __init__(self, xl_name, sheet):
- """
- 初始化
- :param xl_name:
- :param sheet:
- """
- self.xl_name = xl_name
- self.sheet = sheet
- self.xl_path = os.path.join(xl_top_path, self.xl_name)
- self.read_book = xlrd.open_workbook(self.xl_path)
- self.sheets = self.read_book.sheet_by_name(self.sheet)
-
- def get_excel_info(self):
- """
- 读取Excel,返回所有行信息到一个list
- :return:
- """
- value_list = []
- rows = self.sheets.nrows
- for i in range(rows):
- if i != 0:
- value_list.append(self.sheets.row_values(i))
- print(i, self.sheets.row_values(i))
- return value_list
-
- def wt_excel_info(self, i, value):
- """
-
- :param i: 代表插入数据到第几行
- :param value: 代表我们要插入的值
- :return:
- """
- if i != 0:
- write_book = copy(self.read_book) # 利用xlutils.copy下的copy函数复制
- sheet = write_book.get_sheet(self.sheet)
- sheet.write(i, 3, value)
- write_book.save(self.xl_path)
- return '写入成功!'
-
-
- if __name__ == '__main__':
-
- # 插入一条数据,读出来验证读写方法的正确性
- GetExcelInfo('case.xls', 'Sheet1').wt_excel_info(1, 'PASS')
- print(GetExcelInfo('case.xls', 'Sheet1').get_excel_info())
执行一下,看一下结果,打开我们的Excel中看一下数据是否插入成功:
三、有了从配置文件中读取的不同环境的域名,有了Excel中的请求方法,路径、入参,我们就可以拼接了,拼接后要做什么?请求,查看返回。所以我这里创建了一个request.py,不懂的可以百度一下Python requests库。
- import requests
-
-
- class Request():
-
- def __init__(self, method, urls, data):
-
- self.urls = urls
- self.data = data
- self.method = method
-
- def request_get(self):
- result = requests.get(url=self.urls, data=self.data)
- return result.text
-
- def request_post(self):
- result = requests.post(url=self.urls, data=self.data)
- return result.text
-
- def request_all(self):
- """
- 根据传的method来进行不同的请求,并拿到请求后的响应结果
- :return:
- """
- res = ''
- if self.method and self.urls is not None:
- if self.method == 'post':
- res = self.request_post()
- elif self.method == 'get':
- res = self.request_get()
- else:
- print('Excel中的请求method值错误!')
- else:
- return 'method 或 urls 不可未空'
- return res
-
-
- if __name__ == '__main__':
- url = 'http://www.baidu.com'
- re = Request(method='get', urls=url, data=None)
- print(re.request_all())
简单调试运行一下:
四、请求响应的封装好了,内容这么多,怎么验证两个响应的内容到底是否一致?我这用的md5加密 ,两个文本内容一样md5加密后md5串相同。编写一个md5.py
- import hashlib
-
-
- class MD5:
-
- def __init__(self, str1):
- self.str1 = str1.encode('UTF-8')
-
- def md5_str(self):
- result = hashlib.md5(self.str1).hexdigest()
- return result
-
- if __name__ == '__main__':
- str1 = 'fsfsg424%&UHB宋'
- str2 = 'fsfsg424%&UHB宋'
- if MD5(str1).md5_str() == MD5(str2).md5_str():
- print('pass')
这个不细讲了,就是利用hashlib这个库,直接对串进行加密算法,直接看下运行结果吧~
五、添加日志log,因为一个项目接口情况错综复杂,数量较多。如果发现问题,我们不可能通过控制台打印来看具体结果,我们可以把需要的关键信息输出到log中。创建一个log,最开始该log目录下为空,因为我已经生成了日志,所以下面有一个logs文件。然后在项目中创建一个log.py文件
- import os
- import logging
- from logging.handlers import TimedRotatingFileHandler
- from getBasePath import GetBasePath
-
- log_path = os.path.join(GetBasePath().get_base_path(), 'log') # 存放log文件的路径,需要改成对应地址
-
-
- class Logger(object):
- def __init__(self, logger_name='logs…'):
- self.logger = logging.getLogger(logger_name)
- logging.root.setLevel(logging.NOTSET)
- self.log_file_name = 'logs' # 日志文件的名称
- self.backup_count = 5 # 最多存放日志的数量
- # 日志输出级别
- self.console_output_level = 'INFO'
- self.file_output_level = 'DEBUG'
- # 日志输出格式
- self.formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
-
- def get_logger(self):
- """在logger中添加日志句柄并返回,如果logger已有句柄,则直接返回"""
- if not self.logger.handlers: # 避免重复日志
- console_handler = logging.StreamHandler()
- console_handler.setFormatter(self.formatter)
- console_handler.setLevel(self.console_output_level)
- self.logger.addHandler(console_handler)
-
- # 每天重新创建一个日志文件,最多保留backup_count份
- file_handler = TimedRotatingFileHandler(filename=os.path.join(log_path, self.log_file_name), when='D',
- interval=1, backupCount=self.backup_count, delay=True,
- encoding='utf-8')
- file_handler.setFormatter(self.formatter)
- file_handler.setLevel(self.file_output_level)
- self.logger.addHandler(file_handler)
- return self.logger
-
-
- logger = Logger().get_logger()
六、万事具备,只欠东风。所有东西都准备好了,我们开始写我们的runAll.py文件,文件中调用log中的方法,将重要信息存放至日志文件logs
- from getConfigInfo import GetConfigInfo
- from request import Request
- from md5 import MD5
- from reWtExcel import GetExcelInfo
- from log import logger
-
- preissue = GetConfigInfo('env', 'preissue').get_value()
- produce = GetConfigInfo('env', 'produce').get_value()
- excel_all_list = GetExcelInfo('case.xls', 'Sheet1').get_excel_info()
-
-
- def run_all():
- """
- 取出要请求的内容,拼接后请求request,拿到响应后,经过md5加密
- 加密后进行比对,如果一直,就向Excel中result中插入pass
- 如果不一致就插入fail
- :return:
- """
- for i in range(len(excel_all_list)):
- method = excel_all_list[i][0]
- path = excel_all_list[i][1]
- payload = excel_all_list[i][2]
- preissue_url = preissue+path
- produce_url = produce+path
- preissue_req = Request(method=method, urls=preissue_url, data=payload).request_all()
- logger.info('preissue_req:{}'.format(preissue_req[0:10]))
- md5_preissue = MD5(preissue_req).md5_str()
- logger.info('md5_preissue:%s' % md5_preissue)
- produce_req = Request(method=method, urls=produce_url, data=payload).request_all()
- logger.info('produce_req:%s' % preissue_req[0:10])
- md5_produce = MD5(produce_req).md5_str()
- logger.info('md5_produce:%s' % md5_produce)
- logger.info('i::{}'.format(i))
- if md5_preissue == md5_produce:
- logger.info('PASS')
- GetExcelInfo('case.xls', 'Sheet1').wt_excel_info(i + 1, 'PASS')
- else:
- logger.info('FAIL')
- GetExcelInfo('case.xls', 'Sheet1').wt_excel_info(i + 1, 'FAIL')
-
-
- if __name__ == '__main__':
- run_all()
运行一下,查看一下结果,失败fail
查看一下Excel,失败
因为我们配置文件中的两个域名,一个指向百度,一个指向新浪,相同的参数请求,返回结果肯定不一样。修改配置文件中两个域名相同
再次执行:
可以看到这次执行后,全部pass,好了简单的构思和实现就如此。暂且分享大家,希望大家加我微信或者QQ群849102042,多多交流。大家也可以用此思路,对代码进行拓展加固、重构、增加判断、异常处理、注释、异常捕获、日志规范、数据驱动、容错、前端页面等等。
拓展:
1.Excel中的数据可以通过抓取线上日志,将日志中的请求参数数据进行处理后,保存到CVS文件或者Excel
2.接口返回数据,有一些为随机数如server响应中生成的时间戳、随机生成的数字、系统服务间的有条件竞争,这时候就需要对数据进行降噪处理,通过对比将一些不必要的参数过滤。例如:通过对生产环境的稳定版本进行多次请求,比较请求后的差异值,做减法。返回-差异值=需要对比测试验证的值。
3.建议每个接口单独一个.py文件,这样加上多线程可以支持同时跑,提高效率
4.可以了解一下diffy平台
diffy部署
1、克隆代码并构建
git clonehttps://github.com/twitter/diffy.git
cd diffy
./sbt assembly
2、在localhost:9990部署primary(线上稳定版本)的代码
3、在localhost:9991部署secondary(线上稳定版本备份)的代码
4、在localhost:9992部署candidate(测试版本)的代码
5、启动diff服务:
java -jar diffy-server.jar
-candidate=localhost:9992
-master.primary=localhost:9990
-master.secondary=localhost:9991
-service.protocol=http
-serviceName=My-Service
-proxy.port=:8880
-admin.port=:8881
-http.port=:8888
-rootUrl='localhost:8888'
6、对diffy发一些请求
curl localhost:8880/your/application/route?with=queryparams
7、在http://localhost:8888中检查结果
结果展示
如图所示,我们可以看到每个请求在不同节点上的差异之处,如果点击“Exclude Noise”,则可以消除噪声,看到最终的diff结果。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。