接口测试 基于 python 的接口测试框架

adonisjph · May 20, 2016 · Last by zaodaotian replied at July 20, 2021 · 4223 hits

项目背景
公司内部的软件采用 B/S 架构,管理实验室数据,实现数据的存储和分析统计。大部分是数据的增删改查,由于还在开发阶段,所以 UI 界面的变化非常快,之前尝试过用 python+selenium 进行 UI 自动化测试,后来发现今天刚写好的脚本第二天前端就改了页面,又得重新去定位元素什么的,消耗大量的精力与时间维护自动化脚本。针对此种情况,对接口测试较为有效。

工具
由于开发那里不能提供后台代码给我,只能通过抓包分析。使用 fiddler 抓包,发现前端与后台都是采用 POST 方法交互,前端 POST 数据,后台返回数据。这样也比较简单粗暴,直接针对每个接口 POST 测试数据,然后观察返回值就好了。

使用 excel 编写测试用例和数据,requests 发送 HTTP 请求。

功能模块
通过 xlrd 库读取 excel 中的测试用例和数据
requests 负责发送数据并接收后台返回的数据
针对测试结果,需要保存测试日志并生成测试报告,采用 python 自带的 logging 记录日志,测试报告采用 html 格式。
同时测试完成后,需要将测试报告发送给相关的开发人员,需要有自动发送邮件的功能

目录结构

代码实现

#通过自带的ConfigParser模块,读取邮件发送的配置文件,作为字典返回
import ConfigParser

def get_conf():
    conf_file = ConfigParser.ConfigParser()

    conf_file.read(os.path.join(os.getcwd(),'conf.ini'))

    conf = {}

    conf['sender'] = conf_file.get("email","sender")

    conf['receiver'] = conf_file.get("email","receiver")

    conf['smtpserver'] = conf_file.get("email","smtpserver")

    conf['username'] = conf_file.get("email","username")

    conf['password'] = conf_file.get("email","password") 

    return conf

配置文件格式
这个 logging 不熟悉的可以 google 一下,还是挺有用的,需要自己配置一下。需要手动新建一个空白的.log 文件。

#此处使用python自带的logging模块,用来作为测试日志,记录测试中系统产生的信息。
import logging,os
log_file = os.path.join(os.getcwd(),'log/sas.log')
log_format = '[%(asctime)s] [%(levelname)s] %(message)s'     #配置log格式
logging.basicConfig(format=log_format, filename=log_file, filemode='w', level=logging.DEBUG)
console = logging.StreamHandler()
console.setLevel(logging.DEBUG)
formatter = logging.Formatter(log_format)
console.setFormatter(formatter)
logging.getLogger('').addHandler(console)

excel 文件如下图

python 读取 excel 跟读取一个二维数组差不多,下标也是从 0 开始

#读取testcase excel文件,获取测试数据,调用interfaceTest方法,将结果保存至errorCase列表中。
import xlrd,hashlib,json

def runTest(testCaseFile):
      testCaseFile = os.path.join(os.getcwd(),testCaseFile)
      if not os.path.exists(testCaseFile):
          logging.error('测试用例文件不存在!')
          sys.exit()
      testCase = xlrd.open_workbook(testCaseFile)
      table = testCase.sheet_by_index(0)
      errorCase = []                #用于保存接口返回的内容和HTTP状态码

      s = None
      for i in range(1,table.nrows):
            if table.cell(i, 9).vale.replace('\n','').replace('\r','') != 'Yes':
                continue
            num = str(int(table.cell(i, 0).value)).replace('\n','').replace('\r','')
            api_purpose = table.cell(i, 1).value.replace('\n','').replace('\r','')
            api_host = table.cell(i, 2).value.replace('\n','').replace('\r','')
            request_method = table.cell(i, 4).value.replace('\n','').replace('\r','')
            request_data_type = table.cell(i, 5).value.replace('\n','').replace('\r','')
            request_data = table.cell(i, 6).value.replace('\n','').replace('\r','')
            encryption = table.cell(i, 7).value.replace('\n','').replace('\r','')
            check_point = table.cell(i, 8).value

            if encryption == 'MD5':              #如果数据采用md5加密,便先将数据加密
                request_data = json.loads(request_data)
                request_data['pwd'] = md5Encode(request_data['pwd'])
            status, resp, s = interfaceTest(num, api_purpose, api_host, request_url, request_data, check_point, request_methon, request_data_type, s)
            if status != 200 or check_point not in resp:            #如果状态码不为200或者返回值中没有检查点的内容,那么证明接口产生错误,保存错误信息。
                errorCase.append((num + ' ' + api_purpose, str(status), 'http://'+api_host+request_url, resp))
        return errorCase


