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

 


作者:李 鋒|編輯排版:梁莉莉


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM