Python文本变量与函数的解析执行,增强自动化测试数据驱动


关注我,每天分享软件测试技术干货、面试经验,想要领取测试资料、进入软件测试学习交流群的可以直接私信我哦~~

我们在使用Python进行自动化测试或者测试脚本开发时,通常会在代码中融入数据驱动设计,以便于降低代码开发、维护成本,例如我们在进行接口自动化测试时,我们会使用不同的数据(入参数据、期望结果数据),驱动同一条自动化测试用例执行,已验证该接口在不同场景下功能是否正常,通常我们会将这些数据存储在数据库、Yaml、Excel或其他文件中。

在数据驱动的具体实现设计中,我们使用的各数据类型通常都是固定值(静态值),比如固定的字符串、数字、列表、字典等等,来驱动自动化测试用例或者脚本的执行。

但当在需要非固定(动态)进行数据驱动测试时,例如,在进行接口测试时,请求体中存在 “time”(当前时间)属性,每次发送请求时,都需要使用当前时间。那么该如何设计代码实现数据驱动呢 ?

本文,通过Python 解析字符串中变量名、函数名(函数的参数),同时支持加载指定自定义模块,获取模块方法及变量对象,以实现动态加载字符串并将字符串中的变量名、函数名,替换为变量值、函数返回值。

我们可以通过下面示例,更直观地了解一下:

示例

例如,我们加载数据驱动文件(YAML格式 ) ,其中包含 变 量 名 、 变量名 、 {函数名($变量名, )} 内容。

# Yaml
SignMap:
    - TIME: ${now_time()}
    - PHONE: ${phone($MODULE)}
    - TERMINALNAME: $TERMINAL_NAME
    - DESC: 当前时间为:${now_time()} ,联系电话为:${phone($MODULE)}

上述Yaml文件中的变量、函数,我们可以在指定的一个或多个自定义模块中进行设计、开发。如下,我们在 built_in.py 模块中设计了MODULE、TERMINAL_NAME 变量,以及now_time、phone 函数。

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
  
import datetime

MODULE = "2"
TERMINAL_NAME= "alibaba"

def now_time():
    """
    
    Returns:
    """
    curr_time = datetime.datetime.now()
    return curr_time.strftime("%Y-%m-%d")

def phone(module):
    """
    
    Args:
        module: 模式

    Returns:
    """
    if module =="1":
        return "188520011314"
    else:
        return "0532-81910921"


通过已实现 ParseContent 类 中的解析方法,即可完成解析,如下:

if __name__ == '__main__':
	  # 创建解析方法对象 
    parse_context = ParseContent()

    # 加载 yaml 内容 , 如下
    yaml_context = Utils.load_yaml(r'F:\code\moc\conf\DataJob.yml')

    # 加载自定义模块
    parse_context.add_module_variables_functions(
      																												module_dirname=r"F:\code\moc",
                                                              module_file_name="built_in.py")
    # 解析 yaml 中数据 
    result = parse_context.parse_content(yaml_context)
		print(result) 
    
    # 解析字符串
    result_str =  parse_context.parse_content("TIME: ${now_time()}")
		print(result)

执行如上代码,上述 Yaml文件内容,解析结果如下 :

 # 解析 yaml 中数据    
{
        "SignMap": [
            {
                "TIME": "2021-03-22"
            },
            {
                "PHONE": "0532-819109210"
            },
            {
                "TERMINALNAME": "alibaba"
            },
            {
                "DESC": "当前时间为:2021-03-22 ,联系电话为:0532-819109210"
            }
        ]
  }

# 解析字符串
TIME: 2021-03-23

解析方法 源码

为了方便阅读,源码中,将各模块代码合入一个模块中。

#!/usr/bin/env python
# -*- encoding: utf-8 -*-

