zoukankan      html  css  js  c++  java
  • Python接口测试实战4(下)

    本节内容

    • 使用用例基类
    • 自定义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:UsershanzhichaoPycharmProjectsapi_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:UsershanzhichaoPycharmProjectsapi_test_framework_finish>python run.py --rerun-fails
    .
    Time Elapsed: 0:00:00.081812
    C:UsershanzhichaoPycharmProjectsapi_test_framework_finish>python run.py --testlist
    ..
    Time Elapsed: 0:00:00.454654
    C:UsershanzhichaoPycharmProjectsapi_test_framework_finish>python run.py --testsuite=smoke_suite
    ..
    Time Elapsed: 0:00:00.471255
    C:UsershanzhichaoPycharmProjectsapi_test_framework_finish>python run.py --tag=level1
    .
    Time Elapsed: 0:00:00.062273
    C:UsershanzhichaoPycharmProjectsapi_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



  • 相关阅读:
    MSSQL大量数据时,建立索引或添加字段后保存更改超时该这么办
    POJ 3261 Milk Patterns (后缀数组)
    POJ 1743 Musical Theme (后缀数组)
    HDU 1496 Equations (HASH)
    694. Distinct Substrings (后缀数组)
    POJ 1222 EXTENDED LIGHTS OUT (枚举 或者 高斯消元)
    POJ 1681· Painter's Problem (位压缩 或 高斯消元)
    POJ 1054 The Troublesome Frog (hash散列)
    HDU 1716 排列2
    HDU 4405 Aeroplane chess (概率DP & 期望)
  • 原文地址:https://www.cnblogs.com/gdg87813/p/11225582.html
Copyright © 2011-2022 走看看