接口自动化测试框架(用例自动生成)

项目说明

  • 本框架是一套基于pytest+requests+Python3.7+yaml+Allure+Jenkins+docker而设计的数据驱动接口自动化测试框架,pytest 作为执行器,本框架无需你使用代码编写用例,那你可能会担心万一有接口之间相互依赖,或者说需要登入的token等之类的接口,该如何编写用例呢,在这里告诉你们本框架已经完美解决此问题,所有的一切将在yaml中进行!!本框架实现了在yaml中进行接口用例编写,接口依赖关联,接口断言,自定义测试用例运行顺序,还有很重要的一点,实现了类jmeter函数助手的功能,譬如生成MD5、SHA1、随机定长字符串、时间戳等,只需要你在yaml中使用特殊的写法$Function(arg)$,就能够使用这些函数啦,此外在测试执行过程中,还可以 对失败用例进行多次重试,其重试次数和重试时间间隔可自定义;而且可以根据实际需要扩展接口协议,目前已支持http接口和webservice接口

技术栈

  • requests
  • suds-py3
  • Allure
  • pytest
  • pytest-html
  • yaml
  • logging
  • Jenkins
  • docker
  • 函数助手

环境部署

  • 命令行窗口执行pip install -r requirements.txt 安装工程所依赖的库文件

  • 解压allure-commandline-2.12.1.zip到任意目录中

  • 打开\allure-2.12.1\bin文件夹,会看到allure.bat文件,将此路径添加到系统环境变量path下,这样cmd任意目录都能执行了

  • 在cmd下执行 allure --version ,返回版本信息,allure即安装成功

  • 进入 \Lib\site-packages\allure 下面的utils文件,修改成以下代码:

    1. for suitable_name in suitable_names:
    2. # markers.append(item.get_marker(suitable_name))
    3. markers.append(item.get_closest_marker(suitable_name))

目的是解决pytest运行产生的以下错误:

_pytest.warning_types.RemovedInPytest4Warning: MarkInfo objects are deprecated as they contain merged marks which are hard to deal with correctly.

框架流程图与目录结构图及相关说明

1、框架流程图如下

2、代码目录结构图如下

目录结构说明

  • config ===========> 配置文件
  • common ===========> 公共方法封装,工具类等
  • pytest.ini ==========> pytest的主配置文件,可以改变pytest的默认行为,如运行方式,默认执行用例路径,用例收集规则,定义标记等
  • log ==========> 日志文件
  • report ==========> 测试报告
  • tests ===========> 待测试相关文件,比如测试用例和用例数据等
  • conftest.py ============> 存放测试执行的一些fixture配置,实现环境初始化、数据共享以及环境还原等
  • requirements.txt ============> 相关依赖包文件
  • Main.py =============> 测试用例总执行器
  • RunTest_windows.bat ============> 测试启动按钮

conftest.py配置说明

  • conftest.py文件名字是固定的,不可以做任何修改
  • 不需要import导入conftest.py,pytest用例会自动识别该文件,若conftest.py文件放在根目录下,那么conftest.py作用于整个目录,全局调用
  • 在不同的测试子目录也可以放conftest.py,其作用范围只在该层级以及以下目录生效
  • 所有目录内的测试文件运行前都会先执行该目录下所包含的conftest.py文件
  • conftest.py文件不能被其他文件导入

conftest.py与fixture结合

conftest文件实际应用中需要结合fixture来使用,如下

  • conftest中fixture的scope参数为session时,那么整个测试在执行前会只执行一次该fixture
  • conftest中fixture的scope参数为module时,那么每一个测试文件执行前都会执行一次conftest文件中的fixture
  • conftest中fixture的scope参数为class时,那么每一个测试文件中的测试类执行前都会执行一次conftest文件中的fixture
  • conftest中fixture的scope参数为function时,那么所有文件的测试用例执行前都会执行一次conftest文件中的fixture

conftest应用场景

  • 测试中需共用到的token
  • 测试中需共用到的测试用例数据
  • 测试中需共用到的配置信息
  • 结合 yield 语句,进行运行前环境的初始化和运行结束后环境的清理工作,yield前面的语句相当于unitest中的setup动作,yield后面的语句相当于unitest中的teardown动作,不管测试结果如何,yield后面的语句都会被执行。
  • 当fixture超出范围时(即fixture返回值后,仍有后续操作),通过使用yield语句而不是return,来将值返回(因为return后,说明该函数/方法已结束,return后续的代码不会被执行),如下:

  1. @pytest.fixture(scope="module")
  2. def smtpConnection():
  3. smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
  4. yield smtp_connection # 返回 fixture 值smtp_connection
  5. print("teardown smtp")
  6. smtp_connection.close()

无论测试的异常状态如何,print和close()语句将在模块中的最后一个测试完成执行时执行。

  • 可以使用with语句无缝地使用yield语法(with语句会自动释放资源)

  1. @pytest.fixture(scope="module")
  2. def smtpConnection():
  3. with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection:
  4. yield smtp_connection # 返回smtp_connection对象值

测试结束后, 连接将关闭,因为当with语句结束时,smtp_connection对象会自动关闭。

关联详解

  • 公共关联池:意思就是你可以存储接口的响应值到参数池中,以便后续接口使用;同时也可以在测试执行前,制造一些公共关联值,存储到参数池中,供所有的接口使用;
  • 在yaml测试用例中,可通过填写关联键relevance提取响应字段的键值对到参数池中;当提取单个关联值时,关联键relevance的值为字符串,形如relevance: positon;当提取多个关联值时,关联键relevance的值为列表,同时也可提取响应信息中嵌套字典里的键值对;
  • 引用已经存储的关联值:在下个接口入参中使用形如 ${key}$ 的格式,即可提取参数池中的key对应的value,当然你必须保证关联池中已经存储过该key。

函数助手详解

  • 说明:函数助手是来自Jmeter的一个概念,有了它意味着你能在yaml测试用例或者其他配置文件中使用某些函数动态的生成某些数据,比如随机定长字符、随机定长整型数据、随机浮点型数据、时间戳(10位和13位)、md5加密、SHA1、SHA256、AES加解密等等,引用的格式为 $Function(arg)$
  • 目前支持的函数助手:
  • $MD5(arg)$ =========》 md5加密字符串,arg为待加密的字符串,可传入多个字符串拼接后加密,多个字符串之间用逗号隔开,形如$MD5(asd, we57hk)$
  • $SHA1(arg)$ ==========》 SHA1加密字符串,arg为待加密的字符串,可传入多个字符串拼接后加密,多个字符串之间用逗号隔开
  • $SHA256(arg)$ ==========》 SHA256加密字符串,arg为待加密的字符串,可传入多个字符串拼接后加密,多个字符串之间用逗号隔开
  • $DES(arg, key)$ ==========》 DES加密字符串,arg为待加密的字符串
  • $AES(arg, key, vi)$ ==========》 AES加密字符串,arg为待加密的字符串
  • $RandomString(length)$ =========》 生成定长的随机字符串(含数字或字母),length为字符串长度
  • $GetTime(time_type=now,layout=13timestamp,unit=0,0,0,0,0)$ =========》 生成定长的时间戳,time_type=now表示获取当前时间,layout=13timestamp表示时间戳位数为13位,unit为间隔的时间差

代码设计与功能说明

1、定义运行配置文件 runConfig.yml

