zoukankan      html  css  js  c++  java
  • Python3简易接口自动化测试框架设计与实现 实例2

    目录
      1、开发环境
      2、用到的模块
      3、框架设计 ?3.1、流程
      3.2、项目结构
      5、日志打印
      6、接口请求类封装
      7、Excel数据读取
      7.1、读取配置文件
      7.1、编写Excel操作类
      8、用例组装
      9、用例运行结果校验
      10、运行用例
      11 、小结
      1、开发环境
      操作系统:Ubuntu18
      开发工具:IDEA+PyCharm插件
      Python版本:3.6
      2、用到的模块
      requests:用于发送请求
      xlrd:操作Excel,组织测试用例
      smtplib,email:发送测试报告
      logging:日志追踪
      json:数据格式化
      Django:接口开发
      configparser:读取配置文件
      3、框架设计
      3.1、流程
      接口用例是通过Excel来组织的,定义了URL,Request Body等列。执行流程如下:
      使用xlrd工具读取Excel中的信息,拼接成一个个完整的请求。
      接口请求类拿到一个个完整的请求的去执行,这个过程需要记录日志,每一次执行情况都要有迹可循。
      回填测试结果,发送邮件,归档每次的运行结果。更好一点的做法是把历史运行情况做个报表,更直观。
      优点:
      用例通过Excel来组织,不需要编写代码,上手难度小。
      在用例个数不是很多的情况,开发速度快。
      缺点:
      用例依赖是痛点。
      只能支持接口自动化用例。
      Excel中用例无法预先检查正确性,只有跑一把才能知道。
      无法很好地管理大量用例,且不支持团队协同作业,个人用来回归测试或上线后的冒烟测试会是一个不错的选择。
      通过优缺点的对比,可以明显发现这个框架的硬伤其实很多了。所以无论是业界开源的自动化测试框架或企业自研的还没有见过用Excel来组织用例的。值得一提的是个别企业自研的自动化框架非常难用,抑或是把一堆工具简单组合到一起。根本无法提高团队的生产力。不过好的产品也不是一蹴而就的,需要一个持续优化的过程。所以上面用Excel组织用例的框架还是值的玩一玩的,暂且命名为apitest吧。目前比较好的自动化测试框架有unittest,testng,pytest等。
      3.2、项目结构
      testcase:存放测试用例或请求的json文件。
      config:配置文件。
      report:测试报告和日志文件及其归档。
      untils:工具集,send_request用来发送请求,email_tool用来发送邮件,excel_tool用来读取Excel中的数据,check_result用来校验结果,run_main用例执行入口,log_trace用来追踪日志。
    5、日志打印
      采用内置logging模块才记录运行日志,设置日志级别。
      log_trace.log:
       import  logging
      filename = "../report/test_case_run.log"
      logging.basicConfig(level=logging.INFO,
      format='%(asctime)s %(levelname)s1 %(filename)s [line:%(lineno)d]  %(message)s',
      datefmt='%a, %d %b %Y %H:%M:%S',
      filename=filename,
      filemode='w')
      6、接口请求类封装
       安装第三方模块requests
     pip install requests
      定义函数send_request,根据传入的方法类型分别去调用request的get,post,delete,put等方法去发送请求。send_request.py:
       import  requests
      from untils. log_trace import  *
      #发送get请求
      def get_request(url,data=None,headers=None):
      res = requests.get(url=url,data=data,headers=headers)
      return res
      #发送post请求
      def post_request(url,data,headers=None):
      res = requests.post(url=url,data=data,headers=headers)
      return res
      #发送delete请求
      def del_request(url,data=None,headers=None):
      res = requests.delete(url,data=data)
      return res
      #发送put请求
      def put_request(url,data,headers=None):
      pass
      def send_request(method,url,data=None,headers=None):
      try:
      logging.info(headers)
      if headers:
      if method == "GET":
      return get_request(url,data,headers=headers)
      if method == "POST":
      return post_request(url,data=data,headers=headers)
      if method == "DELETE":
      return  del_request(url,data=data,headers=headers)
      #put使用频率低,暂时不写
      if method == "PUT":
      return  put_request(url,data=data,headers=headers)
      else:
      logging.info("Header is null")
      except Exception as e:
      logging.info("send request fail:%s"%e)
      在untils_test.py中编写代码测试send_request方法,代码如下:
       #coding:utf-8
      from untils.send_request import send_request
      def test_send_request():
      url="http://127.0.0.1:9000/articles/"
      headers = {
      "X-Token":"0a6db4e59c7fff2b2b94a297e2e5632e"
      }
      res = send_request("GET",url,headers=headers)
      print(res.json())
      if __name__ == "__main__":
      test_send_request()
      运行结果:
       /usr/bin/python3.6 /home/stephen/IdeaProjects/apitest/untils/untils_test.py
      {'status': 'BS.200', 'all_titles': {'amy1': 'alive', 'modifytest': 'alive', 'addTest': 'alive'}, 'msg': 'query articles sucess.'}
      Process finished with exit code 0
    7、Excel数据读取
      用例是放在Excel中的,用xlrd来读取数据,写数据需要用到xluntils,先安装:
      pip install xlrd
      pip install xluntils
      7.1、读取配置文件
      读取Excel数据,我们需要知道对应的行和列,列相对固定,在配置文件settings中定义,然后读取,行作为参数传入。conf/settings文件中的定义如下:
    [excel]
      case_no=0
      case_name=1
      is_run=2
      case_level=3
      case_header=4
      case_cookies=5
      req_type=6
      case_url=7
      case_body=8
      expect_result=9
      operator=10
      actual_result=11
      test_result=12
    在unitls/load_conf.py中编写读取配置的方法,获取各项列值的方法。lood_conf()函数需要传入两个参数:配置项字符串标识符,配置项类型。比如要读取excel下整数case_url:lood_conf("excel.case_url","int")。class excel_config()下定义返回各项列值的方法。
      完整代码如下:
     import  configparser
      '''
      read conf from setting.conf
      @:parameter:identstr,value_type
      value_type:"int" or "str"
      '''
      def lood_conf(identstr,value_type):
      cf = configparser.ConfigParser()
      cf.read("../config/settings.conf")
      idenlist = identstr.split('.')
      if value_type == "int":
      try:
      value = cf.getint(idenlist[0],idenlist[1])
      return  value
      except (configparser.NoSectionError ,configparser.NoOptionError) as e:
      print(e)
      if value_type == "str":
      try:
      value = cf.get(idenlist[0],idenlist[1])
      return value
      except (configparser.NoSectionError ,configparser.NoOptionError) as e:
      print(e)
      '''
      获取url,request body等的列号
      '''
      class excel_config():
      #获取用例编号的列
      def caseno_col(self):
      return lood_conf("excel.case_no","int")
      def casename_col(self):
      return lood_conf("excel.case_name","int")
      def isrun_col(self):
      #print(lood_conf("excel.is_run","int"))
      return lood_conf("excel.is_run","int")
      def level_col(self):
      return lood_conf("excel.case_level","int")
      def header_col(self):
      return lood_conf("excel.case_header","int")
      def cookies_col(self):
      return lood_conf("excel.case_cookies","int")
      def reqtype_col(self):
      return lood_conf("excel.req_type","int")
      def caseurl_col(self):
      return lood_conf("excel.case_url","int")
      def casebody_col(self):
      return lood_conf("excel.case_body","int")
      def expectresult_col(self):
      return lood_conf("excel.expect_result","int")
      def actualresult_col(self):
      return lood_conf("excel.actual_result","int")
      def testresult_col(self):
      return lood_conf("excel.test_result","int")
      def test_operator_col(self):
      return lood_conf("excel.operator","int")
    7.1、编写Excel操作类
      unitls/excel_tool.py中定义了获取用例编号,用例名称等方法,需要传入行。回写测试结果,回写实际结果方法需要传入两个参数:行,值。完整代码如下:
       #coding:utf-8
      import xlrd
      from untils.log_trace import *
      from xlutils.copy import copy
      from untils.load_conf import excel_config
      class excel_tool():
      def __init__(self,excel_name):
      self.curr_excel = xlrd.open_workbook(excel_name)
      self.table = self.curr_excel.sheet_by_index(0)
      #print(self.table.cell(1,1).value)
      #实例化excel_config
      self.config = excel_config()
      self.rows = self.table.nrows
      self.excel_name = excel_name
    #获取用例编号
      def get_caseno(self,row):
      caseno = self.table.cell(row,self.config.caseno_col()).value
      if caseno:
      return caseno
      else:
      logging.info("case no is null")
      return None
      #获取用例名称
      def get_casename(self,row):
      casename = self.table.cell(row,self.config.casename_col()).value
      return casename
      #获取是否运行标志
      def get_runflag(self,row):
      run_flag = self.table.cell(row,self.config.isrun_col()).value
      return run_flag
      #获取用例级别
      def get_caselevel(self,row):
      caselevel = self.table.cell(row,self.config.level_col()).value
      return caselevel
      #获取请求url
      def get_caseurl(self,row):
      caseurl = self.table.cell(row,self.config.caseurl_col()).value
      return caseurl
      #获取请求body
    #获取请求body
      def get_casebody(self,row):
      case_body = self.table.cell(row,self.config.casebody_col()).value
      return case_body
      #获取header
      def get_headerflag(self,row):
      headerflag = self.table.cell(row,self.config.header_col()).value
      return headerflag
      #获取coocikes
      def get_cookiesflag(self,row):
      cookiesflag = self.table.cell(row,self.config.cookies_col()).value
      return cookiesflag
      #获取请求类型
      def get_methodtype(self,row):
      method_type = self.table.cell(row,self.config.reqtype_col()).value
      return method_type
      #获取预期结果
      def get_expectres(self,row):
      expect_res = self.table.cell(row,self.config.expectresult_col()).value
      return expect_res
      #获取测试结果
    def get_testres(self,row):
      test_res= self.table.cell(row,self.config.testresult_col()).value
      return test_res
      #获取操作符
      def get_operator(self,row):
      operator = self.table.cell(row,self.config.test_operator_col()).value
      return operator
      #回写测试结果到excel
      def write_testres(self,row,value):
      wbook = copy(xlrd.open_workbook(self.excel_name))
      sheet = wbook.get_sheet(0)
      sheet.write(row, self.config.testresult_col(), value)
      wbook.save(self.excel_name)
      #回写实际结果
      def write_actualres(self,row,value):
      wbook = copy(xlrd.open_workbook(self.excel_name))
      sheet = wbook.get_sheet(0)
      sheet.write(row, self.config.actualresult_col(), value)
      wbook.save(self.excel_name)
     8、用例组装
      有了Excel操作类,就可以方便读取数据和回填结果了。接下来,在unitls/run_main.py中来组装用例。组装之前,先获取是否运行的标志:
      运行标志为N,不组装,将用例标记为skiiped,回填测试结果到Excel文件中。
      运行标志为Y,开始组装用例并执行,并对比预期结果和实际结果。
      用例执行通过,将用例标记为pass,回填测试结果和实际结果,实际结果为接口的返回。
      用例执行失败,将用例标记为failed,回填测试结果和实际结果。
      接口鉴权需要用到的headers,先在run_main.py 中写死,这个问题后面解决,在上面的过程中,增加必要的日志,方便定位问题和查看用例的运行日志。完整代码如下:
    #coding:utf-8
      from untils.excel_tool import excel_tool
      from untils.send_request import send_request
      from untils.log_trace import *
      from untils.check_result import CheckResult
      import  json
      headers = {
      "X-Token":"0a6db4e59c7fff2b2b94a297e2e5632e"
      }
      class runner():
      def __init__(self):
      self.excel = excel_tool("../testcase/test.xls")
      self.check = CheckResult()
      def join_case(self):
      global  skip_list,sucess_list,failed_list,skip_list
      sucess_list = []
      sucess_list = []
      failed_list = []
      skip_list = []
      for row in range(1,self.excel.rows):
      no = self.excel.get_caseno(row)
      url = self.excel.get_caseurl(row)
      isrun = self.excel.get_runflag(row)
      name = self.excel.get_casename(row)
      level = self.excel.get_caselevel(row)
      data = self.excel.get_casebody(row)
      expect_res = self.excel.get_expectres(row)
      method = self.excel.get_methodtype(row)
      hasheader = self.excel.get_headerflag(row)
      operator = self.excel.get_operator(row)
      if isrun == "Y":
      logging.info("Begin to run test case : %s,case number :%s" %(name,no))
      logging.info("Request method type is :%s" %method)
      logging.info("Request URL:%s" %url)
      logging.info("Request Body:%s" %json.dumps(json.loads(data),sort_keys=True,indent=2))
      res = send_request(method,url,data=data,headers=headers)
      is_sucess = self.check.cmpdict(eval(expect_res),eval(res.text),operator)
      print(is_sucess)
      if is_sucess:
      sucess_list.append(name)
      #回写测试结果
      self.excel.write_testres(row,"pass")
      #回写实际结果
      self.excel.write_actualres(row,res.text)
      logging.info("Test case %s run sucess." %name)
      else:
      failed_list.append(name)
      print("fail",is_sucess)
      #回写测试结果
      self.excel.write_testres(row,"failed")
      #回写实际结果
      self.excel.write_actualres(row,res.text)
      logging.error("Test case %s run fail." %name)
      logging.info("Response is:%s" %json.dumps(res.json(),sort_keys=True,indent=2))
      else:
      skip_list.append(name)
      self.excel.write_testres(row,"skipped")
      def sum(self):
      total = len(sucess_list)+len(failed_list) + len(skip_list)
      failed = len(failed_list)
      sucess = len(sucess_list)
      logging.info("-----------------------------------------------------------")
      logging.info("本次一共运行:%s 个用例" %total)
      logging.info("本次运行通过:%s 个用例" %sucess)
      logging.info("本次运行跳过:%s 个用例" %len(skip_list))
      logging.info("跳过的用例:%s" %skip_list)
      logging.info("-----------------------------------------------------------")
      9、用例运行结果校验
      在untils/run_main.py中方法cmpdict()是用来校验预期和结果实际结果是否匹配,需要传入三个参数:预期结果字典,实际结果字典,操作符。在check_result.py中编写校验用例结果的方法。目前只支持两种操作符,equal和notequal,预期结果为字典,其中不能嵌套字典。和完整代码如下:
    from untils.log_trace import *
      class  CheckResult():
      def dict_value(self,key,actual):
      try:
      if key in actual:
      return actual[key]
      else:
      for keys in actual:
      return self.dict_value(key,actual[keys])
      except Exception as e:
      logging.error(e)
      return None
      def cmpdict(self,expect,actual,equal):
      logging.info("Begin to check result of  testcase.")
      is_dict = isinstance(expect,dict) and isinstance(actual,dict)
      if is_dict:
      if equal == "equal":
      for key in expect.keys():
      if expect[key] == self.dict_value(key,actual):
      logging.info("%s is equal to %s" %(expect[key],self.dict_value(key,actual)))
      return True
      else:
     
    logging.error("%s is not equal to %s" %(expect[key],self.dict_value(key,actual)))
      return False
      if equal == "notequal":
      for key in expect.keys():
      if key != self.dict_value(key,actual):
      logging.info("%s is not equal to %s" %(expect[key],self.dict_value(key,actual)))
      return True
      else:
      logging.error("%s is equal to %s" %(expect[key],self.dict_value(key,actual)))
      return False
      else:
      logging.error("Operator :%s is not support now,you can define it in file[check_result.py]" %equal)
      else:
      logging.error("Expect or actual  result is not dict,check it in  excel. ")
      10、运行用例
      新建一个名称为test.xls的Excel,将其放到testcase路径下,并在Excel中编写测试用例。接口开发请参考:使用Django开发简单接口:文章增删改查,我准备的用例如下:
     
      在untils/untils_test.py中导入run_mian模块来测试一下:
       from untils.run_main import runner
      if __name__ == "__main__":
      #test_send_request()
      runner = runner()
      runner.join_case()
      runner.sum()
      
    运行untils_test.py,然后去到Excel中查看运行结果:
    report路径下查看测试用例运行日志,如下所示:
     Sat, 11 May 2019 19:37:56 INFO check_result.py [line:16]  Begin to check result of  testcase.
      Sat, 11 May 2019 19:37:56 ERROR check_result.py [line:38]  Operator :e1qual is not support now,you can define it in file[check_result.py]
      Sat, 11 May 2019 19:37:56 INFO run_main.py [line:37]  Begin to run test case : 查询文章,case number :1.0
      Sat, 11 May 2019 19:37:56 INFO run_main.py [line:38]  Request method type is :GET
      Sat, 11 May 2019 19:37:56 INFO run_main.py [line:39]  Request URL:http://127.0.0.1:9000/articles
      Sat, 11 May 2019 19:37:56 INFO run_main.py [line:40]  Request Body:{}
      Sat, 11 May 2019 19:37:56 INFO send_request.py [line:25]  {'X-Token': '0a6db4e59c7fff2b2b94a297e2e5632e'}
      Sat, 11 May 2019 19:37:56 INFO check_result.py [line:16]  Begin to check result of  testcase.
      Sat, 11 May 2019 19:37:56 INFO check_result.py [line:22]  BS.200 is equal to BS.200
      Sat, 11 May 2019 19:37:56 INFO run_main.py [line:52]  Test case 查询文章 run sucess.
      Sat, 11 May 2019 19:37:56 INFO run_main.py [line:62]  Response is:{
      "all_titles": {
      "Hello": "alive",
      "amy1": "alive",
      "modifytest": "alive",
      "useasge of ddt": "alive"
      },
      "msg": "query articles sucess.",
    "status": "BS.200"
      }
      Sat, 11 May 2019 19:37:56 INFO run_main.py [line:37]  Begin to run test case : 新增文章,case number :2.0
      Sat, 11 May 2019 19:37:56 INFO run_main.py [line:38]  Request method type is :POST
      Sat, 11 May 2019 19:37:56 INFO run_main.py [line:39]  Request URL:http://127.0.0.1:9000/articles/
      Sat, 11 May 2019 19:37:56 INFO run_main.py [line:40]  Request Body:{
      "content": "useasge of ddt",
      "title": "useasge of ddt"
      }
      Sat, 11 May 2019 19:37:56 INFO send_request.py [line:25]  {'X-Token': '0a6db4e59c7fff2b2b94a297e2e5632e'}
      Sat, 11 May 2019 19:37:56 INFO check_result.py [line:16]  Begin to check result of  testcase.
      Sat, 11 May 2019 19:37:56 ERROR check_result.py [line:25]  BS.200 is not equal to BS.400
      Sat, 11 May 2019 19:37:56 ERROR run_main.py [line:60]  Test case 新增文章 run fail.
      Sat, 11 May 2019 19:37:56 INFO run_main.py [line:62]  Response is:{
      "msg": "title aleady exist,fail to publish.",
      "status": "BS.400"
      }
      Sat, 11 May 2019 19:37:56 INFO run_main.py [line:37]  Begin to run test case : 修改文章,case number :3.0
    Sat, 11 May 2019 19:37:56 INFO run_main.py [line:38]  Request method type is :POST
      Sat, 11 May 2019 19:37:56 INFO run_main.py [line:39]  Request URL:http://127.0.0.1:9000/articles/7
      Sat, 11 May 2019 19:37:56 INFO run_main.py [line:40]  Request Body:{
      "content": "modify test",
      "title": "modify test"
      }
      Sat, 11 May 2019 19:37:56 INFO send_request.py [line:25]  {'X-Token': '0a6db4e59c7fff2b2b94a297e2e5632e'}
      Sat, 11 May 2019 19:37:57 INFO check_result.py [line:16]  Begin to check result of  testcase.
      Sat, 11 May 2019 19:37:57 ERROR check_result.py [line:25]  BS.200 is not equal to BS.300
      Sat, 11 May 2019 19:37:57 ERROR run_main.py [line:60]  Test case 修改文章 run fail.
      Sat, 11 May 2019 19:37:57 INFO run_main.py [line:62]  Response is:{
      "msg": "article is not exists,fail to modify.",
      "status": "BS.300"
      }
      Sat, 11 May 2019 19:37:57 INFO run_main.py [line:37]  Begin to run test case : 删除文章,case number :4.0
      Sat, 11 May 2019 19:37:57 INFO run_main.py [line:38]  Request method type is :DELETE
      Sat, 11 May 2019 19:37:57 INFO run_main.py [line:39]  Request URL:http://127.0.0.1:9000/articles/7
      Sat, 11 May 2019 19:37:57 INFO run_main.py [line:40]  Request Body:{}
      Sat, 11 May 2019 19:37:57 INFO send_request.py [line:25]  {'X-Token': '0a6db4e59c7fff2b2b94a297e2e5632e'}
      Sat, 11 May 2019 19:37:57 INFO check_result.py [line:16]  Begin to check result of  testcase.
      Sat, 11 May 2019 19:37:57 ERROR check_result.py [line:25]  BS.200 is not equal to BS.300
      Sat, 11 May 2019 19:37:57 ERROR run_main.py [line:60]  Test case 删除文章 run fail.
      Sat, 11 May 2019 19:37:57 INFO run_main.py [line:62]  Response is:{
      "msg": "article is not exists,fail to delete.",
      "status": "BS.300"
      }
      Sat, 11 May 2019 19:37:57 INFO run_main.py [line:74]  -----------------------------------------------------------
      Sat, 11 May 2019 19:37:57 INFO run_main.py [line:75]  本次一共运行:5 个用例
      Sat, 11 May 2019 19:37:57 INFO run_main.py [line:76]  本次运行通过:1 个用例
      Sat, 11 May 2019 19:37:57 INFO run_main.py [line:77]  本次运行跳过:1 个用例
      Sat, 11 May 2019 19:37:57 INFO run_main.py [line:78]  跳过的用例:['新增文章缺少title']
      Sat, 11 May 2019 19:37:57 INFO run_main.py [line:79]  -----------------------------------------------------------
    11 、小结
      框架终于能跑起来了,但是遗留的问题还很多。
      很多地方的代码不够健壮,这个后面慢慢优化。还有用例校验支持的运算符比较少。
      发送邮件模块待完成。
      Headers的问题如何解决?
      如果请求的body比较多,写在Excel是不是很不美观呀?这个可以从固定地方读取文件来完成。
      Excel中测试用例有没有必填项呀?这个可以在运行结果之前进行校验,必填项缺少,不运行。
      最关键的一点,如果第二个用例依赖于第一个用例的返回,用例依赖一直是个痛点,下一篇解决。
      还有很多问题比如,重试机制,耗时的用例设置超时时间,超时默认为失败等等.......
  • 相关阅读:
    Request Payload 和 Form Data 的区别
    es6 字符串模板拼接和传统字符串拼接
    TypeScript
    Jquery的$(document).click() 在iphone手机上失效的问题
    Vuex 是什么?
    什么是JSONP?
    git 放弃本地修改操作
    CSS3+HTML5+JS 实现一个块的收缩&展开动画
    Promise学习笔记(一)
    React@16.13.1配合antd UI使用,自定义主题
  • 原文地址:https://www.cnblogs.com/bzdmz/p/10934173.html
Copyright © 2011-2022 走看看