• apitest学习


     

    程说明图

    这张图是我的一些设计思路。

    yaml文件中管理相关的数据即可实现接口测试。

    采用的接口是智学网网站的API。

    支持token认证

    img

    框架体系介绍

    目录/文件说明是否为python
    apiData 存放测试信息和用例的yaml文件目录  
    config 配置目录,目录配置,allure环境变量配置
    common 公共类,封装读取yaml文件
    core 封装requests等常用方法
    logs 日志文件  
    tests 测试用例
    utils 工具类,日志等
    pytest.ini pytest配置文件  
    run.bat 执行脚本  

    配置用例信息

    经过excel和yaml的对比,最终我选择了yaml文件管理用例信息。

    BusinessInterface.yaml

    业务接口测试

    登录验证:
      method: post
      route: /loginSuccess/
      RequestData:
        data:
          userId: "{{data}}"
      expectcode: 200
      regularcheck:
      resultcheck: '"result":"success"'
    

    stand_alone_interface.yaml

    单个接口测试

    登录:
      method: post
      route: /weakPwdLogin/?from=web_login
      RequestData:
        data:
          loginName: 18291900215
          password: dd636482aca022
          code:
          description: encrypt
      expectcode: 200
      regularcheck: '[d]{16}'
      resultcheck: '"result":"success"'
      extractresult:
        - data
    

    配置测试信息

    testInfo.yaml

    测试信息配置

    test_info: # 测试信息
      url: https://www.zhixue.com
      timeout: 30.0
      headers:
        Accept: application/json, text/javascript, */*; q=0.01
        Accept-Encoding: gzip, deflate, br
        Accept-Language: zh-CN,zh;q=0.9
        Connection: keep-alive
        User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
        cookies: aliyungf_tc=AQAAANNdlkvZ2QYAIb2Q221oiyiSOfhg; tlsysSessionId=cf0a8657-4a02-4a40-8530-ca54889da838; isJump=true; deviceId=27763EA6-04F9-4269-A2D5-59BA0FB1F154; 6e12c6a9-fda1-41b3-82ec-cc496762788d=webim-visitor-69CJM3RYGHMP79F7XV2M; loginUserName=18291900215
        X-Requested-With: XMLHttpRequest
    
    

    读取信息

    ApiData.py

    #!/usr/bin/env python3
    # -*- coding:utf-8 -*-
    import os
    from ruamel import yaml
    from config.conf import DATA_DIR
    
    
    class ApiInfo:
        """接口信息"""
    
        def __init__(self):
            self.info_path = os.path.join(DATA_DIR, 'testinfo.yaml')
            self.business_path = os.path.join(DATA_DIR, 'BusinessInterface.yaml')
            self.stand_alone_path = os.path.join(DATA_DIR, 'stand_alone_interface.yaml')
    
        @classmethod
        def load(cls, path):
            with open(path, encoding='utf-8') as f:
                return yaml.safe_load(f)
    
        @property
        def info(self):
            return self.load(self.info_path)
    
        @property
        def business(self):
            return self.load(self.business_path)
    
        @property
        def stand_alone(self):
            return self.load(self.stand_alone_path)
    
        def test_info(self, value):
            """测试信息"""
            return self.info['test_info'][value]
    
        def login_info(self, value):
            """登录信息"""
            return self.stand_alone['登录'].get(value)
    
        def case_info(self, name):
            """用例信息"""
            return self.business[name]
    
        def stand_info(self, name):
            """单个接口"""
            return self.stand_alone[name]
    
    
    testinfo = ApiInfo()
    
    if __name__ == '__main__':
        print(testinfo.info['test_info'])
    
    
    

    封装日志

    logger.py

    #!/usr/bin/env python3
    # -*- coding:utf-8 -*-
    import os
    import logging
    from config import conf
    from datetime import datetime
    
    
    class Logger:
        def __init__(self, name):
            self.logger = logging.getLogger(name)
            if not self.logger.handlers:
                self.logger.setLevel(logging.DEBUG)
    
                # 创建一个handler,用于写入日志文件
                fh = logging.FileHandler(self.log_path, encoding='utf-8')
                fh.setLevel(logging.DEBUG)
    
                # 在控制台输出
                ch = logging.StreamHandler()
                ch.setLevel(logging.INFO)
    
                # 定义hanler的格式
                formatter = logging.Formatter(self.fmt)
                fh.setFormatter(formatter)
                ch.setFormatter(formatter)
    
                # 给log添加handles
                self.logger.addHandler(fh)
                self.logger.addHandler(ch)
    
        @property
        def fmt(self):
            return '%(asctime)s %(levelname)s %(filename)s:%(lineno)d %(message)s'
    
        @property
        def log_path(self):
            if not os.path.exists(conf.LOG_PATH):
                os.makedirs(conf.LOG_PATH)
            month = datetime.now().strftime("%Y%m")
            return os.path.join(conf.LOG_PATH, '{}.log'.format(month))
    
    
    log = Logger('root').logger
    if __name__ == '__main__':
        log.info("你好")
    
    
    

    封装requests

    request.py

    #!/usr/bin/env python3
    # -*- coding:utf-8 -*-
    import json
    import allure
    import urllib3
    import requests
    from utils.logger import log
    from requests import Response
    from requests.status_codes import codes
    from requests.exceptions import RequestException
    from common.ApiData import testinfo
    from common.RegExp import regexps
    from core.serialize import deserialization, serialization
    from core.getresult import get_result
    
    urllib3.disable_warnings()
    
    __all__ = ['req', 'codes']
    
    
    class HttpRequest(object):
        """requests方法二次封装"""
    
        def __init__(self):
            self.timeout = 30.0
            self.r = requests.session()
            self.headers = testinfo.test_info('headers')
    
        def send_request(self, method: str, route: str, extract: str, **kwargs):
            """发送请求
            :param method: 发送方法
            :param route: 发送路径
            optional 可选参数
            :param extract: 要提取的值
            :param params: 发送参数-"GET"
            :param data: 发送表单-"POST"
            :param json: 发送json-"post"
            :param headers: 头文件
            :param cookies: 验证字典
            :param files: 上传文件,字典:类似文件的对象``
            :param timeout: 等待服务器发送的时间
            :param auth: 基本/摘要/自定义HTTP身份验证
            :param allow_redirects: 允许重定向,默认为True
            :type bool
            :param proxies: 字典映射协议或协议和代理URL的主机名。
            :param stream: 是否立即下载响应内容。默认为“False”。
            :type bool
            :param verify: (可选)一个布尔值,在这种情况下,它控制是否验证服务器的TLS证书或字符串,在这种情况下,它必须是路径到一个CA包使用。默认为“True”。
            :type bool
            :param cert: 如果是字符串,则为ssl客户端证书文件(.pem)的路径
            :return: request响应
            """
            pass
            method = method.upper()
            url = testinfo.test_info('url') + route
            try:
                log.info("Request Url: {}".format(url))
                log.info("Request Method: {}".format(method))
                if kwargs:
                    kwargs_str = serialization(kwargs)
                    is_sub = regexps.findall(kwargs_str)
                    if is_sub:
                        new_kwargs_str = deserialization(regexps.subs(is_sub, kwargs_str))
                        log.info("Request Data: {}".format(new_kwargs_str))
                        kwargs = new_kwargs_str
                log.info("Request Data: {}".format(kwargs))
                if method == "GET":
                    response = self.r.get(url, **kwargs, headers=self.headers, timeout=self.timeout)
                elif method == "POST":
                    response = self.r.post(url, **kwargs, headers=self.headers, timeout=self.timeout)
                elif method == "PUT":
                    response = self.r.put(url, **kwargs, headers=self.headers, timeout=self.timeout)
                elif method == "DELETE":
                    response = self.r.delete(url, **kwargs, headers=self.headers, timeout=self.timeout)
                elif method in ("OPTIONS", "HEAD", "PATCH"):
                    response = self.r.request(method, url, **kwargs, headers=self.headers, timeout=self.timeout)
                else:
                    raise AttributeError("send request method is ERROR!")
                with allure.step("%s请求接口" % method):
                    allure.attach(url, name="请求地址")
                    allure.attach(str(response.headers), "请求头")
                    if kwargs:
                        allure.attach(json.dumps(kwargs, ensure_ascii=False), name="请求参数")
                    allure.attach(str(response.status_code), name="响应状态码")
                    allure.attach(str(elapsed_time(response)), name="响应时间")
                    allure.attach(response.text, "响应内容")
                log.info(response)
                log.info("Response Data: {}".format(response.text))
                if extract:
                    get_result(response, extract)
                return response
            except RequestException as e:
                log.exception(format(e))
            except Exception as e:
                raise e
    
        def __call__(self, *args, **kwargs):
            return self.send_request(*args, **kwargs)
    
        def close_session(self):
            print("关闭会话")
            self.r.close()
    
    
    def elapsed_time(func: Response, fixed: str = 's'):
        """
        用时函数
        :param func: response实例
        :param fixed: 1或1000 秒或毫秒
        :return:
        """
        try:
            if fixed.lower() == 's':
                second = func.elapsed.total_seconds()
            elif fixed.lower() == 'ms':
                second = func.elapsed.total_seconds() * 1000
            else:
                raise ValueError("{} not in ['s','ms']".format(fixed))
            return second
        except RequestException as e:
            log.exception(e)
        except Exception as e:
            raise e
    
    
    req = HttpRequest()
    if __name__ == '__main__':
        pass
    
    

    前置条件

    conftest.py

    #!/usr/bin/env python3
    # -*- coding:utf-8 -*-
    import json
    import pytest
    from core.request import req
    from common.ApiData import testinfo
    from core.checkresult import check_results
    
    
    @pytest.fixture(scope='session')
    def is_login(request):
        """登录"""
        r = req(testinfo.login_info('method'), testinfo.login_info('route'),
                testinfo.login_info('extractresult'), **testinfo.login_info('RequestData'))
        result = json.loads(r.text)
        check_results(r, testinfo.stand_info('登录'))
        if 'token' in result:
            req.headers['Authorization'] = "JWT " + result['token']
    
        def fn():
            req.close_session()
    
        request.addfinalizer(fn)
    
    
    if __name__ == '__main__':
        pass
    
    

    进行测试

    无需依赖的接口

    test_stand_alone.py

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    import pytest
    import allure
    from core.request import req
    from common.ApiData import testinfo
    from core.checkresult import check_results
    
    
    @allure.feature("单个API测试")
    class TestStandAlone:
    
        @pytest.mark.parametrize('case', testinfo.stand_alone.values(), ids=testinfo.stand_alone.keys())
        def test_stand_alone_interface(self, case):
            r = req(case['method'], case['route'], case.get('extractresult'), **case['RequestData'])
            check_results(r, case)
            print(r.cookies)
    
    
    if __name__ == "__main__":
        pytest.main(['test_business.py'])
    
    
    

    无需依赖的接口在测试函数的参数中不传入"is_login"

    需要依赖的接口

    test_business.py

    #!/usr/bin/env python3
    # -*- coding:utf-8 -*-
    import pytest
    import allure
    from core.request import req
    from common.ApiData import testinfo
    from core.checkresult import check_results
    
    
    @allure.feature("业务流程API测试")
    class TestBusiness:
        @pytest.mark.parametrize('case', testinfo.business.values(), ids=testinfo.business.keys())
        def test_business_interface(self, is_login, case):
            r = req(case['method'], case['route'], case.get('extractresult'), **case['RequestData'])
            check_results(r, case)
    
    
    if __name__ == "__main__":
        pytest.main(['test_business.py'])
    
    

    需要依赖的接口在测试函数的参数中传入"is_login"参数

    校验测试结果

    checkresult.py

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    import re
    import pytest
    import allure
    from requests import Response
    
    
    def check_results(r: Response, case_info):
        """检查运行结果"""
        with allure.step("校验返回响应码"):
            allure.attach(name='预期响应码', body=str(case_info['expectcode']))
            allure.attach(name='实际响应码', body=str(r.status_code))
        pytest.assume(case_info['expectcode'] == r.status_code)
        if case_info['resultcheck']:
            with allure.step("校验响应预期值"):
                allure.attach(name='预期值', body=str(case_info['resultcheck']))
                allure.attach(name='实际值', body=r.text)
            pytest.assume(case_info['resultcheck'] in r.text)
        if case_info['regularcheck']:
            with allure.step("正则校验返回结果"):
                allure.attach(name='预期正则', body=case_info['regularcheck'])
                allure.attach(name='响应值', body=str(re.findall(case_info['regularcheck'], r.text)))
            pytest.assume(re.findall(case_info['regularcheck'], r.text))
    
    

    接口参数关联

    在接口测试中我们需要用上一个接口返回的数据,我在思考了两天之后采取了正则提取的方式来实现此功能,本来想用jinja2模板但是不太会用。

    创建正则类

    RegExp.py

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    import re
    from utils.logger import log
    from common.variable import is_vars
    
    
    class RegExp(object):
        """正则相关类"""
    
        def __init__(self):
            self.re = re
    
        def findall(self, string):
            keys = self.re.findall(r"{{(.*?)}}", string)
            return keys
    
        def subs(self, keys, string):
            result = None
            for i in keys:
                log.info("替换变量:{}".format(i))
                result = self.re.sub(r"{{%s}}" % i, is_vars.get(i), string)
            log.info("替换结果:{}".format(result))
            return result
    
        def __call__(self, exp, string):
            return self.re.findall(r'"%s":"(.*?)"' % exp, string)[0]
    
    
    regexps = RegExp()
    
    if __name__ == '__main__':
        pass
    
    

    添加全局变量池

    variable.py

    #!/usr/bin/env python3
    # -*- coding:utf-8 -*-
    
    
    class Variable(object):
        """全局变量池"""
    
        def __init__(self):
            super().__init__()
    
        def set(self, key, value):
            setattr(self, key, value)
    
        def get(self, key):
            return getattr(self, key)
    
        def has(self, key):
            return hasattr(self, key)
    
    
    is_vars = Variable()
    
    if __name__ == '__main__':
        is_vars.set('name', 'hoou')
        print(is_vars.get('name'))
    
    

    获取接口的返回值

    getresults.py

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    import pytest
    import allure
    from utils.logger import log
    from requests import Response
    from common.variable import is_vars
    from common.RegExp import regexps
    
    
    def get_result(r: Response, extract):
        """获取值"""
        for i in extract:
            value = regexps(i, r.text)
            log.info("正则提取结果值:{}={}".format(i, value))
            is_vars.set(i, value)
            pytest.assume(is_vars.has(i))
        with allure.step("提取返回结果中的值"):
            for i in extract:
                allure.attach(name="提取%s" % i, body=is_vars.get(i))
    

    配置pytest.ini

    pytest.ini

    [pytest]
    addopts = -s -q
    

    配置allure环境变量

    APIenv=TEST
    APIversion=1.0
    TestServer=https://www.zhixue.com
    Tester=hoou
    

    执行测试

    run.bat

    pytest --alluredir allure-results --clean-alluredir
    
    COPY configenvironment.properties allure-results
    
    allure generate allure-results -c -o allure-report
    
    allure open allure-report
    

    运行结果

     

    努力努力努力
  • 相关阅读:
    XML的学习
    使用WEB应用时后台发生的事
    企业级应用与互联网应用的区别
    学习JAVA EE的目标
    关于AngularJS的学习报告
    在线进销存软件
    Gridview 实现列表全选、自动选择下级item的功能
    python 计算roc程序报错ValueError: bad input shape (5313, 2)
    pandas保存为hdf格式更高效
    Linux 安装
  • 原文地址:https://www.cnblogs.com/wwtest/p/14103253.html
Copyright © 2020-2023  润新知