【接口自动化测试平台】测试执行引擎开发笔记

测试平台执行引擎开发笔记(CLY)

一、测试引擎架构设计

1、执行引擎方案
  • 接入测试工具,如:Jmeter、MeterSphere

  • 利用第三方自动化框架,如:httprunner(很多培训机构都是采用此方案)

  • 用python自己封装

2、测试执行引擎架构设计

在这里插入图片描述

二、测试数据设计

1、单个测试用例
"""
====================用例数据格式设计====================
用例名称:XXX

接口信息:
    接口地址
    请求方法

请求头信息:

请求参数信息:
    查询参数
    请求体:
        json格式
        表单格式
        文件上传

前置脚本:
    随机生成测试数据
    查询数据库
    定义变量

后置脚本:
    断言
    提取数据
    定义变量

--------------------------------------------
请求库: requests or httpx
示例:requests.post(url, data, params, json, files...)

用例数据中的变量,存放在ENV中:
    ${{变量名}}


"""
case = {
    "title": '',
    "interface": {
        "url": '/system-api/system/auth/login',
        "method": "post"
    },
    "headers": {
        "Content-Type": "application/json"
    },
    "request": {
        "params": {},
        "json": {
            "username": "test1",
            "password": "admin123",
        },
        "data": {},
    },
    "setup_script": '',
    "teardown_script": '',
}

2、测试环境
"""
====================测试环境格式设计====================
环境名称:XXX

基础url:

请求头:

全局变量

调式变量

全局工具函数

数据库配置:

"""
ENV = {
    "name": '',
    "base_url": 'http://172.30.1.88:38180/',
    "headers": {},
    "global_vars": {
        "username": 'test1',
        "password": 'admin123'
    },
    "debug_vars": {},
    "func_tools": '',
    "db": [
        {
            "name": "local",
            "type": "mysql",
            "config": {
                "host": "127.0.0.1",
                "port": 3306,
                'user': 'root',
                'password': '123456',
                'database': 'test001'
            }
        },
        {
            "name": "bmos",
            "type": "mysql",
            "config": {
                "host": "172.30.1.88",
                "port": 3306,
                'user': 'root',
                'password': '不告诉你',
                'database': 'test002'
            }
        }
    ],
}

三、测试执行基类设计

1、测试执行基类搭建
"""
=======================测试执行基类设计V1.0====================
1、执行前置脚本
2、发送请求
3、执行后置脚本
4、执行入口
"""
class BaseCase:
    def __setup_script(self, data):
    	# 执行前置脚本
        pass

    def __teardown_script(self, data, response):
        # 执行后置脚本
        
    def __send_request(self, data):
        # 发送请求

    def perform(self, data):
    	# 记录当前用例数据
        self.data = data
        # 执行前置脚本
        self.__setup_script(data)
        # 发送请求
        response = self.__send_request(data)
        # 执行后置脚本
        self.__teardown_script(data, response)
2、测试执行基类——测试数据处理
"""
=======================测试执行基类设计V2.0====================
1、用例数据初始化
2、用例数据格式组装(符合requests库的格式)
3、用例数据正则替换
"""
class BaseCase:
	def __init__(self, env):
		# 获取每个用例的环境数据
		self.env = env

	def __replace_data(self, request_data):
		# 正则替换 ${{变量名}}
        pattern = r'\${{(.+?)}}'
        request_data = str(request_data)
        while re.search(pattern, request_data):
            match = re.search(pattern, request_data)
            key = match.group(1)
            value = self.env.get('global_vars', {}).get(key, '')
            request_data = request_data.replace(match.group(), str(value))
        return eval(request_data)
    
    def __handle_requests_data(self, data):
        # 处理请求数据
        
        # 1-处理method
        request_data = {'method': data['interface']['method'].upper()}
        
        # 2-处理url
        url = data['interface']['url']
        if not url.startswith('http'):
            base_url = self.env.get('base_url').rstrip('/')
            url = f'{base_url}/{url.lstrip("/")}'
        request_data['url'] = url

        # 3-处理headers
        request_data['headers'] = {**self.env.get('headers', {}), **data.get('headers', {})}

        # 4-处理查询参数
        request = data['request']
        request_data['params'] = request.get('params', {})
        
        # 5-处理请求体参数
        content_type = request_data['headers'].get('Content-Type', '').lower()
        if content_type == 'application/json':
            request_data['json'] = request.get('json')
            self.request_body = request_data['json']
        elif content_type == 'application/x-www-form-urlencoded':
            request_data['data'] = request.get('data')
            self.request_body = request.get('data')
        elif 'multipart/form-data' in content_type:
            request_data['files'] = request.get('files')
            self.request_body = request.get('files')

        # 调用正则替换变量的方法
        request_data = self.__replace_data(request_data)
        return request_data
    
    def __send_request(self, data):
        # 发送请求
        # 1-处理请求数据
        request_data = self.__handle_requests_data(data)
        # 2-发送请求
        response = requests.request(**request_data)
        # 3-返回响应结果
        return response
    
    def ...(其他方法不变)
    
