apiAutoTest:支持自定義函數,用例中可調用

0. 前言

apiAutoTest從去年8月以來開源至今,也更新了不少內容,一起來看看吧

  • 第一個版本

    - 2020/08/08 增加實際響應存儲數據的方法,並在字典可以處理依賴見tools/svae_response.py
    
    - 2020/08/09 實現多文件上傳,接口中Path參數依賴處理
    初步實現用迭代的方式來處理接口中的數據依賴關係
    
  • 第二個版本

    - 2020/11/18 使用re庫替換之前的字典迭代方式來處理數據依賴
    - 2020/11/21 config.yaml文件中新增基準header設置
    - 2020/11/22 支持預期結果中字段斷言時使用語法糖來實現動態字段斷言
    - 2020/12/08 優化斷言信息,增加數據庫(支持mysql)查詢操作, 使用`@pytest.fixture(scope="session")`來託管數據庫對象,用例新增sql欄
    - 2020/12/16 使用conftest.py 初始化用例, 增加失敗重跑機制, 增加運行文件run,優化test_api.py冗餘代碼
    
  • 第三個版本

    - 2021/01/19 添加數據清洗功能(測試開始前進行數據庫備份-分別在服務器和本地進行,測試結束後將備份用以恢複數據-將嘗試從服務器和本地恢復到服務器數據庫中,docker部署的mysql服務已本地調試通過,直接linux部署的mysql並未測試)
    - 2021/02/27 添加hooks.py文件(可在此處自定義方法,並用於用例當中,注意請務必在定義的方法中使用return),移除上次更新的eval語法糖,增加用例處理前的日誌
    

一度說不會再更新維護代碼,結果還是慢慢的更新了…

1. 自定義函數實現的故事

這是今天更新的,主要需求來自一個apiAutoTest的學習者反饋,這裡感謝他,在此之前另一個小夥伴說他需要用上個接口返回的id字段進行運算, 很多測試框架都有這個功能,但我給apiAutoTest的定位是個工具,也就造個輪子嘛

2. 用例中如何使用自定義函數

2.1 在tools/hooks.py中定義好函數

def get_current_highest():
    """獲取當前時間戳"""
    return int(time.time())


def sum_data(a, b):
    """計算函數"""
    return a + b

2.2 在用例中如何使用該函數

語法糖: @函數名()@: 使用無參數函數

@函數名(參數1, 參數2)@: 向函數傳遞參數

ps: 函數參數兼容apiAutoTest中的提取依賴語法&此處為jsonpath語法&

用例(怕截圖不清所以就這裡簡易模擬了兩條)

用例編號 用例標題 接口路徑 是否執行 token操作 請求方式 入參關鍵字 上傳文件 請求數據 後置sql 預期結果
case_001 post請求實現登錄 login post data {“username”: “admin”, “password”: “123456”} select * from user where id=&$.case_002.data.id&; {“$.meta”:{ “msg”: “登錄成功”, “status”: 200 }}
case_002 調試函數sum_data(),從path需要運算 users/@sum_data(&$.case_001.data.id&, 2)@/ put data {“username”: “tery”,”password”: @sum_data(&$.case_001.data.id&, 66)@, “timer”: @get_current_highest()@, “timer_str”: ” @get_current_highest()@”} {“$.meta”:{“msg”: “設置狀態成功”, “status”: 200}}

運行日誌(運行上述用例得到日誌如下)

   
2021-02-27 16:06:50.538 | DEBUG    | api.base_requests:send_request:34 - 用例進行處理前數據: 
 接口路徑: login 
 請求參數: {"username": "admin", "password": "123456"} 
 後置sql: select * from user where id=&$.case_002.data.id&; 
 預期結果: {"$.meta":{ "msg": "登錄成功", "status": 200 }}
