python:接口自动化项目测试框架

最近文章一直都是python的第三方库使用及爬虫的知识,针对自动化测试的优化版本也没有及时发布出来,今天主要抽时间整理了一下,罗列了运行流程及项目工程目录。

所提供的框架仅供参考,中间还有很多不足之处,也希望大家踊跃提出疑义和建议。

下面进入代码的世界……

工程目录

apiTest
├─apiInterface
├─cases
├─common
├─config
├─dynamicData
├─logs
├─reports
│  ├─allure
│  └─html
├─runMain
├─testDatas
├─requirements.txt
└─settings.py

  • apiTest:根目录
  • apiInterface:是存放一些url路径,这里只是返回路径,未做多余操作
  • cases:测试用例存放文件夹
  • common:存放一些公共调用的类
  • config:配置文件存放目录
  • dynamicData:动态参数存放处,就是指一些接口的上下游需要的参数
  • logs:存放程序运行的日志文件
  • reports:存放程序运行完成,生成的测试报告,分为allure和html报告
  • runMain:存放程序运行入口的目录
  • testDatas:测试数据目录,主要是yaml文件
  • requirements.txt:程序需要的依赖包
  • settings.py:配置文件,路径,环境切换及各类配置的存放,类似django的setting文件

框架的运行流程

主要是运用了python、request、pytest、yaml、Jinja2、allure组成的测试框架.

其运行流程就是执行测试用例时,会先拿接口路径,其次再读取yaml文件,然后yaml文件替换需要替换的动态数据,再然后就是接口拿到数据取利用python++pytest+requests库去请求,然后根据返回值进行断言及生成allure报告。

 

 

实际代码介绍

  • apiTest/apiInterface/apiMatch.py
# -*-coding:utf-8 -*-
# ** createDate: 2021/11/16 11:37
# ** scriptFile: apiMatch.py
# ** __author__: Li Feng
"""
注释信息:
"""

__all__ = ["api_match"]


class _ApiMatch:

    @property
    def match_european_cup(self):
        """

        2021欧洲杯赛程
        查询2021欧洲杯赛程详细信息

        :return:
        """
        return "/fapig/euro2020/schedule"


api_match = _ApiMatch()

  • apiTest/cases/test_european_cup.py
# -*- encoding: utf-8 -*-
"""
@__Author__: lifeng
@__Software__: PyCharm
@__File__: test_european_cup.py
@__Date__: 2021/6/13 19:00
"""

import pytest
from common.readRenderYaml import render
from apiInterface.apiMatch import api_match
from dynamicData.matchDynamic import contents


class TestNews:
    # 读取yaml文件,并执行数据替换(contents=contents就是接收需要替换的参数)
    data = render("api_match", "test_european_cup", contents=contents)

    @pytest.mark.parametrize('test_data, title, results', data["test_data"])
    def test_european_cup(self, test_data, title, results, auth):
        """
        2021欧洲杯赛程
        :param test_data:   测试数据
        :param title:       传参名称
        :param results:     预期结果
        :param auth:        登录后返回一个请求对象
        :return:
        """
        response = auth.send_get(api_match.match_european_cup, test_data)

        assert response["reason"] == results["reason"]
        assert type(response["result"]["data"]) == type(results["result"]["data"])


if __name__ == '__main__':
    pytest.main(["-v", "-s", "test_european_cup.py"])

  • apiTest/common/readRenderYaml.py

cases目录中的test_european_cup.py文件中调用的render函数就是下面的这个类提供的。

它的只要功能就是读取yaml文件然后执行Jinja2库进行动态参数替换。

# -*-coding:utf-8 -*-
# ** createDate: 2021/11/16 8:18
# ** scriptFile: readRenderYaml.py
# ** __author__: Li Feng
"""
注释信息:
"""

import json
import yaml
import jinja2
from common.mapMnvironment import MapEnvironment


__all__ = ["render"]


class _ReadYamlRender:

    def __init__(self, yaml_path_name: str, yaml_tier: str, contents: dict = None):

        self._content = contents

        """
        读取yaml文件的数据, 返回正经json数据
        """
        with open(yaml_path_name, encoding="utf-8") as y:
            data = yaml.safe_load(y)

        # json.dumps要把字符串数据转成正经json数据,用于return返回时不报错
        self._template_name = json.dumps(data[yaml_tier])

    @property
    def render(self):
        """
        利用jinja2进行动态数据渲染替换,返回字典类型
        :return:
        """
        if self._content is not None:
            jinja2_data = jinja2.Template(self._template_name).render(self._content)
            return json.loads(jinja2_data)
        else:
            return json.loads(self._template_name)