该文件主要控制测试的执行方式、模块的功能开关、测试用例的筛选、邮件的配置以及日志的配置,具体如下:


  1. runConfig.yml配置信息
  2. # 自动生成测试用例开关,0 -关, 1 -开,根据接口数据自动生成测试用例和单接口执行脚本; 2 -开,根据手工编写的测试用例,自动生成单接口执行脚本
  3. writeCase_switch: 0
  4. # 本次自动生成的测试用例归属的功能模块(项目名称/功能模块)比如: /icmc/pushes ;若不填,则默认不归类
  5. ProjectAndFunction_path: /icmc/pushes
  6. # 扫描用例路径(相对于TestCases的相对路径),以生成执行脚本;若不填,则默认扫描所有的测试用例(只有自动生成测试用例开关为 2 时,此字段才有效),如 /icmc/pushes
  7. scan_path:
  8. # 执行接口测试开关,0 -关, 1 -开
  9. runTest_switch: 1
  10. # 从上往下逐级筛选
  11. # 待执行项目 (可用表达式:not、and、or)(项目名最好唯一,若多个项目或测试名的前缀或后缀相同,则也会被检测到;检测规则为“包含”)
  12. Project: tests
  13. # 待执行接口,可运行单独接口(填接口名),可运行所有接口(None或者空字符串时,即不填),挑选多接口运行可用表达式:not、and、or ,如 parkinside or GetToken or company
  14. markers:
  15. # 本次测试需排除的产品版本(列表),不填,则默认不排除
  16. product_version:
  17. # 本次测试执行的用例等级(列表),不填,则默认执行所有用例等级;可选['blocker', 'critical', 'normal', 'minor', 'trivial']
  18. case_level:
  19. - blocker
  20. - critical
  21. - normal
  22. - minor
  23. # isRun开关,0 -关, 1 -开 ;关闭时,则用例中的is_run字段无效,即会同时执行is_run为 False 的测试用例
  24. isRun_switch: 1
  25. # 用例运行间隔时间(s)
  26. run_interval: 0
  27. # 本轮测试最大允许失败数,达到最大失败数时,则会立即结束当前测试
  28. maxfail: 20
  29. # 测试结束后,显示执行最慢用例数(如:3,表示显示最慢的三条用例及持续时间)
  30. slowestNum: 3
  31. # 失败重试次数,0表示不重试
  32. reruns: 1
  33. # 失败重试间隔时间(s)
  34. reruns_delay: 0.1
  35. #发送测试报告邮件开关, 0 -关, 1 -开
  36. emailSwitch: 0
  37. #邮件配置
  38. #发件邮箱
  39. smtp_server: smtp.126.com
  40. server_username:XXXX@126.com
  41. server_pwd: XXXXX
  42. #收件人(列表)
  43. msg_to:
  44. - XXX@163.com
  45. - XXX@qq.com
  46. #邮件主题
  47. msg_subject: '[XX项目][测试环境-develop][jira号][接口自动化测试报告]'
  48. #日志级别(字典),由高到低: CRITICAL 、 ERROR 、 WARNING 、 INFO 、 DEBUG
  49. log:
  50. backup: 5
  51. console_level: INFO #控制台日志级别
  52. file_level: DEBUG #文件日志级别
  53. pattern: '%(asctime)s - %(filename)s [line:%(lineno)2d] - %(levelname)s: %(message)s'

2、接口配置文件 apiConfig.ini


  1. [host]
  2. host = 127.0.0.1:12306
  3. MobileCodeWS_host = ws.webxml.com.cn
  4. WeatherWebService_host = www.webxml.com.cn
  5. [header]
  6. header1 = {"Content-Type": "application/json"}
  7. header2 = {"Content-Type": "application/json;charset=UTF-8"}
  8. header3 = {"Content-Type": "application/json", "description": "$RandomString(10)$",
  9. "timestamp": "$GetTime(time_type=now,layout=13timestamp,unit=0,0,0,0,0)$", "sign": "$SHA1(${description}$, ${timestamp}$)$"}
  10. [MySqlDB]
  11. host = localhost
  12. port = 3306
  13. user = root
  14. pwd = root
  15. db = course
  16. charset = utf8
  • 可以针对不同的项目,配置不同的host、header等,通过不同的命名区分,如header1、header2,在yaml测试用例文件中,通过变量名引用即可,比如${host}$${header1}$
  • 在该接口配置文件里的字段值,可以调用函数助手的功能,引用相关函数,比如header3,在其字段值里即引用了函数RandomStringtimestamp产生需要的值,并将值拼接在一起,然后再用加密函数SHA1加密后,传给sign。

3、测试用例的设计

测试用例以yaml格式的文件保存,简洁优雅,表达力又强,用例直接反映了接口的定义、请求的数据以及期望的结果,且将测试用例中的公共部分提取出来,平时只需维护测试数据和期望结果,维护成本低。