2021-02-27 16:06:50.765 | INFO     | api.base_requests:send_api:81 - 
最終請求地址://www.ysqorz.top:8888/api/private/v1/login
請求方法:post
請求頭:{'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'zh-CN,zh;q=0.9', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'}
請求參數:{'username': 'admin', 'password': '123456'}
上傳文件:None
響應數據:{'data': {'id': 500, 'rid': 0, 'username': 'admin', 'mobile': '12345678', 'email': '[email protected]', 'token': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjUwMCwicmlkIjowLCJpYXQiOjE2MTQ0MTMyMTAsImV4cCI6MTYxNDQ5OTYxMH0.cZTYLARKNj8SKlPGPdIUh9RmyQaYAJnJrLObaKiNiU4'}, 'meta': {'msg': '登錄成功', 'status': 200}}
2021-02-27 16:06:50.773 | INFO     | tools.data_process:save_response:27 - 添加key: case_001, 對應value: {'data': {'id': 500, 'rid': 0, 'username': 'admin', 'mobile': '12345678', 'email': '[email protected]', 'token': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjUwMCwicmlkIjowLCJpYXQiOjE2MTQ0MTMyMTAsImV4cCI6MTYxNDQ5OTYxMH0.cZTYLARKNj8SKlPGPdIUh9RmyQaYAJnJrLObaKiNiU4'}, 'meta': {'msg': '登錄成功', 'status': 200}}
2021-02-27 16:06:50.775 | INFO     | tools.data_process:assert_result:115 - 第1個斷言,實際結果:{'msg': '登錄成功', 'status': 200} | 預期結果:{'msg': '登錄成功', 'status': 200} 
斷言結果 True
2021-02-27 16:06:50.775 | DEBUG    | api.base_requests:send_request:34 - 用例進行處理前數據: 
 接口路徑: users/@sum_data(&$.case_001.data.id&, 2)@/ 
 請求參數: {"username": "tery","password": @sum_data(&$.case_001.data.id&, 66)@, "timer":  @get_current_highest()@, "timer_str": " @get_current_highest()@"} 
 後置sql:  
 預期結果: {"$.meta":{"msg": "設置狀態成功", "status": 200}}
2021-02-27 16:06:50.775 | DEBUG    | tools:rep_expr:45 - &$.case_001.data.id& 替換的值為 500 
2021-02-27 16:06:50.775 | DEBUG    | tools:rep_expr:50 - 執行hooks函數sum_data(500, 2) 替換的值為 502
2021-02-27 16:06:50.775 | DEBUG    | tools:rep_expr:45 - &$.case_001.data.id& 替換的值為 500 
2021-02-27 16:06:50.783 | DEBUG    | tools:rep_expr:50 - 執行hooks函數sum_data(500, 66) 替換的值為 566
2021-02-27 16:06:50.783 | DEBUG    | tools:rep_expr:50 - 執行hooks函數get_current_highest() 替換的值為 1614413210
2021-02-27 16:06:50.783 | DEBUG    | tools:rep_expr:50 - 執行hooks函數get_current_highest() 替換的值為 1614413210
2021-02-27 16:06:50.835 | INFO     | api.base_requests:send_api:81 - 
最終請求地址://www.ysqorz.top:8888/api/private/v1/users/502/
請求方法:put
請求頭:{'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'zh-CN,zh;q=0.9', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36', 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjUwMCwicmlkIjowLCJpYXQiOjE2MTQ0MTMyMTAsImV4cCI6MTYxNDQ5OTYxMH0.cZTYLARKNj8SKlPGPdIUh9RmyQaYAJnJrLObaKiNiU4'}
請求參數:{'username': 'tery', 'password': 566, 'timer': 1614413210, 'timer_str': ' 1614413210'}
上傳文件:None
響應數據:{'data': {'id': 502, 'username': 'linken', 'role_id': 34}, 'meta': {'msg': '更新成功', 'status': 200}}
2021-02-27 16:06:50.835 | INFO     | tools.data_process:save_response:27 - 添加key: case_002, 對應value: {'data': {'id': 502, 'username': 'linken', 'role_id': 34}, 'meta': {'msg': '更新成功', 'status': 200}}
2021-02-27 16:06:50.835 | INFO     | tools.data_process:assert_result:115 - 第1個斷言,實際結果:{'msg': '更新成功', 'status': 200} | 預期結果:{'msg': '設置狀態成功', 'status': 200} 
斷言結果 False
2021-02-27 16:06:53.418 | SUCCESS  | __main__:run:43 - 報告已生成

分析日誌

從上述日誌可以看出語法糖@sum_data(&$.case_001.data.id&, 2)@運行之後的結果為502, 其處理的順序則是先&$.case_001.data.id& 提取出來得到的值是500, 然後調用函數sum_data(500, 2),然後運行這個函數並把結果502 與@sum_data(&$.case_001.data.id&, 2)@進行替換.

3. 自定義函數實現代碼

因為這裡用例讀取出來的內容是字符串,我影響中反射應該能做到這一點,然後就去找了下資料,然後我又找到getattr()這個內置函數但是這個函數不能解決用例帶參數的函數問題,然後我把目光移到了exec()內置函數,該函數可以執行字符串的Python代碼,然而我又遇到了問題,該函數裏面執行的Python代碼變量在其他函數中不能順利取出來用,最後我找到了資料 在函數內部使用locals()得到一個局部變量字典,通過字典取值的方式 把exec中的變量 取出來

tools/hooks.py

#!/usr/bin/env python
# _*_ coding: utf-8 _*_
"""
@project: apiAutoTest
@file: hooks.py
@author: zy7y
@time: 2021/2/27
@site: //cnblogs.com/zy7y
@github: //github.com/zy7y
@gitee: //gitee.com/zy7y
@desc: 擴展方法, 2021/02/27
關於exec執行python代碼可查閱資料://python3-cookbook.readthedocs.io/zh_CN/latest/c09/p23_executing_code_with_local_side_effects.html

"""
import time

def exec_func(func: str) -> str:
    """執行函數(exec可以執行Python代碼)
    :params func 字符的形式調用函數
    : return 返回的將是個str類型的結果
    """
    # 得到一個局部的變量字典,來修正exec函數中的變量,在其他函數內部使用不到的問題
    loc = locals()
    exec(f"result = {func}")
    return str(loc['result'])


def get_current_highest():
    """獲取當前時間戳"""
    return int(time.time())


def sum_data(a, b):
    """計算函數"""
    return a + b

tools/__init__.py

寫到這裡發現篇幅過長了,所以這裡就把改動的地方代碼貼出來吧

#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: apiAutoTest
@author: zy7y
@file: __init__.py.py
@ide: PyCharm
@time: 2020/7/31
"""
import json
import re
import allure
from jsonpath import jsonpath
from loguru import logger

from tools.hooks import *


... 上方代碼省略


def rep_expr(content: str, data: dict, expr: str = '&(.*?)&') -> str:
    """從請求參數的字符串中,使用正則的方法找出合適的字符串內容並進行替換
    :param content: 原始的字符串內容
    :param data: 在該項目中一般為響應字典,從字典取值出來
    :param expr: 查找用的正則表達式
    return content: 替換表達式後的字符串
    """
    for ctt in re.findall(expr, content):
        content = content.replace(f'&{ctt}&', str(extractor(data, ctt)))
        logger.debug(f"&{ctt}& 替換的值為 {str(extractor(data, ctt))} ")
        
    # 增加自定義函數得的調用,函數寫在tools/hooks.py中
    for func in re.findall('@(.*?)@', content):
        content = content.replace(f'@{func}@', exec_func(func))
        logger.debug(f"執行hooks函數{func} 替換的值為 {exec_func(func)}")

    return content

... 下方代碼省略

4. 源碼地址

github: //www.github.com/zy7y/apiAutoTest

gitee: //www.gitee.com/zy7y/apiAutoTest

其中最早版本(採用字典迭代方式處理依賴)在 version1.0分支

5. 致謝

感謝所有給予我幫助的人,文章,正在學習或使用apiAutoTest的同學們,其實個人開源這個項目以來個人有得到成就感,非常感謝

參考資料:

python3-cookbook

6. 往日文章

apiAutoTest: 開源啦

apiAutoTest: 使用re庫來處理接口之前的數據依賴

apiAutoTest: 增加數據隔離(測試前後備份/還原數據庫)

apiAutoTest: 增加自定義函數,用例中可以調用