3、测试执行基类——前后置脚本执行
class BaseCase:
	...
	def __run_script(self, data):
        # 在前后置脚本执行环境中,内置一些变量
        test = self
        # 获取环境变量
        global_vars = self.env['global_vars']
		
		# 获取前置脚本数据(str)
        setup_script = data.get("setup_script", '')
        # 执行前置脚本
        exec(setup_script)
        
        # 等待后续执行
        response = yield
		
		# 获取后置脚本数据(str)
        teardown_script = data.get("teardown_script", '')
        # 执行后置脚本
        exec(teardown_script)
        yield

    def __setup_script(self, data):
    	# 执行前置脚本
    	# 获得一个脚本生成器
        self.script_hook = self.__run_script(data)
        # 执行脚本直到yield
        next(self.script_hook)

    def __teardown_script(self, data, response):
    	# 执行后置脚本
    	# 发送响应结果并执行脚本到yield
        self.script_hook.send(response)
        # 删除生成器
        delattr(self, 'script_hook')
    def ...
4、测试执行基类——响应结果提取
class BaseCase:
	...
	def re_extract(self, obj, ext):
		# 正则提取
        if not isinstance(obj, str):
            obj = str(obj)
        res = re.search(ext, obj)
        value = res.group(1) if res else ''
        return value

    def json_extract(self, obj, ext):
    	# jsonpath提取》单个数据
        res = jsonpath(obj, ext)
        value = res[0] if res else ''
        return value

    def json_extract_list(self, obj, ext):
    	# jsonpath提取》列表数据
        res = jsonpath(obj, ext)
        value = res if res else []
        return value
    def ...
5、测试执行基类——断言封装
class BaseCase(HandleLogData):
	...
    def assertion(self, method, expect, actual):
    	# 断言
    	# 断言类型函数映射
        method_map = {
            "等于": lambda x, y: x == y,
            "不等于": lambda x, y: x != y,
            "大于": lambda x, y: x > y,
            "大于等于": lambda x, y: x >= y,
            "小于": lambda x, y: x < y,
            "小于等于": lambda x, y: x <= y,
            "包含": lambda x, y: x in y,
            "不包含": lambda x, y: x not in y,
        }
        # 获取断言函数
        assert_func = method_map.get(method, None)
        
        if not assert_func:
        	print('不支持的断言方法:', method)
            return
        else:
            print('断言方法:', method)
        try:
            assert assert_func(actual, expect)
        except AssertionError:
            raise AssertionError('断言失败,预期结果:', expect, '实际结果:', actual)
        else:
            print('断言成功,预期结果:', expect, '实际结果:', actual)
    def ...
6、测试执行基类——环境变量封装
class BaseCase(HandleLogData):
	...
    def save_global_vars(self, name, value):
    	# 保存到环境变量
        self.env['global_vars'][name] = value

    def del_global_vars(self, name):
    	# 从环境变量删除
        del self.env['global_vars'][name]
    def ...

四、其他工具封装

1、日志管理器
class HandleLogData:
    def save_log(self, level, msg):
    	# 将所有输出的log信息,保存到 self.lob_data 属性中
        if not hasattr(self, 'log_data'):
            setattr(self, 'log_data', [])
        getattr(self, 'log_data').append((level, msg, datetime.now()))
        print((level, msg, datetime.now()))

    def print_log(self, *args):
        msg = ' '.join(str(i) for i in args)
        self.save_log(level='Print', msg=msg)

    def debug_log(self, *args):
        msg = ' '.join(str(i) for i in args)
        self.save_log(level='Debug', msg=msg)

    def info_log(self, *args):
        msg = ' '.join(str(i) for i in args)
        self.save_log(level='Info', msg=msg)

    def warning_log(self, *args):
        msg = ' '.join(str(i) for i in args)
        self.save_log(level='Warning', msg=msg)

    def error_log(self, *args):
        msg = ' '.join(str(i) for i in args)
        self.save_log(level='Error', msg=msg)

    def critical_log(self, *args):
        msg = ' '.join(str(i) for i in args)
        self.save_log(level='Critical', msg=msg)

