Python接口自動化測試框架(Pytest+Allure+jsonpath+xlrd+excel、支持Restful接口規範)

廢話

和幾個朋友聊自然後出來的產物但願能幫到你們學習接口自動化測試,歡迎你們交流指出不合適的地方,源碼在文末html

功能

  1. 實現:get/post請求(上傳文件)::理論上其餘delete/put等請求也實現了,支持restful接口規範
  2. 發送郵件
  3. 生成allure測試報告
  4. 壓縮測試報告文件
  5. 數據依賴

運行機制

  1. 經過讀取配置文件,獲取到host地址、提取token的jsonpath表達式,提取實際響應結果用來與預期結果比對的jsonpath表達式。
  2. 讀取excel用例文件數據,組成一個符合pytest參數化的用例數據,根據每列進行數據處理(token操做、數據依賴)
  3. token,寫,須要使用一個正常登陸的接口,而且接口中要返回token數據,才能夠提取,token,讀爲該請求將攜帶有token的header,token 無數據的將不攜帶token
  4. 數據依賴處理,從excel中讀取出來的格式{"用例編號":["jsonpath表達式1", "jsonpath表達式2"]},經過用例編號來獲取對應case的實際響應結果(實際響應結果在發送請求後,回寫到excel中),經過jsonpath表達式提取對應的依賴參數字段,以及對應的值,最終會返回一個存儲該接口須要依賴數據的字典如{"userid":500, "username": "zy7y"},在發送請求時與請求數據進行合併,組成一個新的data放到請求中
  5. 每次請求完成以後將回寫實際的響應結果到excel中
  6. 根據配置文件中配置的jsonpath表達式提取實際響應內容與excel中預期結果的數據對比
  7. 生成測試報告
  8. 壓縮測試報告文件夾
  9. 發送郵件

已知問題

執行接口消耗時間變長,代碼亂(語言學的不紮實),頻繁讀寫excel(可考慮用字典存每一個接口的實際響應,取值直接從響應字典中取出)
總體代碼結構優化未實現,致使最終測試時間變長,其餘工具單接口測試只須要39ms,該框架中使用了101ms,考慮和頻繁讀寫用例數據致使vue

環境與依賴

名稱 版本 做用
python 3.7.8
pytest 6.0.1 底層單元測試框架,用來實現參數化,自動執行用例
allure-pytest 2.8.17 allure與pytest的插件能夠生成allure的測試報告
jsonpath 0.82 用來進行響應斷言操做
loguru 0.54 記錄日誌
PyYAML 5.3.1 讀取yml/yaml格式的配置文件
Allure 2.13.5 要生成allure測試報告必需要在本機安裝allure並配置環境變量
xlrd 1.2.0 用來讀取excel中用例數據
yagmail 0.11.224 測試完成後發送郵件
requests 2.24.0 發送請求

目錄結構

35F236C2-2F64-4891-8384-2FBFE3229F90.png

執行順序

運行test_api.py -> 讀取config.yaml(tools.read_config.py) -> 讀取excel用例文件(tools.read_data.py) -> test_api.py實現參數化 -> 處理是否依賴數據 ->base_requests.py發送請求 -> test_api.py斷言 -> read_data.py回寫實際響應到用例文件中(方便根據依賴提取對應的數據)python

config.ymal展現

server:
  test: http://127.0.0.1:8888/api/private/v1/
  # 實例代碼使用的接口服務,已改成做者是本身的雲服務器部署。(後端源碼來自b站:https://www.bilibili.com/video/BV1EE411B7SU?p=10)
  dev: http://49.232.203.244:8888/api/private/v1/

# 實際響應jsonpath提取規則設置
response_reg:
  # 提取token的jsonpath表達式
  token: $.data.token
  # 提取實際響應的斷言數據jsonpath表達式,與excel中預期結果的數據進行比對用
  response: $.meta

file_path:
  case_data: ../data/case_data.xlsx
  report_data: ../report/data/
  report_generate: ../report/html/
  report_zip: ../report/html/apiAutoTestReport.zip
  log_path: ../log/運行日誌{time}.log

email:
  # 發件人郵箱
  user:  123456.com
  # 發件人郵箱受權碼
  password:  ASGCSFSGS
  # 郵箱host
  host:  smtp.163.com
  contents:  解壓apiAutoReport.zip(接口測試報告)後,請使用已安裝Live Server 插件的VsCode,打開解壓目錄下的index.html查看報告
  # 收件人郵箱
  addressees:  ["收件人郵箱1","收件人郵箱2","收件人郵箱3"]
  title:  接口自動化測試報告(見附件)
  # 附件地址
  enclosures: ["../report/html/apiAutoTestReport.zip",]

EXcel用例展現

