[python automation] July PytestAutoApi open source framework study notes (2)

Implementation process

Note: Please read the author's README.md document first https://gitee.com/yu_xiao_qi/pytest-auto-api2/blob/master/README.md

The contents of this section are as follows:

Directory Structure

├── common                         // 配置
│   ├── conf.yaml                  // 公共配置
│   ├── setting.py                 // 环境路径存放区域
├── data                           // 测试用例数据
├── File                           // 上传文件接口所需的文件存放区域
├── logs                           // 日志层
├── report                         // 测试报告层
├── test_case                      // 测试用例代码
├── utils                          // 工具类
│   └── assertion                
│       └── assert_control.py      // 断言
│       └── assert_type.py         // 断言类型
│   └── cache_process              // 缓存处理模块
│       └── cacheControl.py
│       └── redisControl.py  
│   └── logging_tool                   // 日志处理模块
│       └── log_control.py
│       └── log_decorator.py       // 日志装饰器
│       └── run_time_decorator.py   // 统计用例执行时长装饰器
│   └── mysql_tool                 // 数据库模块      
│       └── mysql_control.py   
│   └── notify                 // 通知模块
│       └── ding_talk.py     // 钉钉通知 
│       └── lark.py       // 飞书通知
│       └── send_mail.py     // 邮箱通知
│       └── wechat_send.py   // 企业微信通知
│   └── other_tools                 // 其他工具类
│       └── allure_data             // allure封装
│           └── allure_report_data.py // allure报告数据清洗
│           └── allure_tools.py   // allure 方法封装
│           └── error_case_excel.py   // 收集allure异常用例,生成excel测试报告
│       └── install_tool            
│           └── install_requirements.py
│           └── version_library_comparisons.txt
│       └── get_local_ip.py      // 获取本地IP
|		└── address_detection.py
|		└── exceptions.py
|		└── jsonpath_date_replace.py
|		└── models.py
|		└── thread_tool.py
│   └── read_files_tools             // 文件操作
│       └── case_automatic_control.py // 自动生成测试代码 
│       └── clean_files.py          // 清理文件
│       └── excel_control.py         // 读写excel
│       └── get_all_files_path.py   // 获取所有文件路径
│       └── get_yaml_data_analysis.py // yaml用例数据清洗
│       └── regular_control.py        // 正则
│       └── swagger_for_yaml.py         
│       └── yaml_control.py          
│       └── testcase_template.py          // yaml文件读写
│   └── recording             // 代理录制
│       └── mitmproxy_control.py
│   └── requests_tool 
│       └── dependentCase.py        // 数据依赖处理
│       └── encryption_algorithm_control.py      
│       └── request_control.py      // 请求封装
│       └── set_current_request_cache.py      
│       └── teardown_control.py      
│   └── times_tool
│       └── time_control.py      
├── Readme.md                       // help
├── pytest.ini                  
├── run.py                           // 运行入口  

parameter configuration

insert image description here

Entry file-run.py

1. Collect relevant use cases through pytest

2. Collect test data through allure_data

3. Send the data to the corresponding DingTalk, Feishu, WeChat, and email

4. If the program reports an error, an exception is thrown and an exception email is sent

pytest.ini

Specify the rules for pytest to collect use cases and execute use cases

test_case initializes data cache

In the files test_caseunder the folder __init__.py, case analysis is performed, and the yaml file is parsed into the cache file.

Since this code is placed __init__.pyin a file, when pytest collects use cases and executes them, it will execute the file first, that is, first load all the use case file content parameters into the cache. For cache knowledge points, please refer to the previous section. content.

from common.setting import ensure_path_sep
from utils.read_files_tools.get_yaml_data_analysis import CaseData
from utils.read_files_tools.get_all_files_path import get_all_files
from utils.cache_process.cache_control import CacheHandler, _cache_config


def write_case_process():
    """
    获取所有用例,写入用例池中
    :return:
    """

    # 循环拿到所有存放用例的文件路径
    for i in get_all_files(file_path=ensure_path_sep("\\data"), yaml_data_switch=True):
        # 循环读取文件中的数据
        case_process = CaseData(i).case_process(case_id_switch=True)
        if case_process is not None:
            # 转换数据类型
            for case in case_process:
                for k, v in case.items():
                    # 判断 case_id 是否已存在
                    case_id_exit = k in _cache_config.keys()
                    # 如果case_id 不存在,则将用例写入缓存池中
                    if case_id_exit is False:
                        CacheHandler.update_cache(cache_name=k, value=v)
                        # case_data[k] = v
                    # 当 case_id 为 True 存在时,则抛出异常
                    elif case_id_exit is True:
                        raise ValueError(f"case_id: {
      
      k} 存在重复项, 请修改case_id\n"
                                         f"文件路径: {
      
      i}")


