Python接口测试实战4(下) - 框架完善:用例基类,用例标签,重新运行上次失败用例

时间:2023-03-08 23:57:56
Python接口测试实战4(下) - 框架完善:用例基类,用例标签,重新运行上次失败用例

如有任何学习问题,可以添加作者微信:lockingfree

课程目录

Python接口测试实战1(上)- 接口测试理论

Python接口测试实战1(下)- 接口测试工具的使用

Python接口测试实战2 - 使用Python发送请求

Python接口测试实战3(上)- Python操作数据库

Python接口测试实战3(下)- unittest测试框架

Python接口测试实战4(上) - 接口测试框架实战

Python接口测试实战4(下) - 框架完善:用例基类,用例标签,重新运行上次失败用例

Python接口测试实战5(上) - Git及Jenkins持续集成

Python接口测试实战5(下) - RESTful、Web Service及Mock Server

更多学习资料请加QQ群: 822601020获取

PDF下载:链接:https://pan.baidu.com/s/1OwAa8nl1eeBj8fcrgd3sBA 密码:e9d8

本节内容

  • 使用用例基类
  • 自定义TestSuite
  • collect-only的实现
  • testlist的实现
  • 用例tags的实现
  • rerun-fails的实现
  • 命令行参数的使用

更简单的用例编写

使用用例基类

因为每条用例都需要从excel中读取数据,解析数据,发送请求,断言响应结果,我们可以封装一个BaseCase的用例基础类,对一些方法进行封装,来简化用例编写

重新规划了test目录,在test下建立case文件夹存放用例,建立suite文件夹存放自定义的TestSuite

test_user_data.xlsx中增加了一列data_typeFORM指表单格式请求,JSON指JSON格式请求

项目test/case文件夹下新建basecase.py

import unittest
import requests
import json
import sys
sys.path.append("../..") # 统一将包的搜索路径提升到项目根目录下 from lib.read_excel import *
from lib.case_log import log_case_info class BaseCase(unittest.TestCase): # 继承unittest.TestCase
@classmethod
def setUpClass(cls):
if cls.__name__ != 'BaseCase':
cls.data_list = excel_to_list(data_file, cls.__name__) def get_case_data(self, case_name):
return get_test_data(self.data_list, case_name) def send_request(self, case_data):
case_name = case_data.get('case_name')
url = case_data.get('url')
args = case_data.get('args')
headers = case_data.get('headers')
expect_res = case_data.get('expect_res')
method = case_data.get('method')
data_type = case_data.get('data_type') if method.upper() == 'GET': # GET类型请求
res = requests.get(url=url, params=json.loads(args)) elif data_type.upper() == 'FORM': # 表单格式请求
res = requests.post(url=url, data=json.loads(args), headers=json.loads(headers))
log_case_info(case_name, url, args, expect_res, res.text)
self.assertEqual(res.text, expect_res)
else:
res = requests.post(url=url, json=json.loads(args), headers=json.loads(headers)) # JSON格式请求
log_case_info(case_name, url, args, json.dumps(json.loads(expect_res), sort_keys=True),
json.dumps(res.json(), ensure_ascii=False, sort_keys=True))
self.assertDictEqual(res.json(), json.loads(expect_res))

简化后的用例:

test/case/user/test_user_login.py

from test.case.basecase import BaseCase

class TestUserLogin(BaseCase):   # 这里直接继承BaseCase
def test_user_login_normal(self):
"""level1:正常登录"""
case_data = self.get_case_data("test_user_login_normal")
self.send_request(case_data) def test_user_login_password_wrong(self):
"""密码错误登录"""
case_data = self.get_case_data("test_user_login_password_wrong")
self.send_request(case_data)

test/case/user/test_user_reg.py

from test.case.basecase import BaseCase
from lib.db import *
import json class TestUserReg(BaseCase): def test_user_reg_normal(self):
case_data = self.get_case_data("test_user_reg_normal") # 环境检查
name = json.loads(case_data.get("args")).get('name') # 范冰冰
if check_user(name):
del_user(name)
# 发送请求
self.send_request(case_data)
# 数据库断言
self.assertTrue(check_user(name))
# 环境清理
del_user(name) def test_user_reg_exist(self):
case_data = self.get_case_data("test_user_reg_exist") name = json.loads(case_data.get("args")).get('name')
# 环境检查
if not check_user(name):
add_user(name, '123456') # 发送请求
self.send_request(case_data)

更灵活的运行方式

之前我们的run_all.py只有运行所有用例一种选择,我们通过增加一些功能,提供更灵活的运行策略

运行自定义TestSuite

项目test/suite文件夹下新建test_suites.py

import unittest
import sys
sys.path.append("../..")
from test.case.user.test_user_login import TestUserLogin
from test.case.user.test_user_reg import TestUserReg smoke_suite = unittest.TestSuite() # 自定义的TestSuite
smoke_suite.addTests([TestUserLogin('test_user_login_normal'), TestUserReg('test_user_reg_normal')]) def get_suite(suite_name): # 获取TestSuite方法
return globals().get(suite_name)