yaml测试用例的数据格式如下:

  • http类型接口

  1. # 用例基本信息
  2. test_info:
  3. # 用例标题,在报告中作为一级目录显示,用接口路径倒数第二个字段名作为标题
  4. title: parkinside
  5. # 用例所属产品版本,不填则为None
  6. product_version: icm_v1.0
  7. # 用例等级,优先级,包含blocker, critical, normal, minor,trivial几个不同的等级
  8. case_level: blocker
  9. # 请求的域名,可写死,也可写成模板关联host配置文件,也可写在用例中
  10. host: ${host}$
  11. # 请求地址 选填(此处不填,每条用例必填)
  12. address: /${api}$
  13. # 请求头 选填(此处不填,每条用例必填,如有的话)
  14. headers: ${header1}$
  15. # 请求协议
  16. http_type: http
  17. # 请求类型
  18. request_type: POST
  19. # 参数类型
  20. parameter_type: json
  21. # 是否需要获取cookie
  22. cookies: False
  23. # 是否为上传文件的接口
  24. file: False
  25. # 超时时间
  26. timeout: 20
  27. # 运行顺序,当前接口在本轮测试中的执行次序,1表示第一个运行,-1表示最后一个运行
  28. run_order: 1
  29. # 前置条件,case之前需关联的接口,与test_case类似,关联接口可写多个
  30. premise:
  31. - test_name: 获取token # 必填
  32. info: 正常获取token # 选填
  33. address: /GetToken # 请求接口
  34. http_type: http # 请求协议
  35. request_type: GET # 请求方式
  36. parameter_type: # 参数类型,默认为params类型
  37. headers: {} # 请求头
  38. timeout: 10 # 超时时间
  39. parameter: # 可填实际传递参数,若参数过多,可保存在相应的参数文件中,用test_name作为索引
  40. username: "admin"
  41. password: "123456"
  42. file: False # 是否上传文件,默认false,若上传文件接口,此处为文件相对路径 bool or string
  43. relevance: # 关联的键 list or string ;string时,直接写在后面即可;可提取多个关联值,以列表形式,此处提取的关联值可用于本模块的所有用例
  44. # 测试用例
  45. test_case:
  46. - test_name: parkinside_1
  47. # 用例ID,第一条用例必填,从1开始递增
  48. case_id: 1
  49. # 是否运行用例,不运行为 False ,空值或其它值则运行
  50. is_run:
  51. # 用例描述
  52. info: parkinside test
  53. # 参数保存在单独文件中时,可通过文件路径引入参数
  54. parameter: data_parkinside.json
  55. # 校验列表(期望结果)
  56. check:
  57. expected_result: result_parkinside.json # 期望结果保存在单独文件中时,可通过文件路径引入
  58. check_type: json # 校验类型,这里为json校验
  59. expected_code: 200 # 期望状态码
  60. # 关联作用范围,True表示全局关联,其他值或者为空表示本模块关联
  61. global_relevance:
  62. # 关联键,此处提取的关联值可用于本模块后续的所有用例
  63. relevance:
  64. - userID
  65. - vpl
  66. - test_name: parkinside_2
  67. # 第二条用例
  68. case_id: 2
  69. is_run:
  70. info: parkinside
  71. # 请求的域名
  72. host: 127.0.0.1:12306
  73. # 请求协议
  74. http_type: http
  75. # 请求类型
  76. request_type: POST
  77. # 参数类型
  78. parameter_type: json
  79. # 请求地址
  80. address: /parkinside
  81. # 请求头
  82. headers:
  83. Content-Type: application/json
  84. # 请求参数
  85. parameter:
  86. sign: ${sign}$ # 通过变量引用关联值
  87. vpl: AJ3585
  88. # 是否需要获取cookie
  89. cookies: False
  90. # 是否为上传文件的接口
  91. file: False
  92. # 超时时间
  93. timeout: 20
  94. # 校验列表
  95. check:
  96. check_type: Regular_check #正则校验,多项匹配
  97. expected_code: 200
  98. expected_result:
  99. - '"username": "wuya'
  100. - '"Parking_time_long": "20小时18分钟"'
  101. - '"userID": 22'
  102. - '"Parking fee": "20\$"'
  103. global_relevance:
  104. # 关联键
  105. relevance:
  106. - test_name: parkinside_3
  107. # 第三条用例
  108. case_id: 3
  109. # 是否运行用例
  110. is_run:
  111. # 用例描述
  112. info: parkinside
  113. # 请求参数
  114. parameter:
  115. vpl: ${vpl}$
  116. userID: ${userID}$
  117. # 校验列表
  118. check:
  119. expected_result: result_parkinside.json # 期望结果保存在单独文件中时,可通过文件路径引入
  120. check_type: only_check_status
  121. expected_code: 400
  122. global_relevance:
  123. # 关联键
  124. relevance:
  • webservice类型接口1

  1. # 用例基本信息
  2. test_info:
  3. # 用例标题
  4. title: MobileCodeWS_getMobileCodeInfo
  5. # 用例所属产品版本
  6. product_version: icm_v5.0
  7. # 用例等级
  8. case_level: normal
  9. # 请求的域名,可写死,也可写成模板关联host配置文件,也可写在用例中
  10. host: ${MobileCodeWS_host}$
  11. # 请求地址 选填(此处不填,每条用例必填)
  12. address: /WebServices/MobileCodeWS.asmx?wsdl
  13. # 请求头 选填(此处不填,每条用例必填,如有的话)
  14. headers:
  15. # 请求协议
  16. http_type: http
  17. # 请求类型
  18. request_type: SOAP
  19. # webservice接口里的函数名
  20. function_name: getMobileCodeInfo
  21. # 参数类型(get请求一般为params,该值可不填)
  22. parameter_type:
  23. # 是否需要获取cookie
  24. cookies: False
  25. # 是否为上传文件的接口
  26. file: False
  27. # 超时时间(s),SOAP默认超时连接为90s
  28. timeout: 100
  29. # 运行顺序
  30. run_order:
  31. # 前置条件,case之前需关联的接口
  32. premise:
  33. # 测试用例
  34. test_case:
  35. - test_name: getMobileCodeInfo_1
  36. # 用例ID
  37. case_id: 1
  38. is_run:
  39. # 用例描述
  40. info: getMobileCodeInfo test
  41. # 请求参数
  42. parameter:
  43. mobileCode: "18300000000"
  44. userID: ""
  45. # 校验列表
  46. check:
  47. check_type: equal
  48. expected_result: result_getMobileCodeInfo.json
  49. expected_code:
  50. global_relevance:
  51. # 关联键
  52. relevance:
  53. - test_name: getMobileCodeInfo_2
  54. case_id: 2
  55. is_run:
  56. info: getMobileCodeInfo test
  57. # 请求参数
  58. parameter:
  59. mobileCode: "18300000000"
  60. userID: ""
  61. # 校验列表
  62. check:
  63. check_type: equal
  64. expected_result: result_getMobileCodeInfo.json
  65. expected_code:
  66. global_relevance:
  67. # 关联键
  68. relevance:
  69. - test_name: getMobileCodeInfo_3
  70. case_id: 3
  71. is_run:
  72. info: getMobileCodeInfo test
  73. # 请求参数
  74. parameter:
  75. mobileCode: "18300000000"
  76. userID: ""
  77. # 校验列表
  78. check:
  79. check_type: Regular
  80. expected_result:
  81. - '18300000000:广东'
  82. - '深圳 广东移动全球通卡'
  83. expected_code:
  84. global_relevance:
  85. # 关联键
  86. relevance:
  87. - test_name: getMobileCodeInfo_4
  88. case_id: 4
  89. is_run:
  90. info: getMobileCodeInfo test
  91. parameter:
  92. mobileCode: "18300000000"
  93. userID: ""
  94. # 校验列表
  95. check:
  96. check_type: no_check
  97. expected_code:
  98. expected_result:
  99. global_relevance:
  100. # 关联键
  101. relevance:
  • webservice类型接口2

  1. # 用例基本信息
  2. test_info:
  3. # 用例标题
  4. title: MobileCodeWS_getMobileCodeInfo
  5. # 用例所属产品版本
  6. product_version: icm_v5.0
  7. # 用例等级
  8. case_level: normal
  9. # 请求的域名,可写死,也可写成模板关联host配置文件,也可写在用例中
  10. host: ${WeatherWebService_host}$
  11. # 请求地址 选填(此处不填,每条用例必填)
  12. address: /WebServices/WeatherWebService.asmx?wsdl
  13. # 请求过滤地址
  14. filter_address: http://WebXml.com.cn/
  15. # 请求头 选填(此处不填,每条用例必填,如有的话)
  16. headers:
  17. # 请求协议
  18. http_type: http
  19. # 请求类型
  20. request_type: soap_with_filter
  21. # webservice接口里的函数名
  22. function_name: getSupportCity
  23. # 参数类型
  24. parameter_type:
  25. # 是否需要获取cookie
  26. cookies: False
  27. # 是否为上传文件的接口
  28. file: False
  29. # 超时时间(s),SOAP默认超时连接为90s
  30. timeout: 100
  31. # 运行顺序
  32. run_order:
  33. # 前置条件,case之前需关联的接口
  34. premise:
  35. # 测试用例
  36. test_case:
  37. - test_name: getSupportCity_1
  38. # 用例ID
  39. case_id: 1
  40. is_run:
  41. # 用例描述
  42. info: getSupportCity test
  43. # 请求参数
  44. parameter:
  45. byProvinceName: "四川"
  46. # 校验列表
  47. check:
  48. check_type: Regular
  49. expected_result:
  50. - '成都 (56294)'
  51. - '广元 (57206)'
  52. expected_code:
  53. global_relevance:
  54. # 关联键
  55. relevance:
  56. - test_name: getSupportCity_2
  57. case_id: 2
  58. is_run:
  59. info: getSupportCity test
  60. parameter:
  61. byProvinceName: "四川"
  62. # 校验列表
  63. check:
  64. check_type: no_check #不校验结果
  65. expected_code:
  66. expected_result:
  67. global_relevance:
  68. # 关联键
  69. relevance:
  • 当该接口的参数数据较多时,为维护方便,可将其保存在一个单独的json文件中,比如上面用例中的data_parkinside.json,就是保存该接口参数数据的一个文件,与测试用例文件在同一个目录下。测试执行时,通过解析该json文件中的test_name字段,获取属于自身用例的参数,参数文件的内容格式如下:

  1. [
  2. {
  3. "test_name": "parkinside_1",
  4. "parameter": {
  5. "token": "asdgfhh32456asfgrsfss",
  6. "vpl": "AJ3585"
  7. }
  8. },
  9. {
  10. "test_name": "parkinside_3",
  11. "parameter": {
  12. "vpl": "AJ3585"
  13. }
  14. }
  15. ]

该json文件保存了两条用例的参数,通过用例名parkinside_1获取到第一条用例的参数,通过用例名parkinside_3获取到第三条用例的参数(json参数文件中的用例名需与yaml用例文件中的用例名一致)。

  • 当该接口的期望结果较长时,为维护方便,可将其保存在一个单独的json文件中,比如上面用例中的result_parkinside.json,就是保存该接口期望结果的一个文件,与测试用例文件在同一目录下。测试执行时,通过解析该json文件中的test_name字段,获取属于自身用例的期望结果,期望结果文件的内容格式如下:

  1. [
  2. {
  3. "json":
  4. {
  5. "vplInfo":
  6. {
  7. "userID":22,
  8. "username":"wuya",
  9. "vpl":"京AJ3585"
  10. },
  11. "Parking_time_long":"20小时18分钟",
  12. "Parking fee":"20$"
  13. },
  14. "test_name": "parkinside_1"
  15. }
  16. ]

该json文件保存了一条用例的期望结果,通过用例parkinside_1获取到第一条用例的期望结果(json文件中的用例名需与yaml用例文件中的用例名一致)。

  • 若该接口的测试用例需要引用函数或者变量,则可先在一个单独的relevance.ini关联配置文件中,定义好相关的变量和函数名,并进行拼接,后续可通过变量名,引入测试用例中,比如上面用例中的 ${sign}$ ,就是引用了关联配置文件中的 sign 变量值,relevance.ini关联配置文件的内容格式如下:

  1. [relevance]
  2. nonce=$RandomString(5)$
  3. timestamp = $GetTime(time_type=now,layout=13timestamp,unit=0,0,0,0,0)$
  4. sign = $SHA1(asdh, ${nonce}$, ${timestamp}$)$

上面配置中的nonce变量,引用了随机函数RandomString,该随机函数产生长度为5的随机数,这些函数的定义都已封装在functions模块中,在这里只需要通过对应的函数名,并存入参数即可引用相关函数。变量timestamp引用了时间戳函数,在这里将生成一个13位的时间戳,并传给变量timestamp。变量sign则是引用了加密函数SHA1,这里将会把字符串asdh、变量nonce的值和变量timestamp的值先拼接起来,然后再将拼接好的字符串传给加密函数SHA1加密。然后即可在用例中引用变量sign,如下:


  1. # 请求参数
  2. parameter:
  3. sign: ${sign}$ # 通过变量引用关联值
  4. vpl: AJ3585

4、单接口用例执行脚本