def render(path_key: str, yaml_tier: str, contents: dict = None):
    """
    执行读取yaml文件并渲染返回数据
    :param path_key:
    :param yaml_tier:
    :param content:
    :return:
    """

    # 获取所有yaml文件路径
    data = MapEnvironment().yaml_path

    # 渲染yaml文件
    return _ReadYamlRender(data[path_key], yaml_tier, contents).render

  • apiTest/common/sendRequest.py

cases目录中的test_european_cup.py文件中调用的auth.send_get方法就是下面的这个请求类提供的。

它的只要功能就是进行接口的请求,可能你会疑问为什么是auth。seng_get,那是因为我这里用了pytest框架提供的测试夹具功能(这个后面会单独说pytest框架,在本篇文章了解下即可)。

import json
import urllib3
import requests
from functools import wraps
from requests import exceptions
from requests_toolbelt import MultipartEncoder
from common.logLogging import do_logger
from common.mapMnvironment import MapEnvironment


__all__ = ["send"]


def _handle_response(func):
    """
    处理请求后的返回值
    :param func: 传入函数
    :return:
    """

    @wraps(func)
    def wraps_response(*args, **kwargs):
        results = func(*args, **kwargs)
        request_body = results.request.body
        request_url = results.request.url

        try:
            if results.ok:
                return results.json()
        except json.JSONDecodeError:
            return results.text.encode("utf-8")
        except Exception as _error:
            do_logger.error(f"接口请求出错:"
                            f"请求url:{request_url},"
                            f"请求参数:{request_body},"
                            f"返回数据:{results.text}")
            raise exceptions.RequestException from _error

    return wraps_response


def _print_url(r, *args, **kwargs):
    """
    回调函数,r接受一个数据块作为它的第一个参数
    :param r:
    :param args:
    :param kwargs:
    :return:
    """
    print(f"请求url:{r.request.url}")
    print(f"请求参数:{r.request.body}")
    # print(f"请求数据:{r.request.prepare()}")
    print(f"返回数据:{r.text}")


class _SendRequest:
    _map = MapEnvironment()

    def __init__(self):
        urllib3.disable_warnings()
        self.s = requests.Session()
        self.s.verify = False
        self.headers = self.s.headers
        self.headers.update(MapEnvironment().headers)
        urllib3.disable_warnings(urllib3.exceptions.InsecurePlatformWarning)

    @classmethod
    def _get_url(cls, url):
        """
        拼接url,增加platform参数
        :param url:
        :return:
        """

        return cls._map.base_url(cls._map.host) + url

    def send_upload(self, url, filename, filetype='application/vnd.ms-excel'):
        """
        上传文件请求
        :param url:
        :param filename: 文件名称
        :param filetype: 文件类型
        :return:
        """
        try:
            url = self._get_url(url)
            with self.s as interface:
                from pathlib import Path
                m = MultipartEncoder(
                    fields={'file': (
                        filename, open(Path().parent.joinpath("upload"), 'rb'), filetype)})
                self.s.headers.update({"Content-Type": m.content_type})
                response = interface.post(url=url, data=m, hooks=dict(response=_print_url))
            results = json.loads(json.dumps(response.text))
        except Exception as e:
            do_logger.error(e)
            raise (ImportError, FileNotFoundError, PermissionError) from e
        else:
            return results

    def send_download(self, url, filename, params=None, **kwargs):
        """
        下载文件请求
        :param url:
        :param filename: 文件的名称加后缀名(例:name.xlsx)
        :param params:
        :param kwargs:
        :return:
        """
        url = self._get_url(url)
        with self.s as interface:
            response = interface.get(url=url, params=params,
                                     hooks=dict(response=_print_url), **kwargs)
        try:
            from pathlib import Path, PurePath
            if response.ok:
                with open(PurePath(Path(__file__).parent).parent.joinpath("download", filename), 'wb') as save:
                    for chunk in response.iter_content():
                        save.write(chunk)
        except Exception as e:
            do_logger.error(e)
            raise (ImportError, FileNotFoundError, PermissionError) from e
        else:
            return True

    @_handle_response
    def send_get(self, url, params=None, **kwargs):
        """
        get请求
        :param url:
        :param params:
        :param kwargs: 动态参数
        :return: 返回状态码
        """
        url = self._get_url(url)
        with self.s as interface:
            response = interface.get(url, params=params,
                                     hooks=dict(response=_print_url), **kwargs)
        return response

    @_handle_response
    def send_post(self, url, json=None, data=None, query=None, **kwargs):
        """
        post请求
        :param url:
        :param json:
        :param data:
        :param query:   接收url跟随的参数
        :param kwargs:  动态参数
        :return: 返回状态码
        """
        url = self._get_url(url)
        with self.s as interface:
            response = interface.post(url=url, data=data,
                                      json=json, params=query,
                                      hooks=dict(response=_print_url), **kwargs)
        return response

    @_handle_response
    def send_put(self, url, json=None, data=None, query=None, **kwargs):
        """
        put请求
        :param url:
        :param json:
        :param data:
        :param query:   接收url跟随的参数
        :param kwargs:  动态参数
        :return: 返回状态码
        """
        url = self._get_url(url)
        with self.s as interface:
            response = interface.put(url=url, data=data,
                                     json=json, params=query,
                                     hooks=dict(response=_print_url), **kwargs)
        return response

    @_handle_response
    def send_delete(self, url, **kwargs):
        """
        delete请求
        :param url:
        :param kwargs:  动态参数
        :return: 返回状态码
        """
        url = self._get_url(url)
        with self.s as interface:
            response = interface.delete(url=url,
                                        hooks=dict(response=_print_url), **kwargs)
        return response