"""
@File    :   new_context_parse.py
@Contact :   https://www.toutiao.com/c/user/token/MS4wLjABAAAAakx_PBJsQXpljEkaBcc0pEteSDMYxTbbBrlQ6F4p3yQ/

@Modify Time      @Author           @Version    @Desciption
------------      -------           --------    -----------
2021/3/22 00:47   软件测试开发技术栈    1.0         None
"""

import ast
import json
import re
import yaml
import os
import sys
import types
import importlib
from collections import defaultdict
from collections import OrderedDict
from compat import basestring, builtin_str, numeric_types, str


class MyBaseError(Exception):
    pass


class FileFormatError(MyBaseError):
    pass


class ParamsError(MyBaseError):
    pass


class NotFoundError(MyBaseError):
    pass


class FileNotFound(FileNotFoundError, NotFoundError):
    pass


class FunctionNotFound(NotFoundError):
    pass


class VariableNotFound(NotFoundError):
    pass


class LoadModule:

    def __init__(self):

        self.custom_module_info = {
            "variables": {},
            "functions": {}
        }

        self.project_working_directory = ""

    @staticmethod
    def is_function(_type: types) -> bool:
        """
        判断对象是否为函数
        Args:
            _type: 对象实际类型

        Returns:

        """
        return isinstance(_type, types.FunctionType)

    @staticmethod
    def is_variable(name: str, _type: types) -> bool:
        """
        判断对象是否为变量,不含私有变量
        Args:
            name:
            _type:

        Returns:

        """
        if callable(_type):
            return False

        if isinstance(_type, types.ModuleType):
            return False

        if name.startswith("_"):
            return False

        return True

    @staticmethod
    def locate_file(start_file_or_dir_path: str, file_name: str) -> str:
        """
        递归查询,返回文件绝对路径
        Args:
            start_file_or_dir_path: 起始目录
            file_name: 文件名称,包含文件类型后缀

        Returns: 不存在时,抛出异常

        Raises:
            exceptions.FileNotFound: 文件不存在

        """

        if os.path.isfile(start_file_or_dir_path):
            start_dir_path = os.path.dirname(start_file_or_dir_path)

        elif os.path.isdir(start_file_or_dir_path):
            start_dir_path = start_file_or_dir_path

        else:
            raise FileNotFound("invalid path: {}".format(start_file_or_dir_path))

        file_path = os.path.join(start_dir_path, file_name)

        if os.path.isfile(file_path):
            return file_path

        # 当前工作目录
        if os.path.abspath(start_dir_path) in [os.getcwd(), os.path.abspath(os.sep)]:
            raise FileNotFound("{} not found in {}".format(file_name, start_file_or_dir_path))

        # 递归向上查找
        return LoadModule.locate_file(os.path.dirname(start_dir_path), file_name)

    @staticmethod
    def locate_py(start_path, module_file_name) -> str or None:
        """
        递归查询,返回文件绝对路径
        Args:
            start_file_or_dir_path: 起始目录
            file_name: 文件名称,包含文件类型后缀

        Returns: 不存在时,返回 None

        Raises:
            exceptions.FileNotFound: 文件不存在

        """
        try:
            path = LoadModule.locate_file(start_path, module_file_name)
            return os.path.abspath(path)

        except FileNotFound:
            return None

    @staticmethod
    def load_module_with_object(module_object: types.ModuleType) -> dict:
        """
        通过模块对象的方式 加载 Python指定模块.获取函数与变量信息。

        Args:
            module_object (Object): python module 对象, module_object = importlib.import_module(module_name)

        Returns:
            dict: 指定python模块的变量和函数字典,字典格式:

                {
                    "variables": {},
                    "functions": {}
                }

        """
        _custom_module_info = defaultdict(dict)

        for _name, _type in vars(module_object).items():

            if LoadModule.is_function(_type):
                _custom_module_info["functions"][_name] = _type

            elif LoadModule.is_variable(_name, _type):
                if isinstance(_type, tuple):
                    continue
                _custom_module_info["variables"][_name] = _type

            else:
                # 过滤掉私有变量、模块等
                pass

        return _custom_module_info

    def load_module_with_name(self, module_name: str) -> None:
        """
           通过模块名字(不含)的方式 加载 Python指定模块.获取函数与变量信息。应该位于项目工作目录中
        Args:
            module_name: 模块名称, 不含后缀

        Returns:
            dict: 指定python模块的变量和函数字典,字典格式:
                {
                    "variables": {},
                    "functions": {}
                }

        """

        imported_module = importlib.import_module(module_name)

        _custom_module_info = LoadModule.load_module_with_object(imported_module)
        # 更新
        self.custom_module_info.update(_custom_module_info)

    def load_specified_path_module(self, start_path: str, module_file_name_with_py: str) -> None:
        """
        通过模块名字(含后缀)的方式 加载 Python指定模块.获取函数与变量信息。
        Args:
            start_path:
            module_file_name_with_py: 模块名字(含后缀)

        Returns:

        """

        """ load  .env, .py.
            api/testcases folder is relative to project_working_directory

        Args:
            module_file_name_with_py: 
            start_path (str):
            module_file_name(str):
        """

        module_path = LoadModule.locate_py(start_path, module_file_name_with_py)

        # 模块工作目录.
        if module_path:

            self.project_working_directory = os.path.dirname(module_path)
        else:
            # 当前目录兜底
            self.project_working_directory = os.getcwd()

        if module_path:
            # 将当前目录作为最优加载目录
            sys.path.insert(0, self.project_working_directory)

            module_name = os.path.splitext(module_file_name_with_py)[0]

            self.load_module_with_name(module_name)