单接口测试用例执行脚本,由程序根据yaml格式的测试用例文件自动生成,并根据相应yaml格式的测试用例文件所在的路径生成当前用例执行脚本的保存路径,且该用例执行脚本平时不需要人工维护,如下是接口parkinside的执行脚本test_parkinside.py的格式:


  1. # -*- coding: utf-8 -*-
  2. import allure
  3. import pytest
  4. import time
  5. from Main import root_path, case_level, product_version, run_interval
  6. from common.unit.initializeYamlFile import ini_yaml
  7. from common.unit.initializePremise import ini_request
  8. from common.unit.apiSendCheck import api_send_check
  9. from common.unit.initializeRelevance import ini_relevance
  10. from common.unit import setupTest
  11. case_path = root_path + "/tests/TestCases/parkinsideApi"
  12. relevance_path = root_path + "/common/configModel/relevance"
  13. case_dict = ini_yaml(case_path, "parkinside")
  14. @allure.feature(case_dict["test_info"]["title"])
  15. class TestParkinside:
  16. @pytest.fixture(scope="class")
  17. def setupClass(self):
  18. """
  19. :rel: 获取关联文件得到的字典
  20. :return:
  21. """
  22. self.rel = ini_relevance(case_path, 'relevance') #获取本用例初始公共关联值
  23. self.relevance = ini_request(case_dict, case_path, self.rel) #执行完前置条件后,得到的本用例最新全部关联值
  24. return self.relevance, self.rel
  25. @pytest.mark.skipif(case_dict["test_info"]["product_version"] in product_version,
  26. reason="该用例所属版本为:{0},在本次排除版本{1}内".format(case_dict["test_info"]["product_version"], product_version))
  27. @pytest.mark.skipif(case_dict["test_info"]["case_level"] not in case_level,
  28. reason="该用例的用例等级为:{0},不在本次运行级别{1}内".format(case_dict["test_info"]["case_level"], case_level))
  29. @pytest.mark.run(order=case_dict["test_info"]["run_order"])
  30. @pytest.mark.parametrize("case_data", case_dict["test_case"], ids=[])
  31. @allure.severity(case_dict["test_info"]["case_level"])
  32. @pytest.mark.parkinside
  33. @allure.story("parkinside")
  34. @allure.issue("http://www.bugjira.com") # bug地址
  35. @allure.testcase("http://www.testlink.com") # 用例连接地址
  36. def test_parkinside(self, case_data, setupClass):
  37. """
  38. 测试接口为:parkinside
  39. :param case_data: 测试用例
  40. :return:
  41. """
  42. self.relevance = setupTest.setupTest(relevance_path, case_data, setupClass)
  43. # 发送测试请求
  44. api_send_check(case_data, case_dict, case_path, self.relevance)
  45. time.sleep(run_interval)
  46. if __name__ == '__main__':
  47. import subprocess
  48. subprocess.call(['pytest', '-v'])

5、封装请求协议apiMethod.py


  1. def post(header, address, request_parameter_type, timeout=8, data=None, files=None, cookie=None):
  2. """
  3. post请求
  4. :param header: 请求头
  5. :param address: 请求地址
  6. :param request_parameter_type: 请求参数格式(form_data,raw)
  7. :param timeout: 超时时间
  8. :param data: 请求参数
  9. :param files: 文件路径
  10. :return:
  11. """
  12. if 'form_data' in request_parameter_type:
  13. for i in files:
  14. value = files[i]
  15. if '/' in value:
  16. file_parm = i
  17. files[file_parm] = (os.path.basename(value), open(value, 'rb'))
  18. enc = MultipartEncoder(
  19. fields=files,
  20. boundary='--------------' + str(random.randint(1e28, 1e29 - 1))
  21. )
  22. header['Content-Type'] = enc.content_type
  23. response = requests.post(url=address, data=enc, headers=header, timeout=timeout, cookies=cookie)
  24. elif 'data' in request_parameter_type:
  25. response = requests.post(url=address, data=data, headers=header, timeout=timeout, files=files, cookies=cookie)
  26. elif 'json' in request_parameter_type:
  27. response = requests.post(url=address, json=data, headers=header, timeout=timeout, files=files, cookies=cookie)
  28. try:
  29. if response.status_code != 200:
  30. return response.status_code, response.text
  31. else:
  32. return response.status_code, response.json()
  33. except json.decoder.JSONDecodeError:
  34. return response.status_code, ''
  35. except simplejson.errors.JSONDecodeError:
  36. return response.status_code, ''
  37. except Exception as e:
  38. logging.exception('ERROR')
  39. logging.error(e)
  40. raise
  41. def get(header, address, data, timeout=8, cookie=None):
  42. """
  43. get请求
  44. :param header: 请求头
  45. :param address: 请求地址
  46. :param data: 请求参数
  47. :param timeout: 超时时间
  48. :return:
  49. """
  50. response = requests.get(url=address, params=data, headers=header, timeout=timeout, cookies=cookie)
  51. if response.status_code == 301:
  52. response = requests.get(url=response.headers["location"])
  53. try:
  54. return response.status_code, response.json()
  55. except json.decoder.JSONDecodeError:
  56. return response.status_code, ''
  57. except simplejson.errors.JSONDecodeError:
  58. return response.status_code, ''
  59. except Exception as e:
  60. logging.exception('ERROR')
  61. logging.error(e)
  62. raise
  63. def put(header, address, request_parameter_type, timeout=8, data=None, files=None, cookie=None):
  64. """
  65. put请求
  66. :param header: 请求头
  67. :param address: 请求地址
  68. :param request_parameter_type: 请求参数格式(form_data,raw)
  69. :param timeout: 超时时间
  70. :param data: 请求参数
  71. :param files: 文件路径
  72. :return:
  73. """
  74. if request_parameter_type == 'raw':
  75. data = json.dumps(data)
  76. response = requests.put(url=address, data=data, headers=header, timeout=timeout, files=files, cookies=cookie)
  77. try:
  78. return response.status_code, response.json()
  79. except json.decoder.JSONDecodeError:
  80. return response.status_code, ''
  81. except simplejson.errors.JSONDecodeError:
  82. return response.status_code, ''
  83. except Exception as e:
  84. logging.exception('ERROR')
  85. logging.error(e)
  86. raise
  87. def delete(header, address, data, timeout=8, cookie=None):
  88. """
  89. delete请求
  90. :param header: 请求头
  91. :param address: 请求地址
  92. :param data: 请求参数
  93. :param timeout: 超时时间
  94. :return:
  95. """
  96. response = requests.delete(url=address, params=data, headers=header, timeout=timeout, cookies=cookie)
  97. try:
  98. return response.status_code, response.json()
  99. except json.decoder.JSONDecodeError:
  100. return response.status_code, ''
  101. except simplejson.errors.JSONDecodeError:
  102. return response.status_code, ''
  103. except Exception as e:
  104. logging.exception('ERROR')
  105. logging.error(e)
  106. raise
  107. def save_cookie(header, address, request_parameter_type, timeout=8, data=None, files=None, cookie=None):
  108. """
  109. 保存cookie信息
  110. :param header: 请求头
  111. :param address: 请求地址
  112. :param timeout: 超时时间
  113. :param data: 请求参数
  114. :param files: 文件路径
  115. :return:
  116. """
  117. cookie_path = root_path + '/common/configModel/relevance/cookie.ini'
  118. if 'data' in request_parameter_type:
  119. response = requests.post(url=address, data=data, headers=header, timeout=timeout, files=files, cookies=cookie)
  120. elif 'json' in request_parameter_type:
  121. response = requests.post(url=address, json=data, headers=header, timeout=timeout, files=files, cookies=cookie)
  122. try:
  123. if response.status_code != 200:
  124. return response.status_code, response.text
  125. else:
  126. re_cookie = response.cookies.get_dict()
  127. cf = Config(cookie_path)
  128. cf.add_section_option('relevance', re_cookie)
  129. for i in re_cookie:
  130. values = re_cookie[i]
  131. logging.debug("cookies已保存,结果为:{}".format(i+"="+values))
  132. return response.status_code, response.json()
  133. except json.decoder.JSONDecodeError:
  134. return response.status_code, ''
  135. except simplejson.errors.JSONDecodeError:
  136. return response.status_code, ''
  137. except Exception as e:
  138. logging.exception('ERROR')
  139. logging.error(e)
  140. raise
  141. ……………………

