Python接口測試課程(第三天)-接口安全驗證,參數化及斷言

目錄

Python接口測試課程(第一天)-Python基礎
Python接口測試課程(第二天)-接口測試快速實踐
Python接口測試課程(第三天)-接口安全驗證,參數化及斷言
Python接口測試課程(第四天)-接口測試框架實現

更多學習資料請加添加作者微信:lockingfree獲取

第三天: Python接口測試(二)

各種類型接口的測試

GET請求接口

requests.get(url=url, params=params)

表單類型

requests.post(url=url, data=data)

REST類型

requests.post(url=url, headers={"Content-Type": "application/json"}, data=json.dumps(data)

上傳文件

requests.post(url=url, files={"file": open("1.jpg", "rb")})

Session依賴

session=requests.session(); session.post(...); session.post()

接口依賴

  1. 接口依賴之動態中間值
resp=requests.get(...);token=resp.split("=")[1];resp2=requests.post(....token...)

驗簽接口

import hashlib

def md5(str):
    m = hashlib.md5()
    m.update(str.encode('utf8'))
    return m.hexdigest()  #返回摘要,作為十六進制數據字符串值

def makeSign(params):
    if not isinstance(params, dict):
        print("參數格式不正確,必須為字典格式")
        return None
    if 'sign' in params:
        params.pop('sign')
    sign = ''
    for key in sorted(params.keys()):
        sign = sign + key + '=' + str(params[key]) + '&'
    sign = md5(sign + 'appsecret=' + appsecret)
    params['sign'] = sign
    return params
data = makeSign(data);resp = requests.post(url=url, headers=headers, data=json.dumps(data))
  1. 接口依賴之Mock Server

Mock和單元測試的樁(Stub)類似, 是通過建立一個模擬對象來解決依賴問題的一種方法.

應用場景:
1. 依賴第三方系統接口, 無法調試
2. 所依賴接口尚未具備(前后端分離項目, 前端開發時,后端接口尚未開發完畢)
3. 所依賴接口需要修改或不穩定
4. 依賴接口較多或場景復雜, 所依賴接口不是主要驗證目標的

解決方法:
1. 通過Mock.js/RAP/RAP2來動態生成, 模擬接口返回數據
2. 自己使用Flask大家簡單的Mock接口
3. 使用Python自帶的mock庫

...

SOAP接口

pip install suds

from suds.client import Client

ip = '127.0.0.1'
port = '5001'

client = Client("http://%s:%s/?wsdl" % (ip, port))
result = client.service.addUser("張790", "123456")
print(result)

XML-RPC接口

import xmlrpc.client

user = xmlrpc.client.ServerProxy('http://127.0.0.1:5002')
print(user.getAll())

參數化

參數化是用來解決動態參數問題的

數據文件參數化

  • csv數據文件
    • 優點:以逗號分隔,輕量級
    • 缺點:參數過多不便于區分
import csv

csv_data = csv.reader(open('data/reg.csv'))
  • config數據文件
    • 優點:方便支持多組數據,支持備注
import configparser
    cf=configparser.ConfigParser()
    cf.read('data/reg.config', encoding='utf-8')
    cf.sections()
    cf.options(section)
    cf.get(section,option)
  • json數據文件
    • 優點:REST接口常用數據格式,格式清楚,適用于多參數及含有嵌套參數
    • 缺點:不支持備注,多組數據不清晰
import json
with open('data/reg.json', encoding='utf-8') as f:
    json_data = json.loads(f)  #json_data為列表或字典
json的序列化和反序列化
需求:python的字典/列表等數據格式為內存對象,需要做存儲(持久化)和進行數據交換

序列化: 從內存對象到可存儲數據, 方便存儲和交換數據
    json.dumps: 列表/字典/json->字符串 ```str_data = json.dumps(dict_data)```
    json.dump: 列表/字典/json->數據文件 ```json.dump(dict_data, open(data_file, "w"))```
反序列化: 從存儲數據到內存對象
    json.loads: 字符串->字典/列表```json.loads('{"a":1,"b":2}') #得到字典{"a":1,"b":2}```
    json.load: json數據文檔->列表/字典```dict_data = json.load(open('data.json'))```
  • excel數據文件
    • 優點:直觀,構造數據方便
    • 缺點:嵌套數據不方便格式化

pip install xlrd

import xlrd

wb=xlrd.open_workbook("data/reg.xlsx")
sh=wb.sheet_by_index(0)
sh=wb.sheet_by_name('Sheet1")
sh.nrows
sh.ncols
sh.cell(x,y).value
  • xml數據文件
    • 優點:方便自定義多層結構,SOAP,RPC通用格式
from xml.dom.minidom import parse
dom=parse('data/reg.xml')
root=dom.documentElement
user_nodes=root.getElementsByTagName("user")
user_node.getAttribute('title')
user_node.hasAttribute('title')
name_node=user_node.getElementsByTagName('name')[0]
name=name_node.childNodes[0].data

案例1: 自動執行excel用例并將結果回寫excel

數據文件: test_user.xlsx

TestCase Url Method DataType Data Excepted Resp.text Status
test_user_reg_normal /api/user/reg/ POST JSON {"name":"九小1","passwd": "123456"} resp.json()["code"]=="100000"
test_user_login_normal /api/user/login/ POST FORM {"name":"九小1","passwd": "123456"} "成功" in resp.text
import xlrd
from xlutils.copy import copy
import json
import requests
import sys

base_url = "http://127.0.0.1:5000"

def run_excel(file_name, save_file="result.xls"):
    wb=xlrd.open_workbook(file_name)
    sh=wb.sheet_by_index(0)

    wb2 = copy(wb)
    sh2 = wb2.get_sheet(0)


    for i in range(1,sh.nrows):
        url = base_url + sh.cell(i,1).value
        data = json.loads(sh.cell(i,4).value)
        headers = {}
        method = sh.cell(i,2).value
        data_type = sh.cell(i,3).value
        excepted = sh.cell(i,5).value
        if data_type.lower() == 'json':
            data = json.dumps(data)
            headers = {"Content-Type":"application/json"}

        if method.lower() == "get":
            resp = requests.get(url=url,headers=headers)
        else:
            resp = requests.post(url=url,headers=headers,data=data)
        if eval(excepted):
            status = "PASS"
        else:
            status = "FAIL"
        sh2.write(i,6, resp.text)
        sh2.write(i,7, status)

    wb2.save(save_file)
    print("保存成功")
        
        
if __name__ == "__main__":
    if len(sys.argv)==2:
        run_excel(sys.argv[1])
    elif len(sys.argv)>2:
        run_excel(sys.argv[1],sys.argv[2])
    else:
        print("調用格式: python run_excel.py 用例文件 輸出文件")

保存腳本為run_excel.py, (最好和數據文件test_user.xlsx放在同一目錄下), 在腳本所在目錄打開命令行,運行

python run_excel.py test_user.xlsx

生成的result.xls預覽

TestCase Url Method DataType Data Excepted Resp.text Status
test_user_reg_normal /api/user/reg/ POST JSON {"name":"九小1","passwd": "123456"} resp.json()["code"]=="100000" {"code":"100001","data":{"name":"\u4e5d\u5c0f1","passwod":"e10adc3949ba59abbe56e057f20f883e"},"msg":"\u5931\u8d25\uff0c\u7528\u6237\u5df2\u5b58\u5728"} FAIL
test_user_login_normal /api/user/login/ POST FORM {"name":"九小1","passwd": "123456"} "成功" in resp.text <h1>登錄成功</h1> PASS

隨機數據參數化

import random

  • 隨機數
    • random.random(): 隨機0,1
    • random.randint(0,100): 隨機整數
    • random.randrange(0,100,2): 隨機偶數
    • random.uniform(1,100): 隨機浮點數
  • 隨機選擇
    • random.choice('abcdefg')
    • random.choice(['趙','錢','孫','李','周'])
隨機姓名的實現:
#隨機漢字: chr(random.randint(0x4e00, 0x9fbf))
name=random.choice(['趙','錢','孫','李','周'])+chr(random.randint(0x4e00, 0x9fbf)
  • 隨機樣本
    • random.sample('abcdefg', 2)

隨機2個字符拼接: ''.join(random.sample('abcdefg',2)

  • 洗牌
    • random.shuffle([1, 2, 3, 4, 5, 6]): 隨機改版列表數據

斷言/檢查點

斷言/檢查點是用來自動驗證接口返回數據/業務操作的結果是否滿足預期

響應斷言

正則表達式

  • 元字符
    • . : 任意字符
    • \d: 任意數字 - \D: 任意非數字
    • \w: 任意字符或數字 - \W: 任意非字符及數字
    • \s: 任意空白字符(包含空格或\n等) - \S: 任意非空白字符
    • ^: 匹配開頭
    • $: 匹配結尾
    • {n,m}: 匹配n-m個重復
      • : 匹配重復任意次(包含0次)
      • : 匹配重復至少一次
    • ? : 匹配0或1次
    • (): 分組,獲取需要的部分數據
    • | : 或, 匹配多個pattern
    • \元字符: 取消元字符轉義
  • 貪婪匹配及非貪婪匹配
  • 系統函數
    • re.findall(): re.S,支持跨行
    • re.match()
    • re.search()
    • re.sub()/re.subn()
    • re.complie()

數據庫斷言

從數據庫讀取數據,驗證數據庫數據是否符合預期

  • MySQL斷言

pip install pymysql

  1. 導入pymysql: import pymysql
  2. 建立數據庫連接:
conn = pymysql.connect(host='',port=3306,db='',user='',passwd='',charset='utf8')
  1. 從連接建立操作游標: cur=conn.cursor()
  2. 使用游標執行sql命令: cur.execute("select * from user")
  3. 獲取執行結果:
    1. cur.fetchall(): 獲取所有結果
    2. cur.fetchmany(3): 獲取多條結果
    3. cur.fetchone(): 獲取一條結果
  4. 關閉游標及連接(先關游標再關連接):cur.close();conn.close()
  • PostgreSQL

pip install pyscopg2

import pyscopg2
conn=pyscopg2.connect(host='',port='',dbname='',user='',password='') # 注意是dbname, password
cur=conn.curser()
cur.execute("...")
cur.fetchall()
  • Oracle

pip install cx_Oracle

...
  • Mongodb

pip install pymongo

from pymongo import MongoClient

conn = MongoClient('', 27017)
db = conn.mydb
my_set = db.test_set

for i in my_set.find({"name": "張三"}):
    print(i)

print(my_set.findone({"name": "張三"}))
  • Redis斷言

pip install redis

import redis

r = redis.Redis(host='192.168.100.198', port=6379, password='!QE%^E2sdf23RGF@ml239', db=0)
print(r.dbsize())
print(r.get('package'))
print(r.hget('event_order_advance_008aea6a62115ec4923829ee09f76a9c18243f5d', 'user'))

服務器斷言

pip install paramiko

import paramiko

client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())  
client.connect('192.168.100.241', 22, username='root', password='1234567', timeout=4)
stdin, stdout, stderr = client.exec_command('cat /proc/meminfo')
print(stdout.read())
client.close()

完整用例:

import requests
import pytest
import json
import hashlib
import re


def md5(string):
    m = hashlib.md5()
    m.update(string.encode('utf8'))
    return m.hexdigest()

def makeSign(data):
    sign=''
    for key in sorted(data.keys()):
        sign += key + '=' + str(data[key]) + '&'
    sign += 'appsecret=NTA3ZTU2ZWM5ZmVkYTVmMDBkMDM3YTBi'
    data['sign'] = md5(sign)
    return data

class DB():
    def __init__(self):
        # 建立連接
        self.conn = pymysql.connect(host='localhost',port=3307,user='root',passwd='',db='api',charset='utf8')

        # 建立一個游標
        self.cur = self.conn.cursor()
    
    def __del__(self):
        self.cur.close()
        self.conn.close()

    def getUserByName(self, name):
        self.cur.execute("select * from user where name='%s'" % name)
        return self.cur.fetchone()

    def checkUser(self, name, passwd):
        user = self.getUserByName(name)
        if user:
            if user[2] == md5(passwd):
                return True
            else:
                return False
        else:
            return None
            
class TestUser(): # pytest識別不能用__init__方法
    base_url = 'http://127.0.0.1:5000'
    db = DB()

    def test_login(self):
        url = self.base_url + '/api/user/login/'
        data = {"name": "張三", "passwd": "123456"}
        resp = requests.post(url=url, data=data)

        #斷言
        assert resp.status_code == 200
        assert '登錄成功' in resp.text

    def test_reg(self):
        url = self.base_url + '/api/user/reg/'
        headers = {"Content-Type": "application/json"}
        data = {'name': '張10', 'passwd': '123456'}
        resp = requests.post(url=url, headers=headers, data=json.dumps(data))

        #斷言
        assert resp.json()['code'] == '100000'
        assert resp.json()['msg'] == '成功'
        assert self.db.getUserByName('張10')


    def test_uploadImage(self):
        url = self.base_url + '/api/user/uploadImage/'
        files = {'file': open("復習.txt")}
        resp = requests.post(url=url, files=files)

        #斷言
        assert resp.status_code == 200
        assert '成功' in resp.text
        # todo 服務器斷言

    def test_getUserList(self):
        session = requests.session()
        login_url = self.base_url + '/api/user/login/'
        login_data = {"name": "張三", "passwd": "123456"}
        session.post(url=login_url, data=login_data)

        url = self.base_url + '/api/user/getUserList/'
        resp = session.get(url=url)

        #斷言
        assert resp.status_code == 200
        assert '用戶列表' in resp.text
        assert re.findall('\w{32}',t2) != []

    def test_updateUser(self):
        session = requests.session()  # 接口依賴的接口需要用session
        get_token_url = self.base_url + '/api/user/getToken/'
        params = {"appid": '136425'}
        token_resp = session.get(url=get_token_url, params=params)
        assert re.findall('token=\w{32}$')
        token = token_resp.text.split('=')[1]

        url = self.base_url + '/api/user/updateUser/?token=' + token
        data = {'name': '張三', 'passwd': '234567'}
        headers = {"Content-Type": "application/json"}
        resp = session.post(url=url, headers=headers, data=json.dumps(data))

        #斷言
        assert resp.status_code == 200
        assert resp.json()['code'] == '100000'
        assert resp.json()['msg'] == '成功'
        assert self.db.checkUser('張三', '234567')

    def test_delUser(self):
        url = self.base_url + '/api/user/delUser/'
        headers = {"Content-Type": "application/json"}
        data = {'name': '張10', 'passwd': '123456'}
        data = makeSign(data)
        resp = requests.post(url=url, headers=headers, data=json.dumps(data))

        #斷言
        assert resp.status_code == 200
        assert resp.json()['code'] == '100000'
        assert resp.json()['msg'] == '成功' 
        assert not self.db.getUserByName('張10')


if __name__ == '__main__':
    t = TestUser()
    # t.test_updateUser()
    # t.test_updateUser()
    t.test_delUser()
    # pytest.main("-q test_user2.py")
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容