class Utils:
    """
    工具类
    """

    @staticmethod
    def load_yaml(yaml_file_path: str) -> dict:
        """
        加载 yaml文件
        Args:
            yaml_file_path: yaml文件路径,绝对或相对路径

        Returns:
            dict

        """
        # 将yaml格式内容 转换成 dict类型
        with open(yaml_file_path, encoding="utf-8") as read_yaml:
            yaml_context_dict = yaml.load(read_yaml.read(), Loader=yaml.Loader)

        return yaml_context_dict

    @staticmethod
    def string_value_number(possible_number: str) -> int or float or str:
        """
        将允许为数字的字符串,解析为数字
        Args:
            possible_number: 可能为数字的字符串

        Returns:

        Examples:
             "9527" => 9527
             "9527.2" => 9527.3
             "abc" => "abc"
             "$name" => "$name"
        """

        try:
            return ast.literal_eval(possible_number)

        except ValueError:
            return possible_number

        except SyntaxError:
            return possible_number

    @staticmethod
    def convert_list_to_dict(mapping_list: list) -> dict:
        """ 将列表转换为有序字典

        Args:
            mapping_list: 列表
                [
                    {"a": 1},
                    {"b": 2}
                ]

        Returns:
            OrderedDict:

                {
                    "a": 1,
                    "b": 2
                }


        """
        ordered_dict = OrderedDict()

        for map_dict in mapping_list:
            ordered_dict.update(map_dict)

        return ordered_dict

    @staticmethod
    def extract_functions(content: str) -> list:
        """ 从字符串内容中提取所有函数,格式为${fun()}

        Args:
            content : 字符串的内容

        Returns:
            list: functions list extracted from string content

        Examples:
            >>> Utils.extract_functions("${func(1)}")
            ["func(1)"]

            >>> Utils.extract_functions("${func(a=1, b=2)}")
            ["func(a=1, b=2)"]

            >>> Utils.extract_functions("/api/${func(1, 2)}")
            ["func(1, 2)"]

            >>> Utils.extract_functions("/api/${func(1, 2)}?_s=${func2()}")
            ["func(1, 2)", "func2()"]

        """
        try:
            return re.findall(StaticVariable.FUNCTION_REGEXP, content)
        except TypeError:
            return []

    @staticmethod
    def extract_variables(content: str) -> list:
        """ 从字符串内容中提取所有变量名,格式为$variable

        Args:
            content : 字符串的内容

        Returns:
            list: 从字符串内容中提取的变量列表

        Examples:
            >>> Utils.extract_variables("$phone")
            ["phone"]

            >>> Utils.extract_variables("/api/$phone")
            ["phone"]

            >>> Utils.extract_variables("/$phone/$name")
            ["phone", "name"]

        """
        try:
            return re.findall(StaticVariable.VARIABLE_REGEXP, content)
        except TypeError:
            return []

    @staticmethod
    def parse_string_variables(content: str, variables_mapping: dict) -> str:
        """ 用变量映射,替换出字符串内容中提取所有变量名。

        Args:
            content : 字符串的内容
            variables_mapping : 变量映射.

        Returns:
            str: 解析字符串内容。

        Examples:
            >>> content = '$TERMINAL_NAME'
            >>> variables_mapping = {"$TERMINAL_NAME": "alibaba"}
            >>> Utils.parse_string_variables(content, variables_mapping)
                "alibaba"

        """

        def get_mapping_variable(__variable_name: str):
            """ 从 variable_mapping 中获取变量。
            Args:
                __variable_name: 变量名称

            Returns:
                变量值.

            Raises:
                exceptions.VariableNotFound: 变量不存在

            """
            try:
                return variables_mapping[__variable_name]
            except KeyError:
                # print variable_name
                raise VariableNotFound("{} is not found.".format(__variable_name))

        variables_list = Utils.extract_variables(content)
        for variable_name in variables_list:

            variable_value = get_mapping_variable(variable_name)

            if "${}".format(variable_name) == content:
                content = variable_value
            else:
                if not isinstance(variable_value, str):
                    variable_value = builtin_str(variable_value)

                content = content.replace(
                    "${}".format(variable_name),
                    variable_value, 1
                )

        return content

    @staticmethod
    def parse_function(content: str) -> dict:
        """ 从字符串内容中解析函数名和参数。

        Args:
            content : 字符串的内容

        Returns:
            dict:
                {
                    "func_name": "xxx",
                    "func_args": [],
                    "func_kwargs": {}
                }

        Examples:
            >>> Utils.parse_function("func()")
            {'func_name': 'func', 'func_args': [], 'func_kwargs': {}}

            >>> Utils.parse_function("func(1)")
            {'func_name': 'func', 'func_args': [1], 'func_kwargs': {}}

            >>> Utils.parse_function("func(1, 2)")
            {'func_name': 'func', 'func_args': [1, 2], 'func_kwargs': {}}

            >>> Utils.parse_function("func(a=1, b=2)")
            {'func_name': 'func', 'func_args': [], 'func_kwargs': {'a': 1, 'b': 2}}

            >>> Utils.parse_function("func(1, 2, a=3, b=4)")
            {'func_name': 'func', 'func_args': [1, 2], 'func_kwargs': {'a':3, 'b':4}}

        """
        matched = StaticVariable.FUNCTION_REGEXP_COMPILE.match(content)
        if not matched:
            raise FunctionNotFound("{} not found!".format(content))

        function_meta = {
            "func_name": matched.group(1),
            "func_args": [],
            "func_kwargs": {}
        }

        args_str = matched.group(2).strip()
        if args_str == "":
            return function_meta

        args_list = args_str.split(',')
        for arg in args_list:
            arg = arg.strip()
            if '=' in arg:
                key, value = arg.split('=')
                function_meta["func_kwargs"][key.strip()] = Utils.string_value_number(value.strip())
            else:
                function_meta["func_args"].append(Utils.string_value_number(arg))

        return function_meta

    @staticmethod
    def get_mapping_function(function_name: str, functions_mapping: dict) -> types.FunctionType or None:
        """ 从functions_mapping中获取函数,如果没有找到,那么尝试检查是否内置函数。

        Args:
            function_name: 函数名称
            functions_mapping: 函数映射


        Returns:
                函数对象
        Raises:
            exceptions.FunctionNotFound: 函数没有定义

        """
        if function_name in functions_mapping:
            return functions_mapping[function_name]

        try:
            # 判断是否是内置函数
            item_func = eval(function_name)
            if callable(item_func):
                return item_func
        except (NameError, TypeError):
            raise FunctionNotFound("{} is not found.".format(function_name))