6、封装方法apiSend.py:处理测试用例,拼接请求并发送


  1. def send_request(data, project_dict, _path, relevance=None):
  2. """
  3. 封装请求
  4. :param data: 测试用例
  5. :param project_dict: 用例文件内容字典
  6. :param relevance: 关联对象
  7. :param _path: case路径
  8. :return:
  9. """
  10. logging.info("="*100)
  11. try:
  12. # 获取用例基本信息
  13. get_header =project_dict["test_info"].get("headers")
  14. get_host = project_dict["test_info"].get("host")
  15. get_address = project_dict["test_info"].get("address")
  16. get_http_type = project_dict["test_info"].get("http_type")
  17. get_request_type = project_dict["test_info"].get("request_type")
  18. get_parameter_type = project_dict["test_info"].get("parameter_type")
  19. get_cookies = project_dict["test_info"].get("cookies")
  20. get_file = project_dict["test_info"].get("file")
  21. get_timeout = project_dict["test_info"].get("timeout")
  22. except Exception as e:
  23. logging.exception('获取用例基本信息失败,{}'.format(e))
  24. try:
  25. # 如果用例中写了headers关键字,则用用例中的headers值(若该关键字没有值,则会将其值置为none),否则用全局headers
  26. get_header = data["headers"]
  27. except KeyError:
  28. pass
  29. try:
  30. # 替换成用例中相应关键字的值,如果用例中写了host和address,则使用用例中的host和address,若没有则使用全局传入的默认值
  31. get_host = data["host"]
  32. except KeyError:
  33. pass
  34. try:
  35. get_address = data["address"]
  36. except KeyError:
  37. pass
  38. try:
  39. get_http_type = data["http_type"]
  40. except KeyError:
  41. pass
  42. try:
  43. get_request_type = data["request_type"]
  44. except KeyError:
  45. pass
  46. try:
  47. get_parameter_type = data["parameter_type"]
  48. except KeyError:
  49. pass
  50. try:
  51. get_cookies = data["cookies"]
  52. except KeyError:
  53. pass
  54. try:
  55. get_file = data["file"]
  56. except KeyError:
  57. pass
  58. try:
  59. get_timeout = data["timeout"]
  60. except KeyError:
  61. pass
  62. Cookie = None
  63. header = get_header
  64. if get_header:
  65. if isinstance(get_header, str):
  66. header = confManage.conf_manage(get_header, "header") # 处理请求头中的变量
  67. if header == get_header:
  68. pass
  69. else:
  70. var_list = re.findall('\$.*?\$', header)
  71. header = literal_eval(header) # 将字典类型的字符串,转成字典
  72. # 处理请求头中的变量和函数
  73. if var_list:
  74. # 将关联对象里的键值对遍历出来,并替换掉字典值中的函数
  75. rel = dict()
  76. for key, value in header.items():
  77. rel[key] = replace_random(value)
  78. header = rel
  79. logging.debug("替换请求头中的函数处理结果为:{}".format(header))
  80. str_header = str(header)
  81. var_list = re.findall('\${.*?}\$', str_header)
  82. if var_list:
  83. # 用自身关联对象里的变量值,替换掉自身关联对象里的变量
  84. header = replaceRelevance.replace(header, header)
  85. str_header = str(header)
  86. var_list = re.findall('\$.*?\$', str_header)
  87. if var_list:
  88. # 再次将关联对象里的键值对遍历出来,并替换掉字典值中的函数
  89. rel = dict()
  90. for key, value in header.items():
  91. rel[key] = replace_random(value)
  92. header = rel
  93. else:
  94. pass
  95. else:
  96. pass
  97. else:
  98. pass
  99. else:
  100. pass
  101. else:
  102. pass
  103. logging.debug("请求头处理结果为:{}".format(header))
  104. if get_cookies is True:
  105. cookie_path = root_path + "/common/configModel/relevance"
  106. Cookie = ini_relevance(cookie_path, 'cookie') # 为字典类型的字符串
  107. logging.debug("cookie处理结果为:{}".format(Cookie))
  108. else:
  109. pass
  110. parameter = readParameter.read_param(data["test_name"], data["parameter"], _path, relevance) #处理请求参数(含参数为文件的情况)
  111. logging.debug("请求参数处理结果:{}".format(parameter))
  112. get_address = str(replaceRelevance.replace(get_address, relevance)) # 处理请求地址中的变量
  113. logging.debug("请求地址处理结果:{}".format(get_address))
  114. get_host = str(confManage.conf_manage(get_host, "host")) # host处理,读取配置文件中的host
  115. logging.debug("host处理结果:{}".format(get_host))
  116. if not get_host:
  117. raise Exception("接口请求地址为空 {}".format(get_host))
  118. logging.info("请求接口:{}".format(data["test_name"]))
  119. logging.info("请求地址:{}".format((get_http_type + "://" + get_host + get_address)))
  120. logging.info("请求头: {}".format(header))
  121. logging.info("请求参数: {}".format(parameter))
  122. # 通过get_request_type来判断,如果get_request_type为post_cookie;如果get_request_type为get_cookie
  123. if get_request_type.lower() == 'post_cookie':
  124. with allure.step("保存cookie信息"):
  125. allure.attach("请求接口:", data["test_name"])
  126. allure.attach("用例描述:", data["info"])
  127. allure.attach("请求地址", get_http_type + "://" + get_host + get_address)
  128. allure.attach("请求头", str(header))
  129. allure.attach("请求参数", str(parameter))
  130. result = apiMethod.save_cookie(header=header, address=get_http_type + "://" + get_host + get_address,
  131. request_parameter_type=get_parameter_type,
  132. data=parameter,
  133. cookie=Cookie,
  134. timeout=get_timeout)
  135. elif get_request_type.lower() == 'post':
  136. logging.info("请求方法: POST")
  137. if get_file:
  138. with allure.step("POST上传文件"):
  139. allure.attach("请求接口:",data["test_name"])
  140. allure.attach("用例描述:", data["info"])
  141. allure.attach("请求地址", get_http_type + "://" + get_host + get_address)
  142. allure.attach("请求头", str(header))
  143. allure.attach("请求参数", str(parameter))
  144. result = apiMethod.post(header=header,
  145. address=get_http_type + "://" + get_host + get_address,
  146. request_parameter_type=get_parameter_type,
  147. files=parameter,
  148. cookie=Cookie,
  149. timeout=get_timeout)
  150. else:
  151. with allure.step("POST请求接口"):
  152. allure.attach("请求接口:", data["test_name"])
  153. allure.attach("用例描述:", data["info"])
  154. allure.attach("请求地址", get_http_type + "://" + get_host + get_address)
  155. allure.attach("请求头", str(header))
  156. allure.attach("请求参数", str(parameter))
  157. result = apiMethod.post(header=header,
  158. address=get_http_type + "://" + get_host + get_address,
  159. request_parameter_type=get_parameter_type,
  160. data=parameter,
  161. cookie=Cookie,
  162. timeout=get_timeout)
  163. elif get_request_type.lower() == 'get':
  164. with allure.step("GET请求接口"):
  165. allure.attach("请求接口:", data["test_name"])
  166. allure.attach("用例描述:", data["info"])
  167. allure.attach("请求地址", get_http_type + "://" + get_host + get_address)
  168. allure.attach("请求头", str(header))
  169. allure.attach("请求参数", str(parameter))
  170. logging.info("请求方法: GET")
  171. result = apiMethod.get(header=header,
  172. address=get_http_type + "://" + get_host + get_address,
  173. data=parameter,
  174. cookie=Cookie,
  175. timeout=get_timeout)
  176. elif get_request_type.lower() == 'put':
  177. logging.info("请求方法: PUT")
  178. if get_file:
  179. with allure.step("PUT上传文件"):
  180. allure.attach("请求接口:", data["test_name"])
  181. allure.attach("用例描述:", data["info"])
  182. allure.attach("请求地址", get_http_type + "://" + get_host + get_address)
  183. allure.attach("请求头", str(header))
  184. allure.attach("请求参数", str(parameter))
  185. result = apiMethod.put(header=header,
  186. address=get_http_type + "://" + get_host + get_address,
  187. request_parameter_type=get_parameter_type,
  188. files=parameter,
  189. cookie=Cookie,
  190. timeout=get_timeout)
  191. else:
  192. with allure.step("PUT请求接口"):
  193. allure.attach("请求接口:", data["test_name"])
  194. allure.attach("用例描述:", data["info"])
  195. allure.attach("请求地址", get_http_type + "://" + get_host + get_address)
  196. allure.attach("请求头", str(header))
  197. allure.attach("请求参数", str(parameter))
  198. result = apiMethod.put(header=header,
  199. address=get_http_type + "://" + get_host + get_address,
  200. request_parameter_type=get_parameter_type,
  201. data=parameter,
  202. cookie=Cookie,
  203. timeout=get_timeout)
  204. elif get_request_type.lower() == 'delete':
  205. with allure.step("DELETE请求接口"):
  206. allure.attach("请求接口:", data["test_name"])
  207. allure.attach("用例描述:", data["info"])
  208. allure.attach("请求地址", get_http_type + "://" + get_host + get_address)
  209. allure.attach("请求头", str(header))
  210. allure.attach("请求参数", str(parameter))
  211. logging.info("请求方法: DELETE")
  212. result = apiMethod.delete(header=header,
  213. address=get_http_type + "://" + get_host + get_address,
  214. data=parameter,
  215. cookie=Cookie,
  216. timeout=get_timeout)
  217. …………………………
  218. else:
  219. result = {"code": False, "data": False}
  220. logging.info("没有找到对应的请求方法!")
  221. logging.info("请求接口结果:\n {}".format(result))
  222. return result

