美文网首页接口编程技术类Python
Python接口测试实战4(下) - 框架完善:用例基类,用例标

Python接口测试实战4(下) - 框架完善:用例基类,用例标

作者: 韩志超 | 来源:发表于2018-09-21 19:49 被阅读354次

    课程目录

    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获取

    本节内容

    • 使用用例基类
    • 自定义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')
            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))
                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))   # 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目录下
    
    1. 增加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接口测试实战4(下) - 框架完善:用例基类,用例标

      本文链接:https://www.haomeiwen.com/subject/tysznftx.html