write_case_process()

1. As shown in the above code , get_all_filesit will traverse to obtain dataall yaml and .yml files in the folder. These two files contain the specific steps of the test case.

2. CaseData(i).case_process(case_id_switch=True)To parse the test data, the corresponding code is in the utils->read_files_tools_analysis.py file. What is finally returned is data in a list and dictionary format. The specific details of this are discussed in the next section.

3. In each use case in the loop, for k, v in case.items()check whether the use case already exists in the cache pool by traversing the key-value pairs.

4. If the use case case_iddoes not exist 缓存_cache_config字典, the use case data will be CacheHandler.update_cache(cache_name=k, value=v)written to the cache pool. If it exists, case_idan exception will be thrown. Therefore, when writing the use case, the case_id must be unique.

5. The format stored in the cache pool is{"case_id_1":{"数据_1"},"case_id_2":{"数据_2"}}

Parse yaml test data

Following the second point above, CaseData(i).case_process(case_id_switch=True), the corresponding main code is as follows:

class CaseData(CaseDataCheck):

    def case_process(self, case_id_switch: Union[None, bool] = None):
        data = GetYamlData(self.file_path).get_yaml_data()
        case_list = []
        for key, values in data.items():
            # 公共配置中的数据,与用例数据不同,需要单独处理
            if key != 'case_common':
                self.case_data = values
                self.case_id = key
                super().check_params_exit()
                case_date = {
    
    
                    'method': self.get_method,
                    'is_run': self.case_data.get(TestCaseEnum.IS_RUN.value[0]),
                    'url': self.get_host,
                    'detail': self.case_data.get(TestCaseEnum.DETAIL.value[0]),
                    'headers': self.case_data.get(TestCaseEnum.HEADERS.value[0]),
                    'requestType': super().get_request_type,
                    'data': self.case_data.get(TestCaseEnum.DATA.value[0]),
                    'dependence_case': self.case_data.get(TestCaseEnum.DE_CASE.value[0]),
                    'dependence_case_data': self.get_dependence_case_data,
                    "current_request_set_cache": self.case_data.get(TestCaseEnum.CURRENT_RE_SET_CACHE.value[0]),
                    "sql": self.get_sql,
                    "assert_data": self.get_assert,
                    "setup_sql": self.case_data.get(TestCaseEnum.SETUP_SQL.value[0]),
                    "teardown": self.case_data.get(TestCaseEnum.TEARDOWN.value[0]),
                    "teardown_sql": self.case_data.get(TestCaseEnum.TEARDOWN_SQL.value[0]),
                    "sleep": self.case_data.get(TestCaseEnum.SLEEP.value[0]),
                }
                if case_id_switch is True:
                    case_list.append({
    
    key: TestCase(**case_date).dict()})
                else:
                    case_list.append(TestCase(**case_date).dict())

        return case_list

1. CaseDataThis class inherits CaseDataCheckthe class and is used for specification checking of use cases. It is easy to understand. Just look at CaseDataCheckthe source code directly. The source code is in the same file.

2. case_id_switchBy default, all inputs must be True, and subsequently they will be converted into key-value pairs. case_idAs key, the test data below it is used as value, and value is also a dictionary. Corresponding codecase_list.append({key: TestCase(**case_date).dict()})