7、测试结果断言封装checkResult.py


  1. def check_json(src_data, dst_data):
  2. """
  3. 校验的json
  4. :param src_data: 检验内容
  5. :param dst_data: 接口返回的数据
  6. :return:
  7. """
  8. if isinstance(src_data, dict):
  9. for key in src_data:
  10. if key not in dst_data:
  11. raise Exception("JSON格式校验,关键字%s不在返回结果%s中" % (key, dst_data))
  12. else:
  13. this_key = key
  14. if isinstance(src_data[this_key], dict) and isinstance(dst_data[this_key], dict):
  15. check_json(src_data[this_key], dst_data[this_key])
  16. elif isinstance(type(src_data[this_key]), type(dst_data[this_key])):
  17. raise Exception("JSON格式校验,关键字 %s 与 %s 类型不符" % (src_data[this_key], dst_data[this_key]))
  18. else:
  19. pass
  20. else:
  21. raise Exception("JSON校验内容非dict格式")
  22. def check_result(test_name, case, code, data, _path, relevance=None):
  23. """
  24. 校验测试结果
  25. :param test_name: 测试名称
  26. :param case: 测试用例
  27. :param code: HTTP状态
  28. :param data: 返回的接口json数据
  29. :param relevance: 关联值对象
  30. :param _path: case路径
  31. :return:
  32. """
  33. # 不校验结果
  34. if case["check_type"] == 'no_check':
  35. with allure.step("不校验结果"):
  36. pass
  37. # json格式校验
  38. elif case["check_type"] == 'json':
  39. expected_result = case["expected_result"]
  40. if isinstance(case["expected_result"], str):
  41. expected_result = readExpectedResult.read_json(test_name, expected_result, _path, relevance)
  42. with allure.step("JSON格式校验"):
  43. allure.attach("期望code", str(case["expected_code"]))
  44. allure.attach('期望data', str(expected_result))
  45. allure.attach("实际code", str(code))
  46. allure.attach('实际data', str(data))
  47. if int(code) == case["expected_code"]:
  48. if not data:
  49. data = "{}"
  50. check_json(expected_result, data)
  51. else:
  52. raise Exception("http状态码错误!\n {0} != {1}".format(code, case["expected_code"]))
  53. # 只校验状态码
  54. elif case["check_type"] == 'only_check_status':
  55. with allure.step("校验HTTP状态"):
  56. allure.attach("期望code", str(case["expected_code"]))
  57. allure.attach("实际code", str(code))
  58. allure.attach('实际data', str(data))
  59. if int(code) == case["expected_code"]:
  60. pass
  61. else:
  62. raise Exception("http状态码错误!\n {0} != {1}".format(code, case["expected_code"]))
  63. # 完全校验
  64. elif case["check_type"] == 'entirely_check':
  65. expected_result = case["expected_result"]
  66. if isinstance(case["expected_result"], str):
  67. expected_result = readExpectedResult.read_json(test_name, expected_result, _path, relevance)
  68. with allure.step("完全校验结果"):
  69. allure.attach("期望code", str(case["expected_code"]))
  70. allure.attach('期望data', str(expected_result))
  71. allure.attach("实际code", str(code))
  72. allure.attach('实际data', str(data))
  73. if int(code) == case["expected_code"]:
  74. result = operator.eq(expected_result, data)
  75. if result:
  76. pass
  77. else:
  78. raise Exception("完全校验失败! {0} ! = {1}".format(expected_result, data))
  79. else:
  80. raise Exception("http状态码错误!\n {0} != {1}".format(code, case["expected_code"]))
  81. # 正则校验
  82. elif case["check_type"] == 'Regular_check':
  83. if int(code) == case["expected_code"]:
  84. try:
  85. result = ""
  86. if isinstance(case["expected_result"], list):
  87. with allure.step("正则校验"):
  88. for i in case["expected_result"]:
  89. result = re.findall(i.replace("\"","\'"), str(data))
  90. allure.attach('正则校验结果\n',str(result))
  91. allure.attach('实际data', str(data))
  92. else:
  93. result = re.findall(case["expected_result"].replace("\"", "\'"), str(data))
  94. with allure.step("正则校验"):
  95. allure.attach("期望code", str(case["expected_code"]))
  96. allure.attach('正则表达式', str(case["expected_result"]).replace("\'", "\""))
  97. allure.attach("实际code", str(code))
  98. allure.attach('实际data', str(data))
  99. allure.attach(case["expected_result"].replace("\"", "\'") + '校验完成结果',
  100. str(result).replace("\'", "\""))
  101. if not result:
  102. raise Exception("正则未校验到内容! {}".format(case["expected_result"]))
  103. except KeyError:
  104. raise Exception("正则校验执行失败! {}\n正则表达式为空时".format(case["expected_result"]))
  105. else:
  106. raise Exception("http状态码错误!\n {0} != {1}".format(code, case["expected_code"]))
  107. # 数据库校验
  108. elif case["check_type"] == "datebase_check":
  109. pass
  110. else:
  111. raise Exception("无该校验方式:{}".format(case["check_type"]))

8、共享模块conftest.py(初始化测试环境,制造测试数据,并还原测试环境)


  1. import allure
  2. import pytest
  3. from common.configModel import confRead
  4. from Main import root_path
  5. from common.unit.initializeYamlFile import ini_yaml
  6. from common.unit.initializeRelevance import ini_relevance
  7. from common.unit.apiSendCheck import api_send_check
  8. from common.configModel.confRead import Config
  9. import logging
  10. import os
  11. conf_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config", "apiConfig.ini")
  12. case_path = root_path + "/tests/CommonApi/loginApi"
  13. relevance_path = root_path + "/common/configModel/relevance"
  14. @pytest.fixture(scope="session", autouse=True)
  15. def setup_env():
  16. # 定义环境;定义报告中environment
  17. Host = confRead.Config(conf_path).read_apiConfig("host")
  18. allure.environment(测试环境="online", hostName=Host["host"], 执行人="XX", 测试项目="线上接口测试")
  19. case_dict = ini_yaml(case_path, "login")
  20. # 参数化 fixture
  21. @pytest.fixture(scope="session", autouse=True, params=case_dict["test_case"])
  22. def login(request):
  23. # setup
  24. """
  25. :param request: 上下文
  26. :param request.param: 测试用例
  27. :return:
  28. """
  29. # 清空关联配置
  30. for i in ["GlobalRelevance.ini", "ModuleRelevance.ini"]:
  31. relevance_file = os.path.join(relevance_path, i)
  32. cf = Config(relevance_file)
  33. cf.add_conf("relevance")
  34. logging.info("执行全局用例依赖接口,初始化数据!")
  35. relevance = ini_relevance(relevance_path, "ModuleRelevance")
  36. if request.param["case_id"] == 1:
  37. relevance = ini_relevance(case_path, "relevance")
  38. logging.info("本用例最终的关联数据为:{}".format(relevance))
  39. # 发送测试请求
  40. api_send_check(request.param, case_dict, case_path, relevance)
  41. logging.info("初始化数据完成!")
  42. yield
  43. # teardown
  44. # 还原测试环境部分代码
  45. ……
  46. ……
  47. logging.info("本轮测试已结束,正在还原测试环境!")