Snipaste_2020-08-13_11-21-18.png
用例格式說明

腳本一覽

請求方法封裝

#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: apiAutoTest
@author: zy7y
@file: base_requests.py
@ide: PyCharm
@time: 2020/7/31
"""
from test import logger
import requests


class BaseRequest(object):
    def __init__(self):
        pass

    # 請求
    def base_requests(self, method, url, parametric_key=None, data=None, file_var=None, file_path=None, header=None):
        """

        :param method: 請求方法
        :param url: 請求url
        :param parametric_key: 入參關鍵字, get/delete/head/options/請求使用params,
         post/put/patch請求可以使用json(application/json)/data

        :param data: 參數數據,默認等於None
        :param file_var: 接口中接受文件的參數關鍵字
        :param file_path: 文件對象的地址, 單個文件直接放地址:/Users/zy7y/Desktop/vue.js
        多個文件格式:["/Users/zy7y/Desktop/vue.js","/Users/zy7y/Desktop/jenkins.war"]
        :param header: 請求頭
        :return: 返回json格式的響應
        """
        session = requests.Session()
        if (file_var in [None, '']) and (file_path in [None, '']):
            files = None
        else:
            # 文件不爲空的操做
            if file_path.startswith('[') and file_path.endswith(']'):
                file_path_list = eval(file_path)
                files = []
                # 多文件上傳
                for file_path in file_path_list:
                    files.append((file_var, (open(file_path, 'rb'))))
            else:
                # 單文件上傳
                files = {file_var: open(file_path, 'rb')}

        if parametric_key == 'params':
            res = session.request(method=method, url=url, params=data, headers=header)
        elif parametric_key == 'data':
            res = session.request(method=method, url=url, data=data, files=files, headers=header)
        elif parametric_key == 'json':
            res = session.request(method=method, url=url, json=data, files=files, headers=header)
        else:
            raise ValueError('可選關鍵字爲:get/delete/head/options/請求使用params, post/put/patch請求可以使用json(application/json)/data')
        logger.info(f'請求方法:{method},請求路徑:{url}, 請求參數:{data}, 請求文件:{files}, 請求頭:{header})')
        return res.json()

讀取excel用例數據

#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: apiAutoTest
@author: zy7y
@file: read_data.py
@ide: PyCharm
@time: 2020/7/31
"""
import xlrd
from test import logger


class ReadData(object):
    def __init__(self, excel_path):
        self.excel_file = excel_path
        self.book = xlrd.open_workbook(self.excel_file)

    def get_data(self):
        """

        :return: data_list - pytest參數化可用的數據
        """
        data_list = []
        table = self.book.sheet_by_index(0)
        for norw in range(1, table.nrows):
            # 每行第4列 是否運行
            if table.cell_value(norw, 3) == '否':
                continue
            value = table.row_values(norw)
            value.pop(3)
            # 配合將每一行轉換成元組存儲,迎合 pytest的參數化操做,如不須要能夠註釋掉 value = tuple(value)
            value = tuple(value)
            logger.info(f'{value}')
            data_list.append(value)
        return data_list

存儲接口實際結果響應

#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: apiAutoTest的副本
@author: zy7y
@file: save_response.py
@ide: PyCharm
@time: 2020/8/8
"""
import json

import jsonpath
from test import logger


class SaveResponse(object):
    def __init__(self):
        self.actual_response = {}

    # 保存實際響應
    def save_actual_response(self, case_key, case_response):
        """

        :param case_key:用例編號
        :param case_response:對應用例編號的實際響應
        :return:
        """
        self.actual_response[case_key] = case_response
        logger.info(f'當前字典數據{self.actual_response}')

    # 讀取依賴數據
    def read_depend_data(self, depend):
        """

        :param depend: 須要依賴數據字典{"case_001":"['jsonpaht表達式1', 'jsonpaht表達式2']"}
        :return:
        """
        depend_dict = {}
        depend = json.loads(depend)
        for k, v in depend.items():
            # 取得依賴中對應case編號的值提取表達式
            try:
                for value in v:
                    # value : '$.data.id'
                    # 取得對應用例編號的實際響應結果
                    actual = self.actual_response[k]
                    # 返回依賴數據的key
                    d_k = value.split('.')[-1]
                    # 添加到依賴數據字典並返回
                    depend_dict[d_k] = jsonpath.jsonpath(actual, value)[0]
            except TypeError as e:
                logger.error(f'實際響應結果中沒法正常使用該表達式提取到任何內容,發現異常{e}')

        return depend_dict

處理依賴數據邏輯

#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: apiAutoTest
@author: zy7y
@file: data_tearing.py
@ide: PyCharm
@time: 2020/8/10
"""
import json
from json import JSONDecodeError

import jsonpath
from test import logger