修改run_all.pyrun.py,添加run_suite()方法

import unittest
from lib.HTMLTestReportCN import HTMLTestRunner
from config.config import *
from lib.send_email import send_email
from test.suite.test_suites import * def discover():
return unittest.defaultTestLoader.discover(test_case_path) def run(suite):
logging.info("================================== 测试开始 ==================================")
with open(report_file, 'wb') as f:
HTMLTestRunner(stream=f, title="Api Test", description="测试描述", tester="卡卡").run(suite) # send_email(report_file)
logging.info("================================== 测试结束 ==================================") def run_all(): # 运行所用用例
run(discover()) def run_suite(suite_name): # 运行`test/suite/test_suites.py`文件中自定义的TestSuite
suite = get_suite(suite_name)
if suite:
run(suite)
else:
print("TestSuite不存在")

只列出所有用例(并不执行)

run.py中添加

def collect():   # 由于使用discover() 组装的TestSuite是按文件夹目录多级嵌套的,我们把所有用例取出,放到一个无嵌套的TestSuite中,方便之后操作
suite = unittest.TestSuite() def _collect(tests): # 递归,如果下级元素还是TestSuite则继续往下找
if isinstance(tests, unittest.TestSuite):
if tests.countTestCases() != 0:
for i in tests:
_collect(i)
else:
suite.addTest(tests) # 如果下级元素是TestCase,则添加到TestSuite中 _collect(discover())
return suite def collect_only(): # 仅列出所用用例
t0 = time.time()
i = 0
for case in collect():
i += 1
print("{}.{}".format(str(i), case.id()))
print("----------------------------------------------------------------------")
print("Collect {} tests is {:.3f}s".format(str(i),time.time()-t0))

按testlist用例列表运行

test文件夹下新建testlist.txt,内容如下

test_user_login_normal
test_user_reg_normal
# test_user_reg_exist # 注释后不执行

run.py中添加

def makesuite_by_testlist(testlist_file):  # test_list_file配置在config/config.py中
with open(testlist_file) as f:
testlist = f.readlines() testlist = [i.strip() for i in testlist if not i.startswith("#")] # 去掉每行结尾的"/n"和 #号开头的行 suite = unittest.TestSuite()
all_cases = collect() # 所有用例
for case in all_cases: # 从所有用例中匹配用例方法名
if case._testMethodName in testlist:
suite.addTest(case)
return suite

按用例标签运行

由于TestSuite我们必须提前组装好,而为每个用例方法添加上标签,然后运行指定标签的用例能更加灵活

遗憾的是,unittest并没有tag相关功能,一种实现方案是:

def tag(tag):
if tag==OptionParser.options.tag: # 运行的命令行参数
return lambda func: func # 如果用例的tag==命令行指定的tag参数,返回用例本身
return unittest.skip("跳过不包含该tag的用例") # 否则跳过用例

用例标记方法

@tag("level1")
def test_a(self):
pass

这种方法在最后的报告中会出现很多skipped的用例,可能会干扰到因其他(如环境)原因需要跳过的用例

我这里的实现方法是通过判断用例方法中的docstring中加入特定的标签来重新组织TestSuite的方式

run.py中添加

def makesuite_by_tag(tag):
suite = unittest.TestSuite()
for case in collect():
if case._testMethodDoc and tag in case._testMethodDoc: # 如果用例方法存在docstring,并且docstring中包含本标签
suite.addTest(case)
return suite

用例标记方法

class TestUserLogin(BaseCase):
def test_user_login_normal(self):
"""level1:正常登录""" # level1及是一个标签,放到docstring哪里都可以
case_data = self.get_case_data("test_user_login_normal")
self.send_request(case_data)

重新运行上次失败用例

我们在每次执行后,通过执行结果result.failures获取到失败的用例,组装成TestSuite并序列化到指定文件中,rerun-fails时,反序列化得到上次执行失败的TestSuite, 然后运行

run.py中添加

import pickle
import sys def save_failures(result, file): # file为序列化保存的文件名,配置在config/config.py中
suite = unittest.TestSuite()
for case_result in result.failures: # 组装TestSuite
suite.addTest(case_result[0]) # case_result是个元祖,第一个元素是用例对象,后面是失败原因等等 with open(file, 'wb') as f:
pickle.dump(suite, f) # 序列化到指定文件 def rerun_fails(): # 失败用例重跑方法
sys.path.append(test_case_path) # 需要将用例路径添加到包搜索路径中,不然反序列化TestSuite会找不到用例
with open(last_fails_file, 'rb') as f:
suite = pickle.load(f) # 反序列化得到TestSuite
run(suite)

修改run.py中的run()方法,运行后保存失败用例序列化文件