下面的就是接口部分
由于所有的操作必须在系统登录之后进行,一开始没有注意到 cookie 这一点,每读取一个测试用例,都会新建一个 session,导致无法维护上一次请求的 cookie。然后将 cookie 添加入请求头中,但是第二个用例仍然无法执行成功。后来用 fiddler 抓包分析了一下,发现 cookie 的值竟然是每一次操作后都会变化的!!!

所以只能通过 session 自动维护 cookie。
在 interfaceTest 函数中,返回三个值,分别是 HTTP CODE,HTTP 返回值与 session。再将上一次请求的 session 作为入参传入 interfaceTest 函数中,在函数内部判断 session 是否存在,如果不为 None,那么直接利用传入的 session 执行下一个用例,如果为 None,那么新建一个 session。

#接受runTest的传参,利用requests构造HTTP请求
import requests

def interfaceTest(num, api_purpose, api_host, request_method,
                  request_data_type, request_data, check_point, s=None)
     headers = {'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8',
                      'X-Requested-With' : 'XMLHttpRequest',
                      'Connection' : 'keep-alive',
                      'Referer' : 'http://' + api_host,
                      'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36'
                }

     if s == None:
          s = requests.session()
     if request_method == 'POST':
          if request_url != '/login' :
              r = s.post(url='http://'+api_host+request_url, data = json.loads(request_data), headers = headers)         #由于此处数据没有经过加密,所以需要把Json格式字符串解码转换成**Python对象**
          elif request_url == '/login' :
              s = requests.session()
              r = s.post(url='http://'+api_host+request_url, data = request_data, headers = headers)          #由于登录密码不能明文传输,采用MD5加密,在之前的代码中已经进行过json.loads()转换,所以此处不需要解码
     else:
          logging.error(num + ' ' + api_purpose + '  HTTP请求方法错误,请确认[Request Method]字段是否正确!!!')
          s = None
          return 400, resp, s
     status = r.status_code
     resp = r.text
     print resp
     if status == 200 :
        if re.search(check_point, str(r.text)):
            logging.info(num + ' ' + api_purpose + ' 成功,' + str(status) + ', ' + str(r.text))
            return status, resp, s
        else:
            logging.error(num + ' ' + api_purpose + ' 失败!!!,[' + str(status) + '], ' + str(r.text))
            return 200, resp , None
     else:
            logging.error(num + ' ' + api_purpose + '  失败!!!,[' + str(status) + '],' + str(r.text))
            return status, resp.decode('utf-8'), None

import hashlib

def md5Encode(data):
      hashobj = hashlib.md5()
      hashobj.update(data.encode('utf-8'))
      return hashobj.hexdigest()

def sendMail(text):
      mail_info = get_conf()
      sender = mail_info['sender']
      receiver = mail_info['receiver']
      subject = '[AutomationTest]接口自动化测试报告通知'
      smtpserver = mail_info['smtpserver']
      username = mail_info['username']
      password = mail_info['password']
      msg = MIMEText(text,'html','utf-8')
      msg['Subject'] = subject
      msg['From'] = sender
      msg['To'] = ''.join(receiver)
      smtp = smtplib.SMTP()
      smtp.connect(smtpserver)
      smtp.login(username, password)
      smtp.sendmail(sender, receiver, msg.as_string())
      smtp.quit()