# 创建对象
send = _SendRequest()

  • apiTest/dynamicData/matchDynamic.py

cases目录中的test_european_cup.py文件中可以看到render(xx, xx, contents=contents),它就是把需要替换的动态参数传给Jinja2去执行替换操作。

# -*-coding:utf-8 -*-
# ** createDate: 2021/11/16 11:40
# ** scriptFile: matchDynamic.py
# ** __author__: Li Feng
"""
注释信息:
"""
__document__ = """

存放一些动态数据,用于yaml文件中的数据替换操作

"""
contents = {
    "key": "9d0dfd9dbaf51de283ee8a88e58e332b"
}

  • apiTest/logs
    存放程序运行时,出现错误的日志。

 

 


  • apiTest/reports
    存放allure和html报告目录,allure生成的是json文件,所有尽量再建一个子文件夹。

 

 


  • apiTest/runMain/main.py

这里就是执行pytest,运行全部用例,然后生成allure报告和html报告,并存放在reports目录中。

# -*-coding:utf-8 -*-
# ** createDate: 2021/11/16 11:31
# ** scriptFile: main.py
# ** __author__: Li Feng
"""
注释信息:
"""

import os
import sys
import pytest

# 执行路径插入操作,增强代码的可移植性
sys.path.insert(0, os.path.dirname(os.path.dirname(os.getcwd())))

print(sys.path)


def main():

    # 入口函数,运行全部用例,生成html和allure报告
    pytest.main(['../cases/', '--html=../reports/report.html',
                '--alluredir=../reports/allure/allure-report'])


if __name__ == '__main__':
    main()

  • apiTest/testDatas
    存放yaml文件,yaml文件中主要是放一些测试数据,针对动态的测试数据,要根据Jinja2的语法来使用:
test_european_cup:
  test_data:
      # 接口参数
    - - type: 1
        key: "{{key}}"
      # 接口传参名称
      - name: type字段传1
      # 实际结果,用于断言操作
      - reason: "查询成功!"
        result:
          data:
            -
    - - type: 2
        key: "{{key}}"
      # 接口传参名称
      - name: type字段传2
      # 实际结果,用于断言操作
      - reason: "查询成功!"
        result:
          data:
            -
    - - type: 3
        key: "{{key}}"
      # 接口传参名称
      - name: type字段传3
      # 实际结果,用于断言操作
      - reason: "查询成功!"
        result:
          data:
            -

{{key}}这里的意思就是取keyvalue值,而value值就是apiTest/dynamicData/matchDynamic.py文件中提供的:

# -*-coding:utf-8 -*-
# ** createDate: 2021/11/16 11:40
# ** scriptFile: matchDynamic.py
# ** __author__: Li Feng
"""
注释信息:
"""
__document__ = """

存放一些动态数据,用于yaml文件中的数据替换操作

"""
contents = {
    "key": "9d0dfd9dbaf51de283ee8a88e58e218b"
}

  • apiTest/requirements.txt

就是你用的一些依赖包

allure-pytest
allure-python-commons
Appium-Python-Client
beautifulsoup4
jmespath
jsonpath
jsonschema
mysqlclient==1.4.6
openpyxl
pyaml
PyMySQL
pytest
pytest-base-url
pytest-cov
pytest-cover
pytest-emoji
pytest-html
pytest-metadata
pytest-rerunfailures
pytest-xdist
python-jenkins
PyYAML
redis
requests
requests-toolbelt
selenium
pytest-pikachu
pytest-clarity

  • apiTest/settings.py
    它主要就是一个配置文件,配置账号密码,日志,环境变量等:
# -*-coding:utf-8 -*-
# ** createDate: 2021/11/16 8:12
# ** scriptFile: setting.py
# ** __author__: Li Feng
"""
注释信息:
"""

from pathlib import Path, PurePath

# 获取项目根目录
BASE_DIR = PurePath(Path(__file__).parent)
print(BASE_DIR)

# 用于判断是否往企业微信发送测试报告:True是发送、False是不发送
IS_SEND = True

# 设置运行的环境变量
ENVIRONMENT = "PRO"  # 环境变量值分别为  测试:TEST;预发布:PRE;生产:PRO

# 接口请求域名
HOST = "http://apis.juhe.cn"
# 设置头信息指定域名和Content-Type类型
HEADERS = {'Content-Type': 'application/json'}

# 环境IP配置
BASE_HOST = {
    "test": None,
    "pre": None,
    "pro": None,
}

# 数据库配置
DATABASES = {
    "pro": {"host": "8.136.250.157", "port": 1234, "user": "root", "passwd": "test.2016", "db": "testing"},
}

# yaml文件路径
YAML_FILE_PATH = {
    "api_idiom": BASE_DIR.joinpath("testDatas", "idiom_modules.yml"),
    "api_match": BASE_DIR.joinpath("testDatas", "match_modules.yml"),
}

# 日志存放目录
LOGGING_PATH = BASE_DIR.joinpath("logs", f"logfile.text")

# 日志记录配置
LOGGING_CONFIG = {
    "version": 1,
    "root": {
        "level": "DEBUG",
        "handlers": ["file", "console"]
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "level": "ERROR",
            "formatter": "console_formatters"
        },
        "file": {
            "class": "logging.handlers.RotatingFileHandler",
            "formatter": "file_formatters",
            "filename": LOGGING_PATH,
            "level": "DEBUG",
            "maxBytes": 100,
            "backupCount": 5,
            "encoding": "utf-8"
        }
    },
    "formatters": {
        "console_formatters": {
            "format": "%(asctime)s [%(name)s:%(lineno)d] [%(module)s:%(funcName)s] [%(levelname)s]- %(message)s",
            "datefmt": "%Y%m%d %H:%M:%S"
        },
        "file_formatters": {
            'format': "%(asctime)s [%(name)s:%(lineno)d] [%(module)s:%(funcName)s] [%(levelname)s]- %(message)s- %(pathname)s",
            "datefmt": "%Y%m%d %H:%M:%S"
        }
    }
}

从配置文件中可以清晰看到日志的配置、环境的配置、还有发送邮件的配置等,这些都需要公共方法调用的,后续会给补充上来,以上就是近期对项目框架的一些优化,后续会把pytest框架的使用及对应的三方库整理出来,allure报告的使用也会整理出来,会做成一套接口自动化教程。

可能中间还有很多不好之处,也会慢慢改善进行优化,一个阶段的努力也是一个阶段的提升,总结这个阶段的结果,是我们追求的星辰大海,哪怕它很慢。


以上总结或许能帮助到你,或许帮助不到你,但还是希望能帮助到你,如有疑问、歧义,直接私信留言会及时修正发布;非常期待你的点赞和分享哟,谢谢!

未完,待续…

一直都在努力,希望您也是!

微信搜索公众号:就用python

 


作者:李 锋|编辑排版:梁莉莉

posted @ 2021-11-17 21:06  一名小测试  阅读(1020)  评论(0编辑  收藏  举报