======================================================在测试执行基类中使用==============================================
class BaseCase(HandleLogData):
	# 继承日志处理类,在基类中需要输出保存的东西,调用父类的 self.xxx_log(*args) 方法
	...
2、数据库管理器
class BaseDB:
	# 数据库基类
    conn = None
    cursor = None

    def execute(self, sql, args=None):
    # 执行SQL语句,返回一行数据
        try:
            self.cursor.execute(sql, args)
            return self.cursor.fetchone()
        except Exception as e:
            raise e

    def execute_list(self, sql, args):
    # 执行SQL语句,返回所有行数据
        try:
            self.cursor.execute(sql, args)
            return self.cursor.fetchall()
        except Exception as e:
            raise e

    def close(self):
    # 关闭数据库链接
        self.cursor.close()
        self.conn.close()


class OracleDB(BaseDB):
	# Oracle数据库
    def __init__(self, db_config):
        self.conn = oracledb.connect(**db_config, autocommit=True)
        self.cursor = self.conn.cursor(pymssql.cursors.DictCursor)


class SqlServerDB(BaseDB):
	# sqlserver数据库
    def __init__(self, db_config):
        self.conn = pymssql.connect(**db_config, autocommit=True)
        self.cursor = self.conn.cursor(pymssql.cursors.DictCursor)


class MysqlDB(BaseDB):
	# mysql数据库
    def __init__(self, db_config):
        self.conn = pymysql.connect(**db_config, autocommit=True)
        self.cursor = self.conn.cursor(pymysql.cursors.DictCursor)


class DBClient:
    """数据库链接工具"""

    def init_connection(self, dbs):
        if isinstance(dbs, dict):
            self.create_db_connection(dbs)
        elif isinstance(dbs, list):
            for db in dbs:
                if isinstance(db, dict):
                    self.create_db_connection(db)
                else:
                    raise TypeError('数据库配置格式错误')
        else:
            raise TypeError('数据库配置格式错误')

    def create_db_connection(self, db: dict):
    	# 获取数据库链接对象,保存为属性
        if not (db.get('name') and db.get('type') and db.get('config')):
            raise TypeError('数据库配置格式错误')
        if db.get('type') == 'mysql':
            setattr(self, db.get('name'), MysqlDB(db.get('config')))

        # elif db.get('type') == 'sqlserver':
        #     setattr(self, db.get('name'), SqlServerDB(db.get('config')))
        # elif db.get('type') == 'oracle':
        #	  setattr(self, db.get('name'), OracleDB(db.get('config')))

    def close_db_connection(self):
    	# 关闭数据链接
        for db in self.__dict__:
            if isinstance(self.__dict__[db], BaseDB):
                self.__dict__[db].close()
======================================================在测试执行基类中使用==============================================
class BaseCase(HandleLogData):
	...
	def perform(self, data):
	# 创建数据库链接对象
        db = DBClient()
    # 初始化数据库链接
        db.init_connection(self.env_data['db'])
    # 执行发送请求相关其他操作
    	...
    # 关闭数据库链接
    	db.close_db_connection()
    def ...
3、全局工具函数管理
======================================================创建一个工具函数py文件==============================================
# 文件名:functools_demo.py

fk = Faker(locale='zh-CN')


def random_name():
    return fk.name()

def ...(定义各种函数)
==========================================创建一个py文件(作为存放全局工具函数的命名空间)=======================================
handle_func.py
======================================================在测试执行基类中使用=================================================
import handle_func as func_tools

# 将内置函数加载到工具函数命名空间
exec(ENV['func_tools'], func_tools.__dict__)  # ENV的数据参考:测试数据设计-测试环境

class BaseCase(HandleLogData):
	...
	
======================================================在前后置脚本中使用=================================================
name=func_tools.random_name()

4、测试数据说明
======================================================用例数据:前后置脚本=================================================
case = {
    "title": '',
    "interface": {
        "url": '/system-api/system/auth/login',
        "method": "post"
    },
    "headers": {
        "Content-Type": "application/json"
    },
    "request": {
        "params": {},
        "json": {
            "username": "${{username}}",  # 这里会自动进行正则替换
            "password": "${{password}}",  # 这里会自动进行正则替换
        },
        "data": {},
    },
    "setup_script": open(setup_script_path, 'r', encoding='utf-8').read(),  # 前后置脚本都是str格式,比如读取某个txt文件
    "teardown_script": open(teardown_script_path, 'r', encoding='utf-8').read(),  # 前后置脚本都是str格式,比如读取某个txt文件
}
======================================================前后置脚本示例=================================================
# 文件名:setup_script_demo.txt