def main():
      errorTest = runTest('TestCase/TestCase.xlsx')
      if len(errorTest) > 0:
          html = '<html><body>接口自动化扫描,共有 ' + str(len(errorTest)) + ' 个异常接口,列表如下:' + '</p><table><tr><th style="width:100px;text-align:left">接口</th><th style="width:50px;text-align:left">状态</th><th style="width:200px;text-align:left">接口地址</th><th   style="text-align:left">接口返回值</th></tr>'
          for test in errorTest:
              html = html + '<tr><td style="text-align:left">' + test[0] + '</td><td style="text-align:left">' + test[1] + '</td><td style="text-align:left">' + test[2] + '</td><td style="text-align:left">' + test[3] + '</td></tr>'
              sendMail(html)

if __name__ == '__main__':
    main()

以上就是一个简单的接口测试实现,参考了论坛里的大神们很多东西,还有很多不足之处。例如 html 格式的邮件很简陋,可以考虑生成一个 HTML 的测试报告,作为附件发送。
接口方法只实现了一个 POST。
编码也很随意,基本想到哪写到哪,不规范的地方请指正。

共收到 44 条回复 时间 点赞

学习了

—— 来自 TesterHome 官方 安卓客户端

不错,我觉得比我写的好

adonisjph #3 · May 21, 2016 Author

#2 楼 @xiaoli 互相学习,很简陋的东西

最近我也在写接口测试的框架,图是我写的测试报告,我自己觉得长的还不错,哈哈!楼主可以参考一下

测试报告

#4 楼 @wyb199026 看上去像是用 bootstrap

#5 楼 @zsx10110 是的,用 bootstrap 写起来比较快

adonisjph #7 · May 21, 2016 Author

#4 楼 @wyb199026 ……这么一比,不对,是完全不能比啊,赞赞赞,很漂亮的测试报告

学习了,谢谢分享

—— 来自 TesterHome 官方 安卓客户端

#7 楼 @jphtmt 谢谢称赞,这个就是 bootstrap 的 demo 拼起来的,我用 Django 一起弄出来的,把测试结果用 txt 文本作为缓存,最后生成这个报告之后直接用爬虫爬下来保存本地,这样历史记录也都可以查了

我最近也是在找接口测试这方面的,看了你这个很有帮助啊。
我这边自己写的流程是:
1.从 excel 读取 URL 和 param
2.接受服务器返回的数据,判断数据的数量、类型
3.判断数据的正确性(准备使用正则判断,还没实现)
总的来说获取数据不是太难,关键是拿到数据后如何去操作。然后还有就是报告生成吧。

adonisjph #11 · May 23, 2016 Author

#10 楼 @linbenyuan 你的流程跟我基本一致,数据正确性我也是使用正则判断,但是我的正则写的很简单,拿到数据后就看你的要求了,我只是跟预期的结果进行比对,如果不正确就返回错误,添加到错误的列表中。
报告的生成可以参见楼上用的 bootstrap,但是感觉有点小复杂,我准备用个 pyh 写个简单的稍微漂亮点的页面。

if status == 200 :
   if re.search(check_point, str(r.text)):
       logging.info(num + ' ' + api_purpose + ' 成功,' + str(status) + ', ' + str(r.text))
       return status, resp, s
   else:
       logging.error(num + ' ' + api_purpose + ' 失败!!!,[' + str(status) + '], ' + str(r.text))
       return 200, resp , None
   else:
       logging.error(num + ' ' + api_purpose + '  失败!!!,[' + str(status) + '],' + str(r.text))
       return status, resp.decode('utf-8'), None

第二个 else 缩进有问题吧