3. Get the specific data in yaml,GetYamlData(self.file_path).get_yaml_data()

  • def get_yaml_data(self) -> dict:
        """
            获取 yaml 中的数据
            :param: fileDir:
            :return:
            """
        # 判断文件是否存在
        if os.path.exists(self.file_dir):
            data = open(self.file_dir, 'r', encoding='utf-8')
            res = yaml.load(data, Loader=yaml.FullLoader)
        else:
            raise FileNotFoundError("文件路径不存在")
            return res
    
  • The listed yaml file is taken from the source code, get_user_info.yaml

    • # 公共参数
      case_common:
        allureEpic: 开发平台接口
        allureFeature: 个人信息模块
        allureStory: 个人信息接口
      
      get_user_info_01:
          host: ${
              
              {
              
              host()}}
          url: /user/lg/userinfo/json
          method: GET
          detail: 正常获取个人身份信息
          headers:
            Content-Type: multipart/form-data;
            # 这里cookie的值,写的是存入缓存的名称
            cookie: $cache{
              
              login_cookie}
          # 请求的数据,是 params 还是 json、或者file、data
          requestType: data
          # 是否执行,空或者 true 都会执行
          is_run:
          data:
            # 是否有依赖业务,为空或者false则表示没有
          dependence_case: False
              # 依赖的数据
          dependence_case_data:
          assert:
            # 断言接口状态码
            errorCode:
              jsonpath: $.errorCode
              type: ==
              value: 0
              AssertType:
            # 断言接口返回的username
            username:
              jsonpath: $.data.userInfo.username
              type: ==
              value: '18800000001'
              AssertType:
          sql:
      
  • The format returned is as follows

    insert image description here

4. The data in the public configuration (case_common field) is different from the use case data and needs to be processed separately. The use case data is processed here.

5、super().check_params_exit():

  • insert image description here

  • def _assert(self, attr: Text):
        # 断言如果为真,程序继续运行,如果为假,则程序会抛出AssertionError异常,并附带后面括号的提示内容
        assert attr in self.case_data.keys(), (
            f"用例ID为 {
            
            self.case_id} 的用例中缺少 {
            
            attr} 参数,请确认用例内容是否编写规范."
            f"当前用例文件路径:{
            
            self.file_path}"
        )
    
    def check_params_exit(self):
        for enum in list(TestCaseEnum._value2member_map_.keys()):
            if enum[1]:
                self._assert(enum[0])
    
  • By judging the second value of the custom enumeration class parameter of TestCaseEnum, if it is True, it is a required item. If the required item is not filled in, the user is reminded to fill it in.

6. Each field needs to be verified. After the verification is completed, case_data is a dictionary, which is stored in the TestCase mapping dictionary and then stored in case_list. Finally return case_list.

Taking the column in the source code get_user_info.yamlas the column, the output result of the case_list returned by this step is as follows:

insert image description here

Test case execution

The test cases to be collected by pytest are all in the test_case folder

conftest.py

Located in the root directory of test_case

insert image description here

session level operations

1. First delete all files in the report folder, autouse=False, and only execute it once.

2. Get the login cookie, which is hard-coded here, autouse=True, and store the cookie value in the cache.

  • Cache file utils/cache_process/cache_control.py

  • There is an empty dictionary in the cache file _cache_config = {}. Every time it is added, it is placed in the dictionary in the form of key-value pairs. If needed, it is taken out from it.

  • _cache_config = {
          
          }
    
    
    class CacheHandler:
        @staticmethod
        def get_cache(cache_data):
            try:
                return _cache_config[cache_data]
            except KeyError:
                raise ValueNotFoundError(f"{
            
            cache_data}的缓存数据未找到,请检查是否将该数据存入缓存中")
    
        @staticmethod
        def update_cache(*, cache_name, value):
            _cache_config[cache_name] = value
    

3. Skip use cases

@pytest.fixture(scope="function", autouse=True)
def case_skip(in_data):
    """处理跳过用例"""
    in_data = TestCase(**in_data) # 这里直接是TestCase的对象,可以直接点出它下面的所有属性
    # 如果yaml文件中,is_run字段为False,则会执行以下代码,从而达到跳过该条用例的目的
    if ast.literal_eval(cache_regular(str(in_data.is_run))) is False:
        allure.dynamic.title(in_data.detail)
        allure_step_no(f"请求URL: {
      
      in_data.is_run}")
        allure_step_no(f"请求方式: {
      
      in_data.method}")
        allure_step("请求头: ", in_data.headers)
        allure_step("请求数据: ", in_data.data)
        allure_step("依赖数据: ", in_data.dependence_case_data)
        allure_step("预期数据: ", in_data.assert_data)
        pytest.skip()

Use case demo analysis

test_case -> UserInfo -> test_get_user_info.pyHere, the sample file in the source code is used as the demo file for our use case analysis.

code show as below:

import allure
import pytest
from utils.read_files_tools.get_yaml_data_analysis import GetTestCase
from utils.assertion.assert_control import Assert
from utils.requests_tool.request_control import RequestControl
from utils.read_files_tools.regular_control import regular
from utils.requests_tool.teardown_control import TearDownHandler