print("执行前置脚本")
print('当前变量:', global_vars)

name=func_tools.random_name()
idno = func_tools.random_idno()

test.save_global_vars('name', name)
test.save_global_vars('idno', idno)

======================================================环境数据:全局工具函数=================================================
ENV = {
    ...
    "func_tools": open(file_path, 'r', encoding='utf-8').read(),
    ...
=======================================================全局工具函数示例=================================================
from datetime import datetime, timedelta

from faker import Faker

fk = Faker(locale='zh-CN')


def random_name():
    return fk.name()


def random_idno():
    return fk.ssn()


def now_datetime(add=None, sub=None):
    fmt = "%Y-%m-%d %H:%M:%S"
    if add is not None:
        return (datetime.now() + timedelta(seconds=add)).strftime(fmt)
    elif sub is not None:
        return (datetime.now() - timedelta(seconds=sub)).strftime(fmt)
    return datetime.now().strftime(fmt)

五、测试套件

以上执行单条用例足够,但实际工作中往往需要一次运行多个测试场景,接下来考虑再写个测试套件

1、测试套件方案
  • 使用pytest或unittest
  • 用python写
2、测试数据(包含多个测试场景)
cases = [
    {
        "name": "演示的用例集",
        "cases": [
            {
                "title": '',
                "interface": {
                    "url": '/system-api/system/auth/login',
                    "method": "post"
                },
                "headers": {
                    "Content-Type": "application/json",
                },
                "request": {
                    "params": {},
                    "json": {
                        "username": "${{username}}",
                        "password": "${{password}}",
                    },
                    "data": {},
                },
                "setup_script": open(setup_script_path, 'r', encoding='utf-8').read(),
                "teardown_script": open(teardown_script_path, 'r', encoding='utf-8').read(),
            }, {
                "title": '',
                "interface": {
                    "url": '/system-api/donor/create-donor',
                    "method": "post"
                },
                "headers": {
                    "Content-Type": "application/json",
                    "Authorization": "Bearer ${{token}}"
                },
                "request": {
                    "params": {},
                    "json": {
                        "marriage": 0,
                        "outJob": 0,
                        "buildMethod": 2,
                        "recruitCode": [],
                        "address": "四川省营山县消水镇巴岩村9组",
                        "name": "${{name}}",
                        "nation": 1,
                        "sex": 1,
                        "idNoImageFront": "/bmos-v2/system/idCardPic/ID_CARD_76593e00a98c4900850e2385900e2c7b_1711010241512.png",
                        "idNoAddress": "四川省营山县消水镇巴岩村9组",
                        "issueOffice": "营山县公安局",
                        "birth": "1996-09-29",
                        "idNo": "${{idno}}",
                        "idNoValidityPre": "2017-07-20",
                        "idNoValidityAfter": "2027-07-20",
                        "validType": 2,
                        "regionCodes": [
                            {
                                "code": "510000000000",
                                "regionName": "四川省",
                                "pCode": "000000000000"
                            },
                            {
                                "code": "511300000000",
                                "regionName": "南充市",
                                "pCode": "510000000000"
                            },
                            {
                                "code": "511322000000",
                                "regionName": "营山县",
                                "pCode": "511300000000"
                            },
                            {
                                "code": "511322109000",
                                "regionName": "消水镇",
                                "pCode": "511322000000"
                            }
                        ],
                        "faceUrl": "/bmos-v2/system/facePic/FACE_PIC_296f7d46d1eb4bd7a7fc4be22830a884_1711010245200.png",
                        "similarity": 71,
                        "age": 27,
                        "donateBloodCheck": "通过",
                        "addressCommunity": "511322109002",
                        "areaCode": "511322109002",
                        "education": 1,
                        "job": 1,
                        "phone": "15111111111",
                        "donorBindReqVO": {
                            "userRecruiterBindCreate": [],
                            "userRecruiterBindUpdate": [],
                            "userRecruiterBindDelete": []
                        }
                    },
                    "data": {},
                },
                "setup_script": open(setup_script_path2, 'r', encoding='utf-8').read(),
                "teardown_script": open(teardown_script_path2, 'r', encoding='utf-8').read(),
            },

        ]
    }, {
        "name": "演示的用例集2",
        "cases": []
    }
]
3、测试结果记录器
class TestResulter:
    def __init__(self, case_count, name='调试运行'):
        self.all = case_count
        self.success = 0
        self.error = 0
        self.fail = 0
        self.name = name
        # 保存结果
        self.result = []

    def add_success(self, test: BaseCase):
        """
        :param test: 用例信息,BaseCase
        :return:
        """
        self.success += 1
        info = {
            'name': getattr(test, 'title', ''),
            'url': getattr(test, 'url', ''),
            'method': getattr(test, 'method', ''),
            'status': 'success',
            'status_code': getattr(test, 'status_code', ''),
            'request_headers': getattr(test, 'request_headers', ''),
            'response_headers': getattr(test, 'response_headers', ''),
            'request_body': getattr(test, 'request_body', ''),
            'response_body': getattr(test, 'response_body', ''),
            'log_data': getattr(test, 'log_data', '')
        }
        self.result.append(info)

    def add_error(self, test: BaseCase, error):
        test.error_log('用例执行错误,错误信息如下:')
        test.error_log(error)
        self.error += 1
        info = {
            'name': getattr(test, 'title', ''),
            'url': getattr(test, 'url', ''),
            'method': getattr(test, 'method', ''),
            'status': 'error',
            'status_code': getattr(test, 'status_code', ''),
            'request_headers': getattr(test, 'request_headers', ''),
            'response_headers': getattr(test, 'response_headers', ''),
            'request_body': getattr(test, 'request_body', ''),
            'response_body': getattr(test, 'response_body', ''),
            'log_data': getattr(test, 'log_data', '')
        }
        self.result.append(info)

    def add_fail(self, test: BaseCase):
        self.fail += 1
        info = {
            'title': getattr(test, 'title', ''),
            'url': getattr(test, 'url', ''),
            'method': getattr(test, 'method', ''),
            'status': 'fail',
            'status_code': getattr(test, 'status_code', ''),
            'request_headers': getattr(test, 'request_headers', ''),
            'response_headers': getattr(test, 'response_headers', ''),
            'request_body': getattr(test, 'request_body', ''),
            'response_body': getattr(test, 'response_body', ''),
            'log_data': getattr(test, 'log_data', '')
        }
        self.result.append(info)

    def get_result(self):
        if self.success == self.all:
            state = 'success'
        elif self.error > 0:
            state = 'error'
        else:
            state = 'fail'
        return {
            'name': self.name,
            'all': self.all,
            'success': self.success,
            'fail': self.fail,
            'error': self.error,
            'result': self.result,
            'state': state,
        }
4、测试执行器
# 定义存放环境数据的变量
BaseEnv = {}

class TestRunner(object):

    def __init__(self, cases, env_data):
		# 传入测试数据和环境数据
        self.cases = cases
        self.env_data = env_data
        self.results = []

    def run(self):
        global BaseEnv
        # 创建数据库链接对象
        db = DBClient()
        # 初始化数据库链接
        db.init_connection(self.env_data['db'])

        # 遍历测试用例
        for items in self.cases:
            # 每次执行测试套件前清理环境变量,保证每个测试套件的环境变量都只是初始环境变量
            BaseEnv.clear()
            BaseEnv = copy.deepcopy(self.env_data)
            # 将内置函数加载到工具函数命名空间
            exec(BaseEnv['func_tools'], func_tools.__dict__)

            print('测试流名称(测试套件):', items['name'])
            # 每个测试套件创建一个测试结果记录器
            case_count = len(items['cases'])
            resulter = TestResulter(case_count=case_count, name=items['name'])
            for case in items['cases']:
                self.perform(case, resulter, BaseEnv)
            # 获取执行结果
            result = resulter.get_result()
            self.results.append(result)

        # 断开数据库链接
        db.close_db_connection()
        return self.results

    def perform(self, case, resulter, BaseEnv):
        c = BaseCase(BaseEnv)
        # 执行单条测试用例
        try:
            c.perform(case)
        except AssertionError as e:
            resulter.add_fail(c)
        except Exception as e:
            resulter.add_error(c, e)
        else:
            resulter.add_success(c)

if __name__ == '__main__':
    results = TestRunner(cases=cases, env_data=ENV).run()

六、其他

1、说明

因为这个引擎主要为测试平台开发,所以用例数据写起来很大一堆,实际使用时,到时候会在页面填写接口测试用例,自动保存到数据库,然后通过数据库读出来

相对于apifox:

​ 1、解决数据同步问题,方便维护使用管理

​ 2、支持定时任务

​ 3、支持定制化扩展功能

​ …

环境部署后续写笔记

2、运行效果图

在这里插入图片描述

  • 11
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值