class StaticVariable:
    # 变量正则规则
    VARIABLE_REGEXP = r"\$([\w_.]+)"
    # 函数正则规则
    FUNCTION_REGEXP = r"\$\{([\w_]+\([\$\w\.\-/_ =,]*\))\}"
    # 函数正则规则
    FUNCTION_REGEXP_COMPILE = re.compile(r"^([\w_]+)\(([$\w.\-/_ =,]*)\)$")


class ParseContent:
    def __init__(self):

        self.variables_mapping = {}
        self.functions_mapping = {}

    def parse(self, content: (str, dict, list, numeric_types, bool, type)) -> list or dict or str:

        """ 解析变量|函数映射值

        Args:
            content :
            variables_mapping : 变量映射
            functions_mapping : 函数映射

        Returns:
            parsed content.

        Examples:
            >>> content = {
                    'SignMap':
                        [
                            {'TIME': '${now_time()}'},
                            { 'PHONE': '${phone($MODULE)}'},
                            {'TERMINALNAME': '$TERMINAL_NAME'}
                        ]
                    }

            >>> variables_mapping = {'MODULE': '2', 'TERMINAL_NAME': 'alibaba'}
            >>> functions_mapping = {'now_time': '<function now_time at 0x00000142659A8C18>', 'phone': '<function phone at 0x00000142659B9AF8>', }
            >>> self.parse(content, variables_mapping)
                {
                    'SignMap':
                        [
                            {'TIME': '2021-03-20'},
                            { 'PHONE': '0532-819109210'},
                            {'TERMINALNAME': 'alibaba'}
                        ]
                    }
                }

        """

        if content is None or isinstance(content, (numeric_types, bool, type)):
            return content

        if isinstance(content, (list, set, tuple)):
            return [
                self.parse(item, )
                for item in content
            ]

        if isinstance(content, dict):
            parsed_content = {}
            for key, value in content.items():
                parsed_key = self.parse(key)
                parsed_value = self.parse(value)
                parsed_content[parsed_key] = parsed_value
            return parsed_content

        if isinstance(content, basestring):
            _variables_mapping = self.variables_mapping or {}
            _functions_mapping = self.functions_mapping or {}
            content = content.strip()

            # 用求值替换函数
            content = self.parse_string_functions(content)
            # 用绑定值替换变量
            content = Utils.parse_string_variables(content, _variables_mapping)

        return content

    def update_original_context_var_func(self, _variables_mapping: dict, _functions_mapping: dict) -> None:
        """
        将模块原始函数、变量更新到对象属性中
        Args:
            _variables_mapping:
            _functions_mapping:

        Returns:

        """
        self.variables_mapping.update(_variables_mapping)
        self.functions_mapping.update(_functions_mapping)

    def update_context_variables(self, _variables: (str, list, dict)) -> None:
        """
        更新上下文中的变量
        Args:
            _variables:

        Returns:

        """

        if isinstance(_variables, list) or isinstance(_variables, dict):

            if isinstance(_variables, list):
                _variables = Utils.convert_list_to_dict(_variables)

            for variable_name, variable_value in _variables.items():
                variable_eval_value = self.parse(variable_value)

                self.variables_mapping[variable_name] = variable_eval_value
        else:
            variable_eval_value = self.parse(_variables)
            self.variables_mapping[_variables] = variable_eval_value

    def parse_string_functions(self, content: str) -> str:
        """ 用函数映射解析字符串内容。

        Args:
            content : 要解析的字符串内容。

        Returns:
            str: 解析字符串内容。

        Examples:
            >>> content = '${now_time()}'
            >>> self.parse_string_functions(content)
                '2021-03-20'

        """
        functions_list = Utils.extract_functions(content)

        for func_content in functions_list:

            function_meta = Utils.parse_function(func_content)
            func_name = function_meta["func_name"]
            args = function_meta.get("func_args", [])
            kwargs = function_meta.get("func_kwargs", {})
            args = self.parse(args)
            kwargs = self.parse(kwargs)

            func = Utils.get_mapping_function(func_name, self.functions_mapping)
            eval_value = func(*args, **kwargs)
            func_content = "${" + func_content + "}"
            if func_content == content:
                content = eval_value
            else:
                # 字符串包含一个或多个函数或其他内容
                content = content.replace(
                    func_content,
                    str(eval_value), 1
                )

        return content

    def add_module_variables_functions(self, module_dirname, module_file_name) -> None:
        """
        添加模块属性、函数
        Args:
            module_dirname:
            module_file_name:
            context:

        Returns:

        """

        load_module = LoadModule()

        load_module.load_specified_path_module(module_dirname, module_file_name)

        custom_module_info = load_module.custom_module_info

        _variables_mapping = custom_module_info["variables"]

        _functions_mapping = custom_module_info["functions"]

        self.update_original_context_var_func(_variables_mapping, _functions_mapping)

    def parse_content(self, context):
        """
        解析内容
        Args:
            context:

        Returns:

        """
        self.update_context_variables(context)

        return self.parse(context)