case_id = ['get_user_info_01']
TestData = GetTestCase.case_data(case_id)
re_data = regular(str(TestData))


@allure.epic("开发平台接口")
@allure.feature("个人信息模块")
class TestGetUserInfo:

    @allure.story("个人信息接口")
    @pytest.mark.parametrize('in_data', eval(re_data), ids=[i['detail'] for i in TestData])
    def test_get_user_info(self, in_data, case_skip):
        """
        :param :
        :return:
        """
        res = RequestControl(in_data).http_request()
        TearDownHandler(res).teardown_handle()
        Assert(assert_data=in_data['assert_data'],
               sql_data=res.sql_data,
               request_data=res.body,
               response_data=res.response_data,
               status_code=res.status_code).assert_type_handle()


if __name__ == '__main__':
    pytest.main(['test_test_get_user_info.py', '-s', '-W', 'ignore:Module already imported:pytest.PytestWarning'])

The corresponding yaml file is as follows:

# 公共参数
case_common:
  allureEpic: 开发平台接口
  allureFeature: 个人信息模块
  allureStory: 个人信息接口

get_user_info_01:
    host: ${
    
    {
    
    host()}}
    url: /user/lg/userinfo/json
    method: GET
    detail: 正常获取个人身份信息
    headers:
      Content-Type: multipart/form-data;
      # 这里cookie的值,写的是存入缓存的名称
      cookie: $cache{
    
    login_cookie}
    # 请求的数据,是 params 还是 json、或者file、data
    requestType: data
    # 是否执行,空或者 true 都会执行
    is_run:
    data:
      # 是否有依赖业务,为空或者false则表示没有
    dependence_case: False
        # 依赖的数据
    dependence_case_data:
    assert:
      # 断言接口状态码
      errorCode:
        jsonpath: $.errorCode
        type: ==
        value: 0
        AssertType:
      # 断言接口返回的username
      username:
        jsonpath: $.data.userInfo.username
        type: ==
        value: '18800000001'
        AssertType:
    sql:

Load data in yaml

1、case_id = [‘get_user_info_01’]

  • The case_id under a yaml file is stored in a list, and there can be multiple cases.

2、TestData = GetTestCase.case_data(case_id)

  • The format of TestData is [{case_data_1},{case_data_2}]
class GetTestCase:
    @staticmethod
    def case_data(case_id_lists: List):
        case_lists = []
        # 一个yaml文件中,有一个或多个case_id,把每一个case_id下的数据取出放到一个列表里
        for i in case_id_lists:
            _data = CacheHandler.get_cache(i)
            case_lists.append(_data)
        return case_lists
  • CacheHandler.get_cache(i) : Retrieve the data from the cache. TestData The format of the data obtained [[数据1],[数据2]]is where the retrieved data comes from - 解析yaml测试数据detailed analysis in the section above.

3、re_data = regular(str(TestData))

  • Convert TestData to str type

  • Use regular to replace request data, such as { { host() }} data, which needs to be processed, utils -> read_files_tools -> regular_control.py -> regular

  • insert image description here

In order to see the regular data more objectively, I get_user_info.yamlset all use cases except for skip. In the code above, I also added print data.

The first output target content is as follows: (To facilitate viewing, you need to combine it with the get_user_info.yaml file)

("[{'url': '${
    
    {host()}}/user/lg/userinfo/json', 'method': 'GET', 'detail': "
 "'正常获取个人身份信息', 'assert_data': {'errorCode': {'jsonpath': '$.errorCode', "
 "'type': '==', 'value': 0, 'AssertType': None}, 'username': {'jsonpath': "
 "'$.data.userInfo.username', 'type': '==', 'value': '18800000001', "
 "'AssertType': None}}, 'headers': {'Content-Type': 'multipart/form-data;', "
 "'cookie': '$cache{login_cookie}'}, 'requestType': 'DATA', 'is_run': None, "
 "'data': None, 'dependence_case': False, 'dependence_case_data': None, 'sql': "
 "None, 'setup_sql': None, 'status_code': None, 'teardown_sql': None, "
 "'teardown': None, 'current_request_set_cache': None, 'sleep': None}]")

The second output key content is as follows:

host()