def run(suite):
logging.info("================================== 测试开始 ==================================") with open(report_file, 'wb') as f:
# 结果赋予result变量
result = HTMLTestRunner(stream=f, title="Api Test", description="测试描述", tester="卡卡").run(suite) if result.failures: # 保存失败用例序列化文件
save_failures(result, last_fails_file) # send_email(report_file) # 从配置文件中读取
logging.info("================================== 测试结束 ==================================")

使用命令行参数

命令行参数是我们通过命令行调用run.py(执行入口文件)传递的一些参数,通过不同的参数,执行不同的运行策略,如python run.py --collect-only

我们通过optparser实现命令行参数:

config/config.py中添加

# 命令行选项
parser = OptionParser() parser.add_option('--collect-only', action='store_true', dest='collect_only', help='仅列出所有用例')
parser.add_option('--rerun-fails', action='store_true', dest='rerun_fails', help='运行上次失败的用例')
parser.add_option('--testlist', action='store_true', dest='testlist', help='运行test/testlist.txt列表指定用例') parser.add_option('--testsuite', action='store', dest='testsuite', help='运行指定的TestSuite')
parser.add_option('--tag', action='store', dest='tag', help='运行指定tag的用例') (options, args) = parser.parse_args() # 应用选项(使生效)
  • '--conllect-only'是参数名,dest='collect-only'指存储到 options.collect_only变量中,'store_true'指,如果有该参数,options.collect_only=True
  • 'store'指将--testsuite='smoke_suite',参数的值'smoke_suite'存到options.testsuite变量中

命令行选项使用方法:

run.py中添加:

from config.config import *

def main():
if options.collect_only: # 如果指定了--collect-only参数
collect_only()
elif options.rerun_fails: # 如果指定了--rerun-fails参数
rerun_fails()
elif options.testlist: # 如果指定了--testlist参数
run(makesuite_by_testlist(testlist_file))
elif options.testsuite: # 如果指定了--testsuite=***
run_suite(options.testsuite)
elif options.tag: # 如果指定了--tag=***
run(makesuite_by_tag(options.tag))
else: # 否则,运行所有用例
run_all() if __name__ == '__main__':
main() # 调用main()

运行结果:

C:\Users\hanzhichao\PycharmProjects\api_test_framework_finish>python run.py --collect-only
1.user.test_user_login.TestUserLogin.test_user_login_normal
2.user.test_user_login.TestUserLogin.test_user_login_password_wrong
3.user.test_user_reg.TestUserReg.test_user_reg_exist
4.user.test_user_reg.TestUserReg.test_user_reg_normal
----------------------------------------------------------------------
Collect 4 tests is 0.006s
C:\Users\hanzhichao\PycharmProjects\api_test_framework_finish>python run.py --rerun-fails
.
Time Elapsed: 0:00:00.081812
C:\Users\hanzhichao\PycharmProjects\api_test_framework_finish>python run.py --testlist
..
Time Elapsed: 0:00:00.454654
C:\Users\hanzhichao\PycharmProjects\api_test_framework_finish>python run.py --testsuite=smoke_suite
..
Time Elapsed: 0:00:00.471255
C:\Users\hanzhichao\PycharmProjects\api_test_framework_finish>python run.py --tag=level1
.
Time Elapsed: 0:00:00.062273
C:\Users\hanzhichao\PycharmProjects\api_test_framework_finish>python run.py
....
Time Elapsed: 0:00:00.663564

其他优化

1.按天生成log,每次执行生成新的报告

修改config/config.py

import time

today = time.strftime('%Y%m%d', time.localtime())
now = time.strftime('%Y%m%d_%H%M%S', time.localtime()) log_file = os.path.join(prj_path, 'log', 'log_{}.txt'.format(today)) # 更改路径到log目录下
report_file = os.path.join(prj_path, 'report', 'report_{}.html'.format(now)) # 更改路径到report目录下

2.增加send_email()开关

config/config.py增加

send_email_after_run = False

修改run.py

from config.config import *

def run(suite):
logging.info("================================== 测试开始 ==================================") with open(report_file, 'wb') as f: # 从配置文件中读取
result = HTMLTestRunner(stream=f, title="Api Test", description="测试描述", tester="卡卡").run(suite) if result.failures:
save_failures(result, last_fails_file) if send_email_after_run: # 是否发送邮件
send_email(report_file)
logging.info("================================== 测试结束 ==================================")

发送最新报告的问题稍后解决

源码地址: 链接:https://pan.baidu.com/s/1DLNSKN0KKuvSgo7gbGbMeg 密码:994e

此为北京龙腾育才 Python高级自动化(接口测试部分)授课笔记

课程介绍

想要参加现场(北京)/网络课程的可以联系作者微信:lockingfree

  1. 高效学习,快速掌握Python自动化所有领域技能
  2. 同步快速解决各种问题
  3. 配套实战项目练习