测试开发工具之——Python小工具 :DIFF接口差异化测试,对比不同环境相同参数请求返回结果是否一致

代码仓库地址: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结果。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值