It can be inferred that regular_pattern = r'\${ {(.*?)}}'this rule is to filter and extract { {host()}}parameters in the form of format.

  • 比如 key 是 int:host(1,2,3) 那就走if分支,提取方法名和参数
    如果 key 是 host(1,2,3) 那就走else分支,提取方法名和参数
    

The third output is the target under the else branch, which is the target after replacement.

欧耶耶耶耶耶else里面的target:[{
    
    'url': 'https://www.wanandroid.com/user/lg/userinfo/json', 'method': 'GET', 'detail': '正常获取个人身份信息', 'assert_data': {
    
    'errorCode': {
    
    'jsonpath': '$.errorCode', 'type': '==', 'value': 0, 'AssertType': None}, 'username': {
    
    'jsonpath': '$.data.userInfo.username', 'type': '==', 'value': '18800000001', 'AssertType': None}}, 'headers': {
    
    'Content-Type': 'multipart/form-data;', 'cookie': '$cache{login_cookie}'}, 'requestType': 'DATA', 'is_run': None, 'data': None, 'dependence_case': False, 'dependence_case_data': None, 'sql': None, 'setup_sql': None, 'status_code': None, 'teardown_sql': None, 'teardown': None, 'current_request_set_cache': None, 'sleep': None}]
  • Instead, use getattr to obtain the corresponding method below the Context() class. Adding a parentheses after it means calling it directly to get its return value.

    • If no parameters are used, if is used, and if there are parameters, else is used.

    • if value_name == "":
          value_data = getattr(Context(), func_name)()
      else:
          value_data = getattr(Context(), func_name)(*value_name.split(","))
      

After each execution, the new and old targets will be replaced until there are no { {host()}}parameters in the format of the string.

target = re.sub(regular_pattern, str(value_data), target, 1) # 1表示替换的最大次数为1

Similarly, $cache{login_02_v_code}there is a special cache_regularmethod to obtain data in the cache, but the rules are different. The processing steps are the same as above.

Data-driven execution use cases

@pytest.mark.parametrize('in_data', eval(re_data), ids=[i['detail'] for i in TestData])

re_dataThe data type here is <class 'str'>that we use the eval method to convert it into a list.

ids takes the detail description as the identifier in the allure report, as shown in the following figure:

  • insert image description here

The test cases are read and executed in a loop. There are as many test cases as the list is long.

Use case prefix

def test_get_user_info(self, in_data, case_skip):

in_data: Without too much explanation, it is all the processed data in the yaml file.

case_skip: This is an operation set in conftest.py, used to determine whether the use case should be skipped.

request request configuration

res = RequestControl(in_data).http_request()

The location of the corresponding method: \utils\requests_tool\request_control.py --> http_request()

in_data is the big string we ran earlier:

[{
    
    'url': 'https://www.wanandroid.com/user/lg/userinfo/json', 'method': 'GET', 'detail': '正常获取个人身份信息', 'assert_data': {
    
    'errorCode': {
    
    'jsonpath': '$.errorCode', 'type': '==', 'value': 0, 'AssertType': None}, 'username': {
    
    'jsonpath': '$.data.userInfo.username', 'type': '==', 'value': '18800000001', 'AssertType': None}}, 'headers': {
    
    'Content-Type': 'multipart/form-data;', 'cookie': '$cache{login_cookie}'}, 'requestType': 'DATA', 'is_run': None, 'data': None, 'dependence_case': False, 'dependence_case_data': None, 'sql': None, 'setup_sql': None, 'status_code': None, 'teardown_sql': None, 'teardown': None, 'current_request_set_cache': None, 'sleep': None}]

For related processing of request request types, such as json, params, file, data and other types, you can see the source code for details. Here I will do a process analysis and encapsulate it very comprehensively.

# 这里只是获取到了一个对象,当没有真的执行 ,这些对象方法都在当前py文件中
# 比如 self.request_type_for_json,只是将这个方法地址存在了 requests_type_mapping 字典中
# 在self.request_type_for_json后面加了 () 才代表执行了这个函数对象
requests_type_mapping = {
    
    
            RequestType.JSON.value: self.request_type_for_json,
            RequestType.NONE.value: self.request_type_for_none,
            RequestType.PARAMS.value: self.request_type_for_params,
            RequestType.FILE.value: self.request_type_for_file,
            RequestType.DATA.value: self.request_type_for_data,
            RequestType.EXPORT.value: self.request_type_for_export
        }

The following part deals with multiple business logic:

# 处理多业务逻辑
if dependent_switch is True:
    # 对jsonpath和依赖的数据进行替换
    DependentCase(self.__yaml_case).get_dependent_data()

res = requests_type_mapping.get(self.__yaml_case.requestType)(
    headers=self.__yaml_case.headers,
    method=self.__yaml_case.method,
    **kwargs
)

if self.__yaml_case.sleep is not None:
    time.sleep(self.__yaml_case.sleep)

_res_data = self._check_params(
    res=res,
    yaml_data=self.__yaml_case)

self.api_allure_step(
    url=_res_data.url,
    headers=str(_res_data.headers),
    method=_res_data.method,
    data=str(_res_data.body),
    assert_data=str(_res_data.assert_data),
    res_time=str(_res_data.res_time),
    res=_res_data.response_data
)
# 将当前请求数据存入缓存中
SetCurrentRequestCache(
    current_request_set_cache=self.__yaml_case.current_request_set_cache,
    request_data=self.__yaml_case.data,
    response_data=res
).set_caches_main()

return _res_data

Multi-business logic code analysis

Fragment 1
# 处理多业务逻辑
if dependent_switch is True:
    DependentCase(self.__yaml_case).get_dependent_data()

utils -> requests_tool -> dependent_case.py

  • def get_dependent_data(self) -> None:
        """
        jsonpath 和 依赖的数据,进行替换
        :return:
        """
        _dependent_data = DependentCase(self.__yaml_case).is_dependent()
        print("数据依赖ssssssssssssss:",_dependent_data)
        _new_data = None
        # 判断有依赖
        if _dependent_data is not None and _dependent_data is not False:
            # if _dependent_data is not False:
            for key, value in _dependent_data.items():
                # 通过jsonpath判断出需要替换数据的位置
                _change_data = key.split(".")
                # jsonpath 数据解析
                # 不要删 这个yaml_case
                yaml_case = self.__yaml_case
                _new_data = jsonpath_replace(change_data=_change_data, key_name='yaml_case')
                # 最终提取到的数据,转换成 __yaml_case.data
                _new_data += ' = ' + str(value)
                exec(_new_data)
    
  • _dependent_data = DependentCase(self.__yaml_case).is_dependent(), click on the source code yourself.

    • This code is a class method in Python, which specifically implements when running a test case, determines whether the test case has data dependencies, and obtains data based on the dependencies.

      The specific analysis is as follows:

      1. __yaml_caseIt is a private property of the current class, used to obtain test case-related data, including dependent types, dependent case data, dependent data settings, etc.
      2. _dependent_type = self.__yaml_case.dependence_caseGet whether the current test case needs to run data dependencies. TrueThe dependencies will only be executed when yes.
      3. _dependence_case_dates = self.__yaml_case.dependence_case_dataGet the dependent case data of the current test case.
      4. _setup_sql = self.__yaml_case.setup_sqlGet the setup_sql database statement, which is a pre-sql operation.
      5. Determine whether there are dependencies:
        • If _dependent_typeyes True, it means that the current use case requires running data dependencies.
        • Otherwise, it is returned False, indicating that the current use case does not require running data dependencies.
      6. If data dependency is required, the dependency data is obtained according to the dependency relationship and the dependency data is returned.
        • If the dependent data is SQL, _dependence_case_datesexecute the SQL statement in the database according to the relevant content and obtain the query results.
        • If the dependent data is the data in the request response body, jsonpaththe response data is obtained according to the expression and processed (such as replacement, storage, etc.) according to the value set by the dependency relationship.
        • If the dependent data is request data, the data is obtained from the request data according to jsonpaththe expression and processed.
        • If the dependent data type does not meet the requirements, an exception is thrown.
      7. Finally, there is the content of exception handling. If an exception occurs during the process of obtaining dependent data (such as KeyErroror TypeError), the exception will be thrown and the specific reason will be given.

      To sum up, the above code implements the function of data dependency processing, and obtains dependent data according to the dependency relationship.

Fragment 2
 # 假如请求的requestType是data
# 那下面代码相当于是 self.request_type_for_data( headers=self.__yaml_case.headers,method=self.__yaml_case.method,**kwargs)
res = requests_type_mapping.get(self.__yaml_case.requestType)(
    headers=self.__yaml_case.headers,
    method=self.__yaml_case.method,
    **kwargs
)