class TreatingData(object):
    """
    處理hader/path路徑參數/請求data依賴數據代碼
    """

    def __init__(self):
        self.no_token_header = {}
        self.token_header = {}

    def treating_data(self, is_token, parameters, dependent, data, save_response_dict):
        # 使用那個header
        if is_token == '':
            header = self.no_token_header
        else:
            header = self.token_header
        logger.info(f'處理依賴前data的數據:{data}')
        # 處理依賴數據data
        if dependent != '':
            dependent_data = save_response_dict.read_depend_data(dependent)
            logger.debug(f'依賴數據解析得到的字典{dependent_data}')
            if data != '':
                # 合併組成一個新的data
                dependent_data.update(json.loads(data))
                data = dependent_data
                logger.info(f'data有數據,依賴有數據時 {data}')
            else:
                # 賦值給data
                data = dependent_data
                logger.info(f'data無數據,依賴有數據時 {data}')
        else:
            if data == '':
                data = None
                logger.info(f'data無數據,依賴無數據時 {data}')
            else:
                data = json.loads(data)
                logger.info(f'data有數據,依賴無數據 {data}')

        # 處理路徑參數Path的依賴
        # 傳進來的參數相似 {"case_002":"$.data.id"}/item/{"case_002":"$.meta.status"},進行列表拆分
        path_list = parameters.split('/')
        # 獲取列表長度迭代
        for i in range(len(path_list)):
            # 按着
            try:
                # 嘗試序列化成dict:   json.loads('2') 能夠轉換成2
                path_dict = json.loads(path_list[i])
            except JSONDecodeError as e:
                # 序列化失敗此path_list[i]的值不變化
                logger.error(f'沒法轉換字典,進入下一個檢查,本輪值不發生變化:{path_list[i]},{e}')
                # 跳過進入下次循環
                continue
            else:
                # 解析該字典,得到用例編號,表達式
                logger.info(f'{path_dict}')
                # 處理json.loads('數字')正常序列化致使的AttributeError
                try:
                    for k, v in path_dict.items():
                        try:
                            # 嘗試從對應的case實際響應提取某個字段內容
                            path_list[i] = jsonpath.jsonpath(save_response_dict.actual_response[k], v)[0]
                        except TypeError as e:
                            logger.error(f'沒法提取,請檢查響應字典中是否支持該表達式,{e}')
                except AttributeError as e:
                    logger.error(f'類型錯誤:{type(path_list[i])},本此將不轉換值 {path_list[i]},{e}')
        # 字典中存在有不是str的元素:使用map 轉換成全字符串的列表
        path_list = map(str, path_list)

        # 將字符串列表轉換成字符:500/item/200
        parameters_path_url = "/".join(path_list)
        logger.info(f'path路徑參數解析依賴後的路徑爲{parameters_path_url}')
        return data, header, parameters_path_url

啓動文件