9、测试执行总入口Main.py(收集测试用例,批量执行并生成测试报告)


  1. import os
  2. import shutil
  3. import subprocess
  4. import pytest
  5. import logging
  6. from common.unit.initializeYamlFile import ini_yaml
  7. from common.utils.logs import LogConfig
  8. from common.script.writeCase import write_case
  9. from common.script.writeCaseScript import write_caseScript
  10. from common.utils.formatChange import formatChange
  11. from common.utils.emailModel.runSendEmail import sendEailMock
  12. root_path = os.path.split(os.path.realpath(__file__))[0]
  13. xml_report_path = root_path + "\\report\\xml"
  14. detail_report_path = root_path + "\\report\\detail_report"
  15. summary_report_path = root_path + "\\report\\summary_report\\summary_report.html"
  16. runConf_path = os.path.join(root_path, "config")
  17. # 获取运行配置信息
  18. runConfig_dict = ini_yaml(runConf_path, "runConfig")
  19. case_level = runConfig_dict["case_level"]
  20. if not case_level:
  21. case_level = ["blocker", "critical", "normal", "minor", "trivial"]
  22. else:
  23. pass
  24. product_version = runConfig_dict["product_version"]
  25. if not product_version:
  26. product_version = []
  27. else:
  28. pass
  29. isRun_switch = runConfig_dict["isRun_switch"]
  30. run_interval = runConfig_dict["run_interval"]
  31. writeCase_switch = runConfig_dict["writeCase_switch"]
  32. ProjectAndFunction_path = runConfig_dict["ProjectAndFunction_path"]
  33. if not ProjectAndFunction_path:
  34. ProjectAndFunction_path = ""
  35. else:
  36. pass
  37. scan_path = runConfig_dict["scan_path"]
  38. if not scan_path:
  39. scan_path = ""
  40. else:
  41. pass
  42. runTest_switch = runConfig_dict["runTest_switch"]
  43. reruns = str(runConfig_dict["reruns"])
  44. reruns_delay = str(runConfig_dict["reruns_delay"])
  45. log = runConfig_dict["log"]
  46. def batch(CMD):
  47. output, errors = subprocess.Popen(CMD, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
  48. outs = output.decode("utf-8")
  49. return outs
  50. if __name__ == "__main__":
  51. try:
  52. LogConfig(root_path, log)
  53. if writeCase_switch == 1:
  54. # 根据har_path里的文件,自动生成用例文件yml和用例执行文件py,若已存在相关文件,则不再创建
  55. write_case(root_path, ProjectAndFunction_path)
  56. elif writeCase_switch == 2:
  57. write_caseScript(root_path, scan_path)
  58. else:
  59. logging.info("="*20+"本次测试自动生成测试用例功能已关闭!"+"="*20+"\n")
  60. if runTest_switch == 1:
  61. # 清空目录和文件
  62. email_target_dir = root_path + "/report/zip_report" # 压缩文件保存路径
  63. shutil.rmtree(email_target_dir)
  64. if os.path.exists(summary_report_path):
  65. os.remove(summary_report_path)
  66. else:
  67. pass
  68. os.mkdir(email_target_dir)
  69. args = ["-k", runConfig_dict["Project"], "-m", runConfig_dict["markers"], "--maxfail=%s" % runConfig_dict["maxfail"],
  70. "--durations=%s" % runConfig_dict["slowestNum"], "--reruns", reruns, "--reruns-delay", reruns_delay,
  71. "--alluredir", xml_report_path, "--html=%s" % summary_report_path]
  72. test_result = pytest.main(args) # 全部通过,返回0;有失败或者错误,则返回1
  73. cmd = "allure generate %s -o %s --clean" % (xml_report_path, detail_report_path)
  74. reportResult = batch(cmd)
  75. logging.debug("生成html的报告结果为:{}".format(reportResult))
  76. # 发送report到邮件
  77. emailFunction = runConfig_dict["emailSwitch"]
  78. if emailFunction == 1:
  79. if test_result == 0:
  80. ReportResult = "测试通过!"
  81. else:
  82. ReportResult = "测试不通过!"
  83. # 将字符中的反斜杠转成正斜杠
  84. fileUrl_PATH = root_path.replace("\\", "/")
  85. logging.debug("基础路径的反斜杠转成正斜杠为:{}".format(fileUrl_PATH))
  86. fileUrl = "file:///{}/report/summary_report/summary_report.html".format(fileUrl_PATH)
  87. logging.info("html测试报告的url为:{}".format(fileUrl))
  88. save_fn = r"{}\report\zip_report\summary_report.png".format(root_path)
  89. logging.debug("转成图片报告后保存的目标路径为:{}".format(save_fn))
  90. formatChange_obj = formatChange()
  91. formatChange_obj.html_to_image(fileUrl, save_fn)
  92. email_folder_dir = root_path + "/report/detail_report" # 待压缩文件夹
  93. logging.debug("待压缩文件夹为:{}".format(email_folder_dir))
  94. sendEailMock_obj = sendEailMock()
  95. sendEailMock_obj.send_email(email_folder_dir, email_target_dir, runConfig_dict, ReportResult, save_fn)
  96. else:
  97. logging.info("="*20+"本次测试的邮件功能已关闭!"+"="*20+"\n")
  98. else:
  99. logging.info("="*20+"本次运行测试开关已关闭!"+"="*20+"\n")
  100. except Exception as err:
  101. logging.error("本次测试有异常为:{}".format(err))

10、结合Allure生成报告

  • 好的测试报告在整个测试框架是至关重要的部分,Allure是一个很好用的报告框架,不仅报告美观,而且方便CI集成。

  • Allure中对严重级别的定义:

    1. Blocker级别:中断缺陷(客户端程序无响应,无法执行下一步操作)
    2. Critical级别:临界缺陷(功能点缺失)
    3. Normal级别:普通缺陷(数值计算错误)
    4. Minor级别:次要缺陷(界面错误与UI需求不符)
    5. Trivial级别:轻微缺陷(必输项无提示,或者提示不规范)
  • Allure报告总览,如图所示:















  • 发送到邮件中的测试报告



  • 测试执行项目演示

pytest、Allure与Jenkins集成

1、集成环境部署

1、Linux安装docker容器

  • 安装docker容器脚本

    1. curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
  • 启动docker

    1. systemctl start docker
  • 通过修改daemon配置文件/etc/docker/daemon.json来使用阿里云镜像加速器

    1. sudo mkdir -p /etc/docker
    2. sudo tee /etc/docker/daemon.json <<-'EOF'
    3. {
    4. "registry-mirrors": ["https://XXXX.XXXX.aliyuncs.com"]
    5. }
    6. EOF
    7. sudo systemctl daemon-reload
    8. sudo systemctl restart docker
  • 查看阿里云加速器是否配置成功

    1. vi /etc/docker/daemon.json

2、安装Jenkins

  • 在 Docker 中安装并启动 Jenkins 的样例命令如下:

    1. docker run -d -u root \
    2. --name jenkins-blueocean \
    3. --restart=always \
    4. -p 8080:8080 \
    5. -p 50000:50000 \
    6. -p 50022:50022 \
    7. -v /home/jenkins/var:/var/jenkins_home \
    8. -v /var/run/docker.sock:/var/run/docker.sock \
    9. -v "$HOME":/home \
    10. jenkinsci/blueocean
    11. 其中的 50000 是映射到 TCP port for JNLP agents 对应的端口,50022 是映射到 SSHD Port。在成功启动 Jenkins 后,可在Jenkins启动页面 http://ip:8080/configureSecurity/ 上设置。
    12. 这两个端口其实不是必须的,只是为了方便通过 SSH 使用 Jenkins 才开启它们。
  • 在此页面打开 SSHD Port 后,运行以下命令即可验证对应的端口值。

    1. curl -Lv http://ip:8080/login 2>&1 | grep 'X-SSH-Endpoint'
  • 把Jenkins容器里的密码粘贴上

    1. /var/jenkins_home/secrets/initialAdminPassword
  • 访问 http://ip:8080 ,安装默认推荐插件

  • 先到admin配置界面,再次修改admin的用户密码

3、allure与jenkins集成

  • jenkins安装插件

    在管理Jenkins-插件管理-可选插件处,搜索allure ,然后安装,如下

    插件名称为Allure Jenkins Plugin,如下图所示:



  • jenkins安装allure_commandline(若之前已安装过allure插件,则跳过此步,按第三步进行)

    如果jenkins上有安装maven的话,则此工具安装就比较简单了,打开jenkins的Global Tool Configuration,找到Allure Commandline,选择安装,如下所示:

如果没有安装maven,则需要去jenkins服务器上安装此工具。

  • 点击管理Jenkins,打开jenkins的Global Tool Configuration,找到Allure Commandline

    配置已安装的jdk的JAVA_HOME,如图

  • 配置Allure Commandline,如图

  • 针对Linux上的远程从节点配置:

  1. 配置远程从节点

  2. 将agent.jar下载到该远程节点Linux的某个目录中,然后在agent.jar所在的目录下,执行所生成的节点命令,即可启动节点,将该节点连接到Jenkins。
  • 针对Windows的远程从节点配置:

    1. 配置远程从节点

    2. 在Windows上启动该节点



      将agent.jar下载到该远程节点windows的某个目录中,然后在agent.jar所在的目录下,执行里面的命令,比如java -jar agent.jar -jnlpUrl http://192.168.201.9:8080/computer/win10_jun/slave-agent.jnlp -secret 1db00accef84f75b239febacc436e834b2164615a459f3b7f00f77a14ed51539 -workDir "E:\jenkins_work"

      即可以将该节点连接到Jenkins,如下

    3. 新建job,配置如下,比如保留7天以内的build,并规定最多只保留10个build







      编写构建脚本



      在命令后,换行,写上 exit 0 (加上exit 0表示执行完成退出)

      添加allure report插件



      配置生成的xml路径和生成html报告的路径

  • 设置邮件通知

  1. 安装插件Email Extension



  2. 进入jenkins的系统管理-系统设置,进行相关配置





  3. 修改Default Content的内容,具体内容如下:

    1. $PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS:
    2. Check console output at ${BUILD_URL}allure/ to view the results.





  1. 再进入【系统管理-系统设置】拉到最下面,设置问题追踪,在Allure Report下选择增加:

    1. Key: allure.issues.tracker.pattern
    2. Value: http://tracker.company.com/%s
  • 对构建的job添加邮件发送
  1. job配置页面,添加构建后步骤“Editable Email Notification”,如图

  2. 以下可以使用默认配置:







  3. 在Default Content中定义邮件正文,模板如下


  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>${ENV, var="JOB_NAME"}-第${BUILD_NUMBER}次构建日志</title>
  6. </head>
  7. <body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4" offset="0">
  8. <table width="95%" cellpadding="0" cellspacing="0" style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">
  9. <tr>
  10. <td>(本邮件由程序自动下发,请勿回复!)</td>
  11. </tr>
  12. <tr>
  13. <td>
  14. <h2><font color="#FF0000">构建结果 - ${BUILD_STATUS}</font></h2>
  15. </td>
  16. </tr>
  17. <tr>
  18. <td><br />
  19. <b><font color="#0B610B">构建信息</font></b>
  20. <hr size="2" width="100%" align="center" />
  21. </td>
  22. </tr>
  23. <tr><a href="${PROJECT_URL}">${PROJECT_URL}</a>
  24. <td>
  25. <ul>
  26. <li>项目名称:${PROJECT_NAME}</li>
  27. <li>GIT路径:<a href="${GIT_URL}">${GIT_URL}</a></li>
  28. <li>构建编号:第${BUILD_NUMBER}次构建</li>
  29. <li>触发原因:${CAUSE}</li>
  30. <li>系统的测试报告 :<a href="${PROJECT_URL}${BUILD_NUMBER}/allure">${PROJECT_URL}${BUILD_NUMBER}/allure</a></li><br />
  31. <li>构建日志:<a href="${BUILD_URL}console">${BUILD_URL}console</a></li>
  32. </ul>
  33. </td>
  34. </tr>
  35. <tr>
  36. <td>
  37. <b><font color="#0B610B">变更信息:</font></b>
  38. <hr size="2" width="100%" align="center" />
  39. </td>
  40. </tr>
  41. <tr>
  42. <td>
  43. <ul>
  44. <li>上次构建成功后变化 : ${CHANGES_SINCE_LAST_SUCCESS}</a></li>
  45. </ul>
  46. </td>
  47. </tr>
  48. <tr>
  49. <td>
  50. <ul>
  51. <li>上次构建不稳定后变化 : ${CHANGES_SINCE_LAST_UNSTABLE}</a></li>
  52. </ul>
  53. </td>
  54. </tr>
  55. <tr>
  56. <td>
  57. <ul>
  58. <li>历史变更记录 : <a href="${PROJECT_URL}changes">${PROJECT_URL}changes</a></li>
  59. </ul>
  60. </td>
  61. </tr>
  62. <tr>
  63. <td>
  64. <ul>
  65. <li>变更集:${JELLY_SCRIPT,template="html"}</a></li>
  66. </ul>
  67. </td>
  68. </tr>
  69. <hr size="2" width="100%" align="center" />
  70. </table>
  71. </body>
  72. </html>
  • 在Jenkins上启动测试,如图

  • 启动测试产生的过程日志如下

  • 测试构建完成结果如下

  • 构建完成后,Jenkins自动发送的邮件报告如下

  • CI集成后,产生的Allure报告如下

  • Jenkins中启动测试项目演示

pytest+requests+Python3.7+yaml+Allure+Jenkins+docker实现接口自动化测试的更多相关文章

  1. 使用HttpRunner3+Allure+Jenkins实现Web接口自动化测试

    陆续给不同项目做了Web接口自动化测试,在尝试不同方法的同时会有新的体会.最近用到了HttpRunner3,本文将记录使用HttpRunner3+Allure+Jenkins在项目中快速实现Web接口 ...

  2. 接口自动化 [授客]基于python+Testlink+Jenkins实现的接口自动化测试框架V3.0

    基于python+Testlink+Jenkins实现的接口自动化测试框架V3.0   by:授客 QQ:1033553122     博客:http://blog.sina.com.cn/ishou ...

  3. 接口自动化 基于python+Testlink+Jenkins实现的接口自动化测试框架[V2.0改进版]

    基于python+Testlink+Jenkins实现的接口自动化测试框架[V2.0改进版]   by:授客 QQ:1033553122 由于篇幅问题,,暂且采用网盘分享的形式: 下载地址: [授客] ...

  4. 基于python+Testlink+Jenkins实现的接口自动化测试框架V3.0

    基于python+Testlink+Jenkins实现的接口自动化测试框架V3.0 目录 1. 开发环境2. 主要功能逻辑介绍3. 框架功能简介 4. 数据库的创建 5. 框架模块详细介绍6. Tes ...

  5. jenkins+ant+jmeter接口自动化测试(持续构建)

    使用badboy录制脚本,到处到jmeter后进行接口自动化,后来想着 可不可以用自动化来跑脚本呢,不用jmeter的图形界面呢, 选择了ant来进行构建,最后想到了用Jenkins来进行持续构建接口 ...

  6. 接口自动化 基于python+Testlink+Jenkins实现的接口自动化测试框架

    链接:http://blog.sina.com.cn/s/blog_13cc013b50102w94u.html

  7. 基于Python+Requests+Pytest+YAML+Allure实现接口自动化

    本项目实现接口自动化的技术选型:Python+Requests+Pytest+YAML+Allure ,主要是针对之前开发的一个接口项目来进行学习,通过 Python+Requests 来发送和处理H ...

  8. Python接口自动化测试框架: pytest+allure+jsonpath+requests+excel实现的接口自动化测试框架(学习成果)

    废话 最近在自己学习接口自动化测试,这里也算是完成一个小的成果,欢迎大家交流指出不合适的地方,源码在文末 问题 整体代码结构优化未实现,导致最终测试时间变长,其他工具单接口测试只需要39ms,该框架中 ...

  9. Pytest单元测试框架——Pytest+Allure+Jenkins的应用

    一.简介 pytest+allure+jenkins进行接口测试.生成测试报告.结合jenkins进行集成. pytest是python的一种单元测试框架,与python自带的unittest测试框架 ...

随机推荐

  1. 09-SpringMVC03

    今日知识 1. SpringMVC自定义异常处理 2. SpringMVC的interceptor(过滤器) SpringMVC自定义异常处理 1.web.xml正常写 <servlet> ...

  2. malloc返回地址的对齐问题

    http://man7.org/linux/man-pages/man3/malloc.3.html RETURN VALUE         top The malloc() and calloc( ...

  3. lwip netbuf

    lwip2.0.2 netbuf_new——分配netbuf结构体的内存. netbuf_alloc,分配netbuf中pbuf内存(pbuf_alloc中PBUF_RAM类型,包括pbuf结构体和p ...

  4. 代码质量:SonarQube

    SonarQube SonarQube的安装 jenkins(十四):Jenkins和sonarqube集成 https://www.cnblogs.com/sunyllove/p/9895373.h ...

  5. 安利自己写的easy-clipboard库

    概述 clipboard.js 是一个非常好用的剪切板插件,但是随着前端框架的演变,用户与网页交互的方式越来越多,不仅限于点击事件了,并且在很多情况下,我们可能不需要它强制性自带的点击事件,所以我打算 ...

  6. Re:萌娘百科上的黑幕实现

    Re:萌娘百科上的黑幕 说明 本文所有的代码均来自萌娘百科.萌娘百科打钱! 第零段话(我想说的) 这方面不是我的专长,所以有的地方说的不对也请纠正! 我可不是萌娘百科的员工或者管理员或者收了钱 我只是 ...

  7. Play! 1.x Eclipse Debug调试报错解决方法记录

    使用Play eclipsify xxxx[项目路径],可以把play new xxxx[项目路径]创建的工程生成为Eclipse的项目 但是在Debug AS 调试的时候,会报以下错误 Error ...

  8. 记录 Spine骨骼动画导入unity 步骤[unity3d 4.6.6版本 2d动画]

    1:准备好unity使用Spine所需要的运行库,可到如下地址 https://github.com/EsotericSoftware/spine-runtimes/tree/master/spine ...

  9. 区块链 POS和POW的区别

    如果你是一名资深的比特币矿工或商人,你一定听说过POW和POS,否则,很难理解. 读完本文,相信会让你明白,原来,虚拟货币除了挖矿,还有利息! 第一段:通俗的概念解析 POW:全称Proof of W ...

  10. javaSE学习笔记(17)---锁

    javaSE学习笔记(17)---锁 Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率.本文旨在对锁相关源码(本文中的源码来自JDK 8).使用场景进行举例,为读 ...