if __name__ == '__main__':
    parse_context = ParseContent()

    # 加载 yaml 内容 ,如下
    yaml_context = Utils.load_yaml(r'E:\PythonCode\MOC\conf\DataJob.yml')

    # 加载指定 built_in 模块
    parse_context.add_module_variables_functions(
        module_dirname=r"E:\PythonCode\MOC",
        module_file_name="built_in.py")

    # 加载指定 common 模块
    parse_context.add_module_variables_functions(
        module_dirname=r"E:\PythonCode\MOC",
        module_file_name="common.py")

    # 解析 yaml 中数据 
    result = parse_context.parse_content(yaml_context)
    print(result)
    # {'SignMap': [{'TIME': '2021-03-23'}, {'PHONE': '0532-819109210'}, {'TERMINALNAME': 'alibaba'}]}

    # 解析字符串
    result_str = parse_context.parse_content("TIME: ${now_time()}")
    print(result_str)
    # TIME: 2021-03-23

上述代码已经完成改造,支持添加多个自定义模块,解耦模块加载和字符串解析功能。

一句话送给你们:

世界的模样取决于你凝视它的目光,自己的价值取决于你的追求和心态,一切美好的愿望,不在等待中拥有,而是在奋斗中争取。


如果你

①从事功能测试,想进阶自动化测试

②在测试界混了1、2年,依然不会敲代码

③面试大厂却屡屡碰壁

我邀你进群吧!来吧~~测试员,313782132(Q群里有技术大牛一起交流分享,学习资源的价值取决于你的行动,莫做“收藏家”)获取更多大厂技术、面试资料


金九银十面试季,跳槽季。给大家整理的资料,整体是围绕着【软件测试】来进行整理的,主体内容包含:python自动化测试专属视频、Python自动化详细资料、全套面试题等知识内容。愿你我相遇,皆有所获! 关注我领取~

如果文章对你有帮助,麻烦伸出发财小手点个赞,感谢您的支持,你的点赞是我持续更新的动力。

推荐阅读:

什么样的人适合从事软件测试工作?

谈谈从小公司进入大厂,我都做对了哪些事?

想转行做软件测试?快来看看你适不适合

软件测试从自学到工作,软件测试学习到底要怎样进行?

软件测试工程师简历项目经验怎么写?–1000个已成功入职的软件测试工程师简历范文模板(真实简历)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值