#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: apiAutoTest
@author: zy7y
@file: test_api.py
@ide: PyCharm
@time: 2020/7/31
"""
import json
import jsonpath
from test import logger
import pytest
import allure
from api.base_requests import BaseRequest
from tools.data_tearing import TreatingData
from tools.read_config import ReadConfig
from tools.read_data import ReadData
from tools.save_response import SaveResponse

# 讀取配置文件 對象
rc = ReadConfig()
base_url = rc.read_serve_config('dev')
token_reg, res_reg = rc.read_response_reg()
case_data_path = rc.read_file_path('case_data')
report_data = rc.read_file_path('report_data')
report_generate = rc.read_file_path('report_generate')
log_path = rc.read_file_path('log_path')
report_zip = rc.read_file_path('report_zip')
email_setting = rc.read_email_setting()
# 實例化存響應的對象
save_response_dict = SaveResponse()
# 讀取excel數據對象
data_list = ReadData(case_data_path).get_data()
# 數據處理對象
treat_data = TreatingData()
# 請求對象
br = BaseRequest()
logger.info(f'配置文件/excel數據/對象實例化,等前置條件處理完畢\n\n')


class TestApiAuto(object):
    # 啓動方法
    def run_test(self):
        import os, shutil
        if os.path.exists('../report') and os.path.exists('../log'):
            shutil.rmtree(path='../report')
            shutil.rmtree(path='../log')
        # 日誌存取路徑
        logger.add(log_path, encoding='utf-8')
        pytest.main(args=[f'--alluredir={report_data}'])
        os.system(f'allure generate {report_data} -o {report_generate} --clean')
        logger.warning('報告已生成')

    @pytest.mark.parametrize('case_number,case_title,path,is_token,method,parametric_key,file_var,'
                             'file_path, parameters, dependent,data,expect', data_list)
    def test_main(self, case_number, case_title, path, is_token, method, parametric_key, file_var,
                  file_path, parameters, dependent, data, expect):
        """
        :param case_number: 用例編號
        :param case_title: 用例標題
        :param path: 接口路徑
        :param is_token: token操做:寫入token/讀取token/不攜帶token
        :param method: 請求方式:get/post/put/delete....
        :param parametric_key: 入參關鍵字:params/data/json
        :param file_var: 接口中接受文件對象的參數名稱
        :param file_path: 文件路徑,單文件實例:/Users/zy7y/PycharmProjects/apiAutoTest/test/__init__.py
        多文件實例['/Users/zy7y/PycharmProjects/apiAutoTest/test/__init__.py','/Users/zy7y/PycharmProjects/apiAutoTest/test/test_api.py']

        :param parameters: path參數(攜帶在url中的參數)依賴處理 users/:id(id攜帶在url中) 實例:{"case_001": '$.data.id'},解析
        從用例編號爲case_001的實際結果響應中提取data字典裏面的id的內容(假設提取出來是500), 最後請求的路徑將是host + users/500

        :param dependent: data數據依賴,該接口須要上一個接口返回的響應中的某個字段及內容:實例{"case_001",["$.data.id","$.data.username"]}
        解析: 從用例case_001的實際響應結果中提取到data下面的id,與username的值(假設id值爲500,username爲admin),那麼提取的數據依賴內容將是{"id":500, "username":"admin"}
        納悶最終請求的data 將是 {"id":500, "username":"admin"} 與自己的data合併後的內容
        :param data: 請求數據
        :param expect:預期結果,最後與config/config.yaml下的response_reg->response提取出來的實際響應內容作對比,實現斷言
        :return:
        """

        # 感謝:https://www.cnblogs.com/yoyoketang/p/13386145.html,提供動態添加標題的實例代碼
        # 動態添加標題
        allure.dynamic.title(case_title)

        logger.debug(f'⬇️⬇️⬇️...執行用例編號:{case_number}...⬇️⬇️⬇️️')
        with allure.step("處理相關數據依賴,header"):
            data, header, parameters_path_url = treat_data.treating_data(is_token, parameters, dependent, data, save_response_dict)

        with allure.step("發送請求,取得響應結果的json串"):
            res = br.base_requests(method=method, url=base_url + path + parameters_path_url, parametric_key=parametric_key, file_var=file_var, file_path=file_path,
                                   data=data, header=header)

        with allure.step("將響應結果的內容寫入實際響應字典中"):
            save_response_dict.save_actual_response(case_key=case_number, case_response=res)
            # 寫token的接口必須是要正確無誤能返回token的
            if is_token == '寫':
                with allure.step("從登陸後的響應中提取token到header中"):
                    treat_data.token_header['Authorization'] = jsonpath.jsonpath(res, token_reg)[0]
        with allure.step("根據配置文件的提取響應規則提取實際數據"):
            really = jsonpath.jsonpath(res, res_reg)[0]
        with allure.step("處理讀取出來的預期結果響應"):
            expect = json.loads(expect)
        with allure.step("預期結果與實際響應進行斷言操做"):
            assert really == expect
            logger.info(f'完整的json響應: {res}\n須要校驗的數據字典: {really} 預期校驗的數據字典: {expect} \n測試結果: {really == expect}')
            logger.debug(f'⬆⬆⬆...用例編號:{case_number},執行完畢,日誌查看...⬆⬆⬆\n\n️')


if __name__ == '__main__':
    TestApiAuto().run_test()

    # 使用jenkins集成將不會使用到這兩個方法(郵件發送/報告壓縮zip)
    # from tools.zip_file import zipDir
    # from tools.send_email import send_email
    # zipDir(report_generate, report_zip)
    # send_email(email_setting)

運行測試

首先確保須要的環境與依賴包無問題以後,使用Pycharm打開項目,找到settings修改成unitest或者其餘非pytest,具體操做以下
B21lr9.md.jpg
B21GUx.pnggit

運行結果

Snipaste_2020-08-03_15-54-45.png

致謝

jsonpath語法學習:https://blog.csdn.net/liuchunming033/article/details/106272542
zip文件壓縮:http://www.javashuo.com/article/p-uumfypnx-dz.html
歡迎交流。github

源碼地址

源碼地址Gitee - version1.0分支: https://gitee.com/zy7y/apiAutoTest/tree/version1.0/
源碼地址GitHub- version1.0 分支:https://github.com/zy7y/apiAutoTest/tree/version1.0/json

更新

2020/11/23 - 優化數據參數、路徑參數依賴處理方式,現版本與以前同等環境下,測試時間提高2S
介紹:http://www.javashuo.com/article/p-ywzmuiuw-nv.html後端

相關文章
相關標籤/搜索