• 预发和线上的自动化对比工具微框架


    问题与背景###

    订单导出需要将交易数据通过报表的形式导出并提供下载給商家,供商家发货、对账等。由于交易的场景非常多,承接多个业务(微商城、零售单店、零售连锁版、餐饮),订单类型很多,新老报表的字段覆盖交易、支付、会员、优惠、发货、退款、特定业务等,合计超过100个。每次代码变更(尤其是比较大的改动),如果想要手工验证指定时间段内的绝大多数场景下绝大多数订单类型的所有字段都没有问题,在前端页面点击下载报表,然后手工对比,将是非常大的工作量。因此,迫切需要一个自动化的对比工具,对比变更分支与线上分支的导出报表,找出和分析差异,修复问题。

    为什么选择要在预发而不在QA进行呢? 因为订单导出的准确性不仅包含导出和下载功能(20%),更重要的是数据的准确性(80%)。而QA的数据不一定准确,且涵盖面不广,不准确的数据会导致错误的对比结果,对变更的影响造成很大的干扰,延误时间。 因此,这里直接选择用线上的数据来做对比,有时也会意外发现线上数据的一点问题。

    整体思路###

    先做出一个假定:如果master分支的线上逻辑是没有问题的,那么预发的branch分支导出的结果,应该跟线上保持一致; 如果线上的逻辑有问题,那么预发 branch 分支导出的结果,应该有部分跟线上不一致,且不一致的地方根据推断应该仅跟改动部分有关。 分两种情况:

    • 系统代码优化与重构:逻辑没有改动,那么预发和线上的导出结果应该完全一致。如果有不一致的情况发生,那么需要分析不一致的原因,决定是否可以接受和取舍。
    • 业务逻辑优化:比如在某个场景下,“订单类型”字段原来输出“分销买家订单”,现在需要输出“分销买家订单/拼团订单”,那么导出结果的不一致应该限于“订单类型”。当然,如果有其他报表字段的输出也依赖于“订单类型”字段,那么可能其他字段也会不一致,这时候需要进一步分析。

    整体思路如下:

    • 使用 Python 来完成该任务,因为 Python 非常简洁实用 ,适合做质量要求不是非常高的接口测试工具;
    • 分别往预发和线上发送相同的请求,然后通过导出ID拿到预发请求的文件和线上请求的文件,然后读取并逐字段对比,打印出差异;
    • 将对比结果保存在 /tmp/cmp_export.txt , 发送邮件保存。
    • 不同店铺的不同业务配置的导出测试用例通过一个单独的配置文件来给出,测试用例配置与请求测试功能分离。

    这里使用了闭包的技术来配置化地构造大量测试用例。参阅:Python使用闭包结合配置自动生成函数

    抽离通用部分###

    为了做一个尽可能通用一点的工具框架,需要将通用部分尽可能抽离出来。

    从流程上看: 构造请求 - 发送请求 - 获取结果 - 比较结果并输出 。其中,具体请求会有所不同,获取结果的方式会有所不同,比较结果的方法可能不同。需要自定义请求构造函数、获取结果函数、比较函数。 不过从中也可以抽离通用部分。

    • 可以根据基本请求自动生成批量请求;
    • 发送 http rest 请求获取结果是通用的;
    • 逐行对比字段是通用的。

    这里拆分成五个文件:
    (1) conf.py :工具的配置部分
    (2) common.py :包含可复用的基础功能函数,基本不用动;
    (2) cases.py :是测试用例构造部分;
    (3) export.py :是根据具体业务的定制化部分,需要实现 getFromService, compare 函数。
    (4) test.py :是测试入口,基本不用动。

    源代码###

    test.py : 主测试程序。 只要运行 python test.py 即可。然后看看是否有 diff 。如果没有 diff ,那就说明预发和线上导出结果一致; 如果有 diff ,就需要仔细分析 diff ,找出原因并解决。

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    #-------------------------------------------------------------------------------
    # Name:        test.py
    # Purpose:     test if result from pre is consistent with result from production
    # USAGE:       python test.py
    # When:        before deploy to production
    #              STEP1: login in pre machine and vim test.py, cases.py in your directory ,
    #              enter :set paste ,  copy this script and save ;
    #              STEP2: run test.py
    #
    # Author:      qin.shuq
    #
    # Created:     12/22/2017
    # Copyright:   (c) qin.shuq 2017
    # Licence:     <your licence>
    #-------------------------------------------------------------------------------
    from cases import *
    from export import *
    
    import sys
    import codecs
    import locale
    sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout)
    
    def getResult(req, env):
        serviceUrl = serviceUrlMap[env]
        return getFromService(serviceUrl, req, env)
    
    if __name__ == '__main__':
    
        savedStdout = sys.stdout
    
        mkdir(filedir)
        f_result = open(resultfile, 'w')
        sys.stdout = f_result
    
        allreqs = []
        for reqBuilder in caseGenerateFuncs:
            allreqs.extend(reqBuilder(startTimeParam, endTimeParam))
        for req in allreqs:
            resultPre = getResult(req, 'pre')
            resultProd = getResult(req, 'prod')
            extra = {}          # for customized
            compare(resultProd, resultPre, req, extra)
            print '
    '
    
        print 'success done !'
    
        sendmail('cmp result', resultfile , senderEmail, receiverEmail, smtpServer, smtpPort, loginUser, loginPassword)
    
        f_result.close()
    
        sys.stdout = savedStdout
    

    common.py

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    import requests
    import os
    import json
    import time
    import math
    import urllib2
    import traceback
    
    import smtplib
    from email.mime.text import MIMEText
    from email.mime.multipart import MIMEMultipart
    from email.header import Header
    
    def mkdir(filedir):
        isExists=os.path.exists(filedir)
        if not isExists:
            os.makedirs(filedir)
            return True
        else:
            return False
    
    def divideNParts(total, N):
        '''
           divide [0, total) into N parts:
            return [(0, total/N), (total/N, 2*total/N), ((N-1)*total/N, total)]
        '''
    
        each = total / N
        parts = []
        for index in range(N):
            begin = index*each
            if index == N-1:
                end = total
            else:
                end = begin + each
            parts.append((begin, end))
        return parts
    
    def sendRequest(url, query):
        try:
            r = requests.post(url, data=query, headers={"Content-type":"application/json"})
            return r.json()
        except:
            print '%s' % traceback.format_exc()
            return {}
    
    def getData(url, query):
        try:
            resp = sendRequest(url, query)
            if resp['result'] and resp['data']['success']:
                return resp['data']['data']
            return None
        except:
            print '%s' % traceback.format_exc()
            return None
    
    def download(url, filename):
        f = urllib2.urlopen(url)
        data = f.read()
        with open(filename, "w") as csvFile:
            csvFile.write(data)
        return filename
    
    def getFileLines(filename):
        with open(filename, 'r') as f:
            lines = f.readlines()
        return (filename,lines)
    
    def cmplines(prodLines, preLines, fields, keyIndex=0):
        print 'length: online=%d, pre=%d' % (len(prodLines), len(preLines))
        try:
            for i in range(len(prodLines)):
                online = prodLines[i].strip().split(',')
                preline = preLines[i].strip().split(',')
                for t in range(len(online)):
                    try:
                        if online[t] != preline[t]:
                            print 'diff: field=%s, online=%s, pre=%s, keyValue=%s' % (fields[t], online[t].decode('gb18030'), preline[t].decode('gb18030'), online[keyIndex])
                    except:
                        print 'compare failed. field=%s keyValue=%s %s' % (fields[t], online[keyIndex], traceback.format_exc())
            print 'passed.'
        except:
            print 'compare failed. %s' % traceback.format_exc()
    
    def getSortedFile(originFile, index):
    
        filename = originFile.rsplit('.',1)[0]
        sortedfilename = filename + "_sorted.csv"
        cmd = 'sort -k %d %s > %s' % (index+1, originFile, sortedfilename)
        os.system(cmd)
        return sortedfilename
    
    def sendmail(text, resultfile='', senderEmail='', receiverEmail='', smtpServer='', smtpPort='', loginUser='', loginPassword=''):
        sender = senderEmail
        receivers = receiverEmail
    
        message = MIMEMultipart()
        message['From'] = Header("对比工具", 'utf-8')
        message['To'] =  Header("对比工具", 'utf-8')
        subject = '对比结果'
        message['Subject'] = Header(subject, 'utf-8')
    
        message.attach(MIMEText('对比结果如附件所示', 'plain', 'utf-8'))
    
        att1 = MIMEText(open(resultfile, 'rb').read(), 'base64', 'utf-8')
        att1["Content-Type"] = 'application/octet-stream'
        att1["Content-Disposition"] = 'attachment; filename="export_cmp_result.txt"'
        message.attach(att1)
    
        try:
            smtpObj = smtplib.SMTP(smtpServer, smtpPort)
            smtpObj.login(loginUser, loginPassword)
            smtpObj.sendmail(sender, receivers, message.as_string())
            print "Email Send success!"
        except smtplib.SMTPException:
            print "Email Send failed!"
    

    cases.py

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    #-------------------------------------------------------------------------------
    # Name:        cases.py
    # Purpose:     Provides cases of exports
    #
    # Author:      qin.shuq
    #
    # Created:     12/22/2017
    # Copyright:   (c) qin.shuq 2017
    # Licence:     <your licence>
    #-------------------------------------------------------------------------------
    
    import time
    import math
    import json
    from common import divideNParts
    from conf import *
    
    def buildReq(baseReqTemplate, startTime, endTime, bizId=, templateId=1, field=None, value=None):
        requestId = str(startTime) + "_" + str(endTime) + "_" + str(bizId) + "_" + str(templateId)
        baseReq = json.loads(baseReqTemplate)
        # your request fields 
        baseReq['request_id'] = requestId
        return baseReq
    
    def commonGenerateReqByTime(startTime, endTime, bizId=63077, templateId=1):
        def generateReqByTimeInner(startTime, endTime):
            totalInterval = endTime-startTime
            timeparts = divideNParts(totalInterval, parts)
            timeparts = map(lambda t: (t[0]+startTime, t[1]+startTime), timeparts)
            reqs = []
            for timeRange in timeparts:
                baseReq = buildReq(baseReqStr, timeRange[0], timeRange[1], bizId, templateId)
                reqs.append(json.dumps(baseReq))
            return reqs
        return generateReqByTimeInner
    
    def commonGenerator(startTime, endTime, bizId=63077, templateId=1, field='', values=[]):
        def generateReqInner(startTime, endTime):
            reqs = []
            for val in values:
                baseReq = buildReq(baseReqStr, startTime, endTime, bizId, templateId, field, val)
                reqs.append(json.dumps(baseReq))
            return reqs
        return generateReqInner
    
    
    def generateGenerators(startTime, endTime, configs):
        gvars = globals()
        for (templateId,bizId) in bizIdTemplateIdMap.iteritems():
            if len(configs) == 0:
                funcName = 'generateReqByTime_' + str(bizId) + "_" + str(templateId)
                gvars[funcName] = commonGenerateReqByTime(startTime, endTime, bizId, templateId)
            else:
                for (field, values) in configs.iteritems():
                    funcName = 'generateReqBy_' + str(bizId) + "_" + str(templateId) + "_" + field
                    gvars[funcName] = commonGenerator(startTime, endTime, bizId, templateId, field, values)
    
    def getGenerateFuncs():
        gvars = globals()
        caseGenerators = [ gvars[var] for var in gvars if var.startswith('generateReq')  ]
        print 'case generators: ', [ var for var in gvars if var.startswith('generateReq') ]
        return caseGenerators
    
    
    generateGenerators(startTime, endTime, detailconfigs)
    caseGenerateFuncs = getGenerateFuncs()
    

    export.py 定制化部分

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    from conf import *
    from common import *
    
    def getExportId(url, query):
        exportId = getData(url,query)
        if not exportId:
            return 0
        return int(exportId)
    
    def getExportedFile(url, query, env):
        exportId = getExportId(url, query)
        print 'exportId: ', exportId
        return getByExportId(exportId, env)
    
    def getByExportId(exportId, env):
        # get your fileUrl
        fileUrl = 
        filename = filedir + env + "_" + str(exportId) + ".csv"
        csvFile = download(fileUrl, filename)
        return csvFile
    
    def cmpExportFile(preFile, prodFile, exportReq, templateId=0):
        fields = templateIdFieldsMap[templateId]
        keyIndex = 0
    
        preSortedFile = getSortedFile(preFile, keyIndex)
        prodSortedFile = getSortedFile(prodFile, keyIndex)
        preSorted = getFileLines(preSortedFile)[1]
        prodSorted = getFileLines(prodSortedFile)[1]
        print 'exportReq=[ %s ], prodFile=%s, preFile=%s' % (exportReq, prodSortedFile, preSortedFile)
        cmplines(prodSorted, preSorted, fields, keyIndex)
    
    
    # define your getFromService compare
    
    def getFromService(serviceUrl, query, env):
        return getExportedFile(serviceUrl, query, env)
    
    def compare(resultProd, resultPre, req, extra):
        templateId = json.loads(req)['template_id']
        cmpExportFile(resultPre, resultProd, req, templateId)
    

    conf.py

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    #-------------------------------------------------------------------------------
    # Name:        conf.py
    # Purpose:     Provides confs for cases and biz
    #
    # Author:      qin.shuq
    #
    #-------------------------------------------------------------------------------
    
    import math
    import time
    
    ### time range for comparing ###
    startTimeParam = 1527782400
    endTimeParam = 1530374400
    
    ### serivceHttpUrl ###
    preUrl = 
    prodUrl = 
    queryUrl = 
    serviceUrlMap = {'pre': preUrl, 'prod': prodUrl}
    
    filedir = './files/'
    resultfile = '/tmp/export_cmp.txt'
    
    ### email ###
    senderEmail = ''
    receiverEmail = ['']
    smtpServer = 'smtp.exmail.qq.com'
    smtpPort = 25
    loginUser = ''
    loginPassword = ''
    
    bizId = 
    parts = 2
    
    endTime = math.floor(time.time()) - 300
    startTime = endTime - 600
    
    baseExportReqStr = 
    
    bizIdTemplateIdMap = {}
    
    #如果改动了搜索相关,则需要测试订单搜索,使用该配置
    searchconfigs = {}
    
    # 如果只改动了详情,不需要测试订单搜索,只需要按照时间段来导出预发线上数据进行比较即可。
    detailconfigs = {}
    
    def extractFields(fieldsStr):
        return map(lambda x: x.strip(), fieldsStr.split(','))
    
    templateIdFieldsMap = {}
    

    小结###

    无论大改还是小改,通过运行这个预发和线上对比工具,很大程度上增强了成功发布的信心。可见,预发和线上的自动化对比工具,确实是发布前的最后一道防线。

  • 相关阅读:
    利用JS判断浏览器种类
    Navicat for MySQL导出表结构脚本的方法
    Spring中Quartz的配置及corn表达式
    easyUI中点击datagrid列标题排序
    JAVA中科学计数法转换普通计数法
    MySQL查询结果复制到新表(更新、插入)
    SVN错误:Attempted to lock an already-locked dir的解决
    TMS320VC5509的外部中断
    TMS320VC5509总线驱动LED灯
    TMS320VC5509的USB口通信
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/9277071.html
Copyright © 2020-2023  润新知