我真的很不明白,为什么大多数人要用 excel 管理测试数据
太繁琐了

adonisjph #14 · May 30, 2016 Author

#13 楼 @jacexh 其实我也没有多考虑,就是看之前有人这么做的,就这么做了,你有什么更好的方式吗,分享一下

adonisjph #15 · May 30, 2016 Author

#12 楼 @onecd 恩,我没注意,你看下代码,我已经修改好了

建议使用 nose 或者 pytest 等 python 主流的测试框架,直接使用参数--junit-xml 方法导出标准的 junit 类型的测试报告,然后结合 jenkins 的 report 插件,就可以展示了

adonisjph #18 · June 01, 2016 Author

#17 楼 @cesc nose 和 pytest 是单元测试框架吧

感谢楼主,最近也在看这方面的😄

adonisjph #20 · June 03, 2016 Author

#19 楼 @thanksdanny 希望能帮到你

挺好的,赞!!!

—— 来自 TesterHome 官方 安卓客户端

#14 楼 @jphtmt pyh 是啥,htmlrunner?

#4 楼 @wyb199026 csdn 好像有见过层主的教学,不知道层主是不是叫 IT 印记~

然后非常感谢楼主@jphtmt 为我指了一条明路~~~~

#23 楼 @yiyusixing 额,是的。那是我学习过程的笔记

adonisjph #25 · June 13, 2016 Author

#22 楼 @mads 是一个写 html 文件的库

adonisjph #26 · June 13, 2016 Author

#23 楼 @yiyusixing 我也是参考了别人,自己整理的一点小东西,互相学习吧

#26 楼 @jphtmt 您 qq 多少,我能问点问题么

adonisjph #28 · June 13, 2016 Author

#27 楼 @mads 见我账号

报告生成推荐用 HTMLTestRunner. 可以百度一下. 很不错的东西


额 这两块参数是不是写错了啊

adonisjph #32 · July 07, 2016 Author

#31 楼 @klxiaoqi 没有,re 是一个包,导入后进行正则的。resp 是 http 响应

可以加个 qq 吗 看你账号没找到呢

adonisjph #34 · July 08, 2016 Author

#33 楼 @klxiaoqi 573980664

你好,我目前也在做接口测试这一块,刚入门,遇到一个问题,还望给点思路!
测试数据存放于 excel,sheet 与接口名对应,一个 sheet 页一个接口,每个 sheet 页多条测试数据
请问这种情况下 如何与单元测试结合,如何查看测试结果?望回复

学习啦~~帮我入门,感谢楼主的分享😁 😁

请教下,python 读取接口 Excel 文件,如果接口请求参数为多条的话,对参数格式有要求吗,我目前这样写去请求,接口不返回数据

adonisjph #38 · March 31, 2017 Author
miaoyan 回复

得看你的接口是怎么解析的了,如果是按照 json 解析的, 那么传入标准格式的 json 就好。
最好在写之前,先手动发一些请求到接口看看是怎么样的情况,然后确定自己的数据格式。

Author only
adonisjph · #40 · May 04, 2018 Author
Author only
点点寒彬 回复

报告挺清晰的,就是左右两边的,执行的用例和结果还需要说清楚一下,第一眼没看出来

大佬,请教一下,为什么大部分人封装 requests 都会封装 json.loads()?为什么不先判断返回的状态码是否正确?还有您用例里考虑的这几个参数就足够了吗,会不会有一些参数没有考虑进去啊

红客联盟 回复

看情况的,用到 json 格式的数据,就会用到 json.loads() 方法。
不先判断返回码是因为返回码不一定准确,比如有的系统,当查询不到结果的时候,也会返回 200,然后内容是 无结果 这样,所以当你对接口返回有很清楚的定义时,就直接判断内容就好

大佬,你所在的那个苏州测试圈社团,我想加入,但是点击申请报错,状态码是 500😂

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up