This code is a function call statement in Python, which uses the dictionary object get()method to achieve the function of obtaining value based on the key value. The function is to call the corresponding method according to the request type, then pass self.__yaml_case.headersand self.__yaml_case.methodto the method and return the result to the variable res, and also kwargspass other parameters in to the method.

The specific analysis is as follows:

  1. requests_type_mappingIs a dictionary object.

  2. self.__yaml_case.requestTypeObtained a string type request type (such as GET, POST, DELETE, etc.).

  3. requests_type_mapping.get(self.__yaml_case.requestType)
    

    According to the key value, the corresponding value value is obtained self.__yaml_case.requestTypefrom the dictionary object.requests_type_mapping

    • If the corresponding value can be found, the corresponding request will be executed according to the obtained function.
    • If requests_type_mappingthe corresponding key value does not exist in , the default value None is returned.
  4. If requests_type_mappingthe corresponding value value can be found in the dictionary object, the corresponding request is executed by calling the function, passing self.__yaml_case.headersand self.__yaml_case.methodas parameters to the function.

  5. **kwargsRepresents variable-length parameters, passing the parameters as named parameters to the called function, and executing the function.

  6. The result of the call is received by resthe variable, usually a request response object.

data dependency

insert image description here

Written in the following form:

insert image description here

Keywords:current_request_set_cache

Shown:

current_request_set_cache:
    # 1、response 从响应中提取内容 2、request从请求中提取内容
    - type: response
        jsonpath: $.data.data.[0].id
        # 自定义的缓存名称
        name: test_query_shop_brand_list_02_id

**Author's suggestion:** You can also extract relevant data in collect_addtool_01 and put it in the cache. Subsequent interfaces can read it directly from the cache.

insert image description here

Post-processing (data cleaning)

utils -> requests_tool -> teardown_control.py -> teardown_handle()

TearDownHandler(res).teardown_handle()

The res here is returned in the previous step. The data format of res is:

class ResponseData(BaseModel):
    url: Text
    is_run: Union[None, bool, Text]
    detail: Text
    response_data: Text
    request_body: Any
    method: Text
    sql_data: Dict
    yaml_data: "TestCase"
    headers: Dict
    cookie: Dict
    assert_data: Dict
    res_time: Union[int, float]
    status_code: int
    teardown: List["TearDown"] = None
    teardown_sql: Union[None, List]
    body: Any

insert image description here

  1. self._resIt is a private attribute of the current class and represents the result information after the current test case execution.
  2. _teardown_data = self._res.teardownGet teardown related data in the current test case.
  3. _resp_data = self._res.response_dataGet the interface response data of the current test case.
  4. _request_data = self._res.yaml_data.dataGet the request parameters of the interface.
  5. If teardown data exists, it will be processed differently based on keywords.
  6. If the teardown data contains keywords param_prepare, data such as forward request parameters need to be processed. The specific implementation is to call self.param_prepare_request_handler()the method, process the relevant data and return the result.
  7. If the teardown data contains keywords send_request, request data needs to be sent. The specific implementation is to call self.send_request_handler()the method, send relevant requests and process the results.
  8. Finally, call self.teardown_sql()the method to handle the data operation.
  9. In the process of teardown data processing, in order to determine whether the currently processed teardown data needs to send a request, two situations need to be distinguished, one is param_prepare and the other is send_request.

To sum up, this method mainly performs different data processing based on different teardown data operations, including sending requests, processing request parameters, etc. Since the teardown data operation may need to obtain information such as request responses of the current and previous test cases, the processing methods need to be distinguished.

Assertion operation

Assert(assert_data=in_data['assert_data'],
               sql_data=res.sql_data,
               request_data=res.body,
               response_data=res.response_data,
               status_code=res.status_code).assert_type_handle()

\utils\assertion\assert_control.py -> assert_type_handle()

Main code segment: (Other codes in this file can be read from top to bottom, they are neat and easy to understand)

def assert_type_handle(self):
    # 判断请求参数数据库断言
    if self.get_assert_type == "R_SQL":
        self._assert(self._assert_request_data, self.get_sql_data, self.get_message)

        # 判断请求参数为响应数据库断言
    elif self.get_assert_type == "SQL" or self.get_assert_type == "D_SQL":
        self._assert(self._assert_resp_data, self.get_sql_data, self.get_message)

        # 判断非数据库断言类型
    elif self.get_assert_type is None:
        self._assert(self._assert_resp_data, self.get_value, self.get_message)
    else:
        raise AssertTypeError("断言失败,目前只支持数据库断言和响应断言")

