项目背景
公司内部的软件采用 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。
编码也很随意,基本想到哪写到哪,不规范的地方请指正。


↙↙↙阅读原文可查看相关链接,并与作者交流