The specific analysis of this code is as follows:

  1. Get the assertion type ( get_assert_type) of the current instance.
  2. If the assertion type is "R_SQL", _assert()the method is called to assert the request parameters and database data.
  3. If the assertion type is "SQL" or "D_SQL", call _assert()the method to assert the response data and database data.
  4. If the assertion type is None, the method is called _assert()to assert the value in the response data and request parameters.
  5. If the assertion type is not one of the above three, a custom exception is thrown AssertTypeError, indicating that this method currently only supports database assertions and response assertions.

Note: _assert()The method is another private method in the class, used for specific data assertions.

To sum up, this method mainly calls different data assertion methods according to the assertion type. An exception is thrown if the type is not supported.

Assert the type contained in

class AssertMethod(Enum):
    """断言类型"""
    equals = "=="
    less_than = "lt"
    less_than_or_equals = "le"
    greater_than = "gt"
    greater_than_or_equals = "ge"
    not_equals = "not_eq"
    string_equals = "str_eq"
    length_equals = "len_eq"
    length_greater_than = "len_gt"
    length_greater_than_or_equals = 'len_ge'
    length_less_than = "len_lt"
    length_less_than_or_equals = 'len_le'
    contains = "contains"
    contained_by = 'contained_by'
    startswith = 'startswith'
    endswith = 'endswith'

Q&A with the author

Big brother, in your framework, dependency_case, the latest code is a little different from the tutorial on gitee.
1. In the tutorial, set_cache is used, and then the cached data is used.
2. Your latest code is an extraction expression of replace_key written directly.
I have written it in two ways. Are both methods possible?
Another question is, if the current use case A depends on use case B, then when use case A is run, will use case B be executed again?

收到,第一个问题推荐使用setcache关键字,replacekey为旧版本研发的功能,目前在特定场景下使用会存在一定的问题
第二个问题,是的,dependentcase类似源码调用函数的功能,需要使用的时候去调用它,可以减少部分冗余的用例字段

If you don't call it, can you use the keyword current_request_set_cache to extract relevant data from the use case you depend on and put it in the cache, and then other interfaces will read it directly from the cache?

是的

Finish

allure report generation

Data statistics summary

Result sent

Partial code analysis:

Fragment 1

pytest.main(['-s', '-W', 'ignore:Module already imported:pytest.PytestWarning',
             '--alluredir', './report/tmp', "--clean-alluredir"])

This code snippet runs the Pytest module in Python and passes in some parameters to set the test options. Specifically, the code does the following:

  1. pytest.main(): Call the function in the Pytest module main()to start running the test.
  2. ['-s', '-W', 'ignore:Module already imported:pytest.PytestWarning', '--alluredir', './report/tmp', "--clean-alluredir"]: Set the option parameters for Pytest to run tests.
    • -s: Indicates to output detailed information of all test results to the standard output stream, including log information, etc.
    • -W: Indicates to ignore specific types of warnings.
    • ignore:Module already imported:pytest.PytestWarning: Indicates to ignore the warning "Module already imported".
    • --alluredir: Indicates specifying the output directory of the Allure test report.
    • ./report/tmp: Indicates writing the test result data to the specified Allure test report data folder.
    • --clean-alluredir: Indicates that before generating the Allure test report, clear the old data in the specified directory to avoid affecting the generation of the test report.

This is how the code is executed and what each parameter does. When executing Pytest tests, you can use these parameters to customize how the tests are run and the generation of test reports.

Fragment 2

os.system(r"allure generate ./report/tmp -o ./report/html --clean")

This line of code calls a system command in Python and uses os.system()the function to execute the system command. The specific command is:

allure generate ./report/tmp -o ./report/html --clean

This command uses the Allure tool to generate a test report, ./report/tmpreads the test result data from the path, and then outputs the report to ./report/htmlthe path.

Among them, --cleanthe parameter indicates that before generating the test report, clear the output directory to prevent the old test report files from interfering with the new test report.

Because it is os.system()a system command called through a function, the execution result of this code is consistent with the execution result of the command line. A new Allure report will be generated if the execution is successful, and an exception will be thrown if the execution fails.

Guess you like

Origin blog.csdn.net/qq_46158060/article/details/132690514