歡迎關注公衆号 學習資料不會少
01
簡介
本文從一個簡單的登入接口測試入手,一步步調整優化接口調用姿勢,然後簡單讨論了一下接口測試架構的要點,最後介紹了一下我們目前正在使用的接口測試架構pithy。期望讀者可以通過本文對接口自動化測試有一個大緻的了解。
02
引言
接口自動化測試的重要性:
在目前網際網路産品疊代頻繁的背景下,回歸測試的時間越來越少,很難在每個疊代都對所有功能做完整回歸。但接口自動化測試因其實作簡單、維護成本低,容易提高覆寫率等特點,越來越受重視。
怎樣提高架構的編寫效率:
用requets + unittest很容易實作接口自動化測試,而且requests的api已經非常人性化,非常簡單,但通過封裝以後(特别是針對公司内特定接口),再加上對一些常用工具的封裝,可以進一步提高業務腳本編寫效率。
03
環境準備
確定本機已安裝python3.7.2以上版本,然後安裝如下:
● pip install flask
● pip install requests
後面我們會使用flask寫一個用來測試的接口,使用requests去測試
04
測試接口準備
下面使用flask實作兩個http接口,一個登入,另外一個查詢詳情,但需要登入後才可以,建立一個demo.py檔案(注意,不要使用windows記事本),把下面代碼copy進去,然後儲存、關閉
接口代碼
#!/usr/bin/python
# coding=utf-8
from flask import Flask, request, session, jsonify
USERNAME = 'admin'
PASSWORD = '123456'
app = Flask(__name__)
app.secret_key = 'pithy'
@app.route('/login', methods=['GET', 'POST'])
def login():
error = None
if request.method == 'POST':
if request.form['username'] != USERNAME:
error = 'Invalid username'
elif request.form['password'] != PASSWORD:
error = 'Invalid password'
else:
session['logged_in'] = True
return jsonify({'code': 200, 'msg': 'success'})
return jsonify({'code': 401, 'msg': error}), 401
@app.route('/info', methods=['get'])
def info():
if not session.get('logged_in'):
return jsonify({'code': 401, 'msg': 'please login !!'})
return jsonify({'code': 200, 'msg': 'success', 'data'
: 'info'})
if __name__ == '__main__':
app.run(debug=True)
最後執行如下指令
python demo.py
響應如下
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
大家可以看到服務已經起起來了
接口資訊
登入接口
請求url
● /login
請求方法
● post
請求參數
響應資訊
詳情接口
請求url
● /info
請求方法
● get
請求cookies
響應資訊
05
編寫接口測試
測試思路
使用requests [使用連結] 庫模拟發送HTTP請求
使用python标準庫裡unittest寫測試case
腳本實作
#!/usr/bin/python
# coding=utf-8
import requests
import unittest
class TestLogin(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.login_url = 'http://127.0.0.1:5000/login'
cls.info_url = 'http://127.0.0.1:5000/info'
cls.username = 'admin'
cls.password = '123456'
def test_login(self):
"""
測試登入
"""
data = {
'username': self.username,
'password': self.password
}
response = requests.post(self.login_url, data=data).
json()
assert response['code'] == 200
assert response['msg'] == 'success'
def test_info(self):
"""
測試info接口
"""
data = {
'username': self.username,
'password': self.password
}
response_cookies = requests.post(self.login_url,
data=data).cookies
session = response_cookies.get('session')
assert session
info_cookies = {
'session': session
}
response = requests.get(self.info_url, cookies=info_
cookies).json()
assert response['code'] == 200
assert response['msg'] == 'success'
assert response['data'] == 'info'
06
優化
封裝接口調用
寫完這個測試登入腳本,你或許會發現,在整個項目的測試過程,登入可能不止用到一次,如果每次都這麼寫,會不會太備援了?對,确實太備援了,下面做一下簡單的封裝,把登入接口的調用封裝到一個方法裡,把調用參數暴漏出來,示例腳本如下:
#!/usr/bin/python
# coding=utf-8
import requests
import unittest
try:
from urlparse import urljoin
except ImportError:
from urllib.parse import urljoin
class DemoApi(object):
def __init__(self, base_url):
self.base_url = base_url
def login(self, username, password):
"""
登入接口
:param username: 使用者名
:param password: 密碼
"""
url = urljoin(self.base_url, 'login')
data = {
'username': username,
'password': password
}
return requests.post(url, data=data).json()
def get_cookies(self, username, password):
"""
擷取登入cookies
"""
url = urljoin(self.base_url, 'login')
data = {
'username': username,
'password': password
}
return requests.post(url, data=data).cookies
def info(self, cookies):
"""
詳情接口
"""
url = urljoin(self.base_url, 'info')
return requests.get(url, cookies=cookies).json()
class TestLogin(unittest.TestCase):
@classmethod def setUpClass(cls):
cls.base_url = 'http://127.0.0.1:5000'
cls.username = 'admin'
cls.password = '123456'
cls.app = DemoApi(cls.base_url)
def test_login(self):
"""
測試登入
"""
response = self.app.login(self.username, self.
password)
assert response['code'] == 200
assert response['msg'] == 'success'
def test_info(self):
"""
測試擷取詳情資訊
"""
cookies = self.app.get_cookies(self.username, self.
password)
response = self.app.info(cookies)
assert response['code'] == 200
assert response['msg'] == 'success'
assert response['data'] == 'info'
OK,在這一個版本中,我們不但在把登入接口的調用封裝成了一個執行個體方法,實作了複用,而且還把host(self.base_url)提取了出來,但問題又來了,登入之後,登入接口的http響應會把session以 cookie的形式set到用戶端,之後的接口都會使用此session去請求,還有,就是在接口調用過程中,希望可以把日志列印出來,以便調試或者出錯時檢視。
好吧,我們再來改一版。
保持cookies&增加log資訊
使用requests庫裡的同一個Session對象(它也會在同一個Session 執行個體發出的所有請求之間保持 cookie),即可解決上面的問題,示例代碼如下:
#!/usr/bin/python
# coding=utf-8
import unittest
from pprint import pprint
from requests.sessions import Session
try:
from urlparse import urljoin
except ImportError:
from urllib.parse import urljoinclass DemoApi(object):
def __init__(self, base_url):
self.base_url = base_url
# 建立session執行個體
self.session = Session()
def login(self, username, password):
"""
登入接口
:param username: 使用者名
:param password: 密碼
"""
url = urljoin(self.base_url, 'login')
data = {
'username': username,
'password': password
}
response = self.session.post(url, data=data).json()
print('\n*****************************************')
print(u'\n1、請求url: \n%s' % url)
print(u'\n2、請求頭資訊:')
pprint(self.session.headers)
print(u'\n3、請求參數:')
pprint(data)
print(u'\n4、響應:')
pprint(response)
return response
def info(self):
"""
詳情接口
"""
url = urljoin(self.base_url, 'info')
response = self.session.get(url).json()
print('\n*****************************************')
print(u'\n1、請求url: \n%s' % url)
print(u'\n2、請求頭資訊:')
pprint(self.session.headers)
print(u'\n3、請求cookies:')
pprint(dict(self.session.cookies))
print(u'\n4、響應:')
pprint(response)
return responseclass TestLogin(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.base_url = 'http://127.0.0.1:5000'
cls.username = 'admin'
cls.password = '123456'
cls.app = DemoApi(cls.base_url)
def test_login(self):
"""
測試登入
"""
response = self.app.login(self.username, self.
password)
assert response['code'] == 200
assert response['msg'] == 'success'
def test_info(self):
"""
測試擷取詳情資訊
"""
self.app.login(self.username, self.password)
response = self.app.info()
assert response['code'] == 200
assert response['msg'] == 'success'
assert response['data'] == 'info'
大功告成,我們把多個相關接口調用封裝到一個類中,使用同一個requests Session執行個體來保持cookies,并且在調用過程中列印出了日志,我們所有目标都實作了,但再看下腳本,又會感覺不太舒服,在每個方法裡,都要寫一遍print 1、2、3... 要拼url、還要很多細節等等,但其實我們真正需要做的隻是拼出關鍵的參數(url參數、body參數或者傳入headers資訊),可不可以隻需定義必須的資訊,然後把其它共性的東西都封裝起來呢,統一放到一個地方去管理?
封裝重複操作
來,我們再整理一下我們的需求:
首先,不想去重複做拼接url的操作
然後,不想每次都去手工列印日志
不想和requests session打交道
隻想定義好參數就直接調用
我們先看一下實作後,腳本可能是什麼樣:
class DemoApi(object):
def __init__(self, base_url):
self.base_url = base_url
@request(url='login', method='post')
def login(self, username, password):
"""
登入接口
"""
data = {
'username': username,
'password': password
}
return {'data': data}
@request(url='info', method='get')
def info(self):
"""
詳情接口
"""
pass
調用登入接口的日志
******************************************************
1、接口描述
登入接口
2、請求url
http://127.0.0.1:5000/login
3、請求方法
post
4、請求headers
{
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Connection": "keep-alive",
"User-Agent": "python-requests/2.7.0 CPython/2.7.10 Darwin/16.4.0"
}
5、body參數
{
"password": "123456",
"username": "admin"}
6、響應結果
{
"code": 200,
"msg": "success"
}
在這裡,我們使用python的裝飾器功能,把公共特性封裝到裝飾器中去實作。現在感覺好多了,沒什麼多餘的東西了,我們可以專注于關鍵參數的構造,剩下的就是如何去實作這個裝飾器了,我們先理一下思路:
● 擷取裝飾器參數
● 擷取函數/方法參數
● 把裝飾器和函數定義的參數合并
● 拼接url
● 處理requests session,有則使用,無則新生成一個
● 組裝所有參數,發送http請求并列印日志
07
擴充
http接口請求的姿勢我們定義好了,我們還可以做些什麼呢?
● 非HTTP協定接口
● 測試用例編寫
● 配置檔案管理
● 測試資料管理
● 工具類編寫
● 測試報告生成
● 持續內建
● 等等等等
需要做的還是挺多的,要做什麼不要做什麼,或者先做哪個,我覺得可以根據以下幾點去判斷:
●是否有利于提高團隊生産效率
●是否有利于提高測試品質
●有沒有現成的輪子可以用
下面就幾項主要的點進行一下說明,限于篇幅,不再展開了
測試報告
請輸入
這個應該是大家最關心的了,畢竟這是測試工作的産出;
目前python的主流單元測試框均有report插件,是以不建議自己再編寫,除非有特殊需求的。
pytest:推薦使用pytest-html和allure pytest
unittest:推薦使用HTMLTestRunner
持續內建
請輸入
持續內建推薦使用Jenkins,運作環境、定時任務、觸發運作、郵件發送等一系列功能均可以在Jenkins上實作。
測試用例編寫
請輸入
推薦遵守如下規則:
● 必需參數覆寫。對于接口的參數,接口文檔一般都會說明哪些兒是必需的,哪兒是非必需的。對于必需的參數,一定要測試傳參數和不傳參數接口是否報錯?
● 必需的參數各種情況覆寫。傳非法的字元,特殊的字元,空值,超過邊界的參數是否報錯?錯誤資訊是否正确?
● 非必需參數覆寫。一般接口對于非必需參數都不會做非正常性傳值的判斷,是以要測試合法的參數值 ,接口傳回的内容是否正确。如果有接口文檔說明對非必需參數做了非正常的驗證的話,也要對其進行驗證。
● 參數的組合覆寫。有些兒參數需要互相配合着才起作用,如“offset”和“count”組合起來進行翻頁,這個時候要組合起來進行測試。
● 業務邏輯相關的覆寫。有些兒接口與業務邏輯關聯密切,單獨從接口角度測試,可能會遺漏掉一些兒因業務邏輯而産生的bug。是以如果和業務邏輯相關,也要考慮到業務邏輯相關的測試用例。
其實接口的測試用例差不多也就這些兒情況,也許有特殊的接口,到時候和産品,開發人員做好溝通,盡量先從接口層面保證品質。這樣再從測試接口的應用層的時候,就可以少很多工作量,隻注重樣式和各個接口調用的配合就可以了。
測試資料管理
自動化測試過程中,現在大多都預設測試腳本與測試資料分離的設計,這樣做的好處是:降低維護成本,遷移成本以及提高效率。
是以測試資料放在哪裡,如何管理,不能一概而論。個人覺得應該從以下幾方面來考慮:
業務場景
● 比如在UI自動化測試中,需要測試某個電商網站的各個業務子產品,但前提是要使用者登入。這個用來執行登入的測試賬号資料往往是固定的,那麼專門将一組username和password放在一個測試資料檔案或者測試資料庫中,這樣就顯得太笨重,耗時費力。将其寫入測試腳本或者寫入配置檔案,直接引用效率會更高。
● 同樣,測試電商網站,賬号體系分為普通賬号,會員賬号,會員還分很多等級,有時候為了測試會員中心不同的賬号展示的資訊是否不同,就需要使用不同的等級的賬号登入,這種場景下,可以将測試資料放在測試檔案裡(比如excel、csv),通過參數化的方式來循環讀取,執行後續操作。
● 在API自動化測試中,比如針對restful風格的接口,它的域名相對來說都是固定的,隻是不同接口的path不同,那麼也可以将域名寫入配置檔案,測試過程中隻需要将執行個體化的域名和path進行拼接即可,這樣也省卻了在測試資料檔案中維護的成本,一定程度上提升了測試效率。
資料類型
測試資料也分不同類型,大概分為以下幾種類型:
base-data:即基礎資料,比如電商網站的商品資訊、SKU,比如物流公司的倉儲管理等,這類資料往往基數比較大,可以視為持久層,儲存在DB中;test-data:測試資料,根據業務場景不同,資料無論量級還是變更頻次也不同,基于測試腳本與資料分離的概念,可放在專門的測試檔案中,比如excel、csv;ephemeral-data:臨時資料,即使用一次的資料,這種類型的資料可以用臨時檔案存儲(比如dat、csv等)格式,然後進行參數化讀取,或者直接寫入腳本中;
資料量級
● 還是電商網站的某個場景,需要先執行登入,登入的賬号比如是專門配置的一個測試賬号,相對固定,那麼将測試賬号寫入測試腳本也無可厚非。不過我本人不喜歡将測試資料直接寫入腳本,這種情況我會寫入配置檔案,然後執行個體化調用,這種情況就需要根據個人習慣來設計,沒有固定的套路;
● 資料量級在幾十——幾百上千之間,這種時候,可以寫入excel檔案進行存儲管理,但是excel的局限在于其本身目前最大支援65500+行的資料存儲,而且隻支援單事務,如果需要多線程讀取,就會變成瓶頸。
● csv檔案,結構簡單、通用,可以和excel進行轉換,可以減少存儲檔案size,且具備簡單的安全性,可以在一定程度上替代excel成為資料存儲檔案。我本人目前在大多數場景下也是使用csv類型的檔案進行測試資料存儲管理;
● 當測試資料超過一定量級,比如性能測試中,如果要執行并發測試或者穩定性測試,那麼所需測試資料量級就很大,這時使用excel或者csv就會變得很不友善。無論是從維護的成本還是便捷性考慮,都應該選擇利用DB或其他高效的管理方式來存儲和管理測試資料;
使用頻次
測試資料的重用頻次不同,也需要選擇不同的存儲方式,比如:
● once:隻使用一次的測試資料,那麼隻需要寫入臨時檔案,用完廢棄或者删除即可;
● often:即經常使用的測試資料,應根據資料量級,使用場景,資料類型選擇合适的存儲管理方式;
● alway:可以了解為base-data或者持久資料,這種類型的資料因為其本身更新頻次很低,或者資料量級較大,一般存儲在DB中是比較好的一種管理方案。
08
pithy測試架構介紹
pithy意為簡潔有力的,意在簡化自動化接口測試,提高測試效率
目前實作的功能如下:
一鍵生成測試項目
http client封裝
thrift接口封裝
簡化配置檔案使用
優化JSON、日期等工具使用
編寫測試用例推薦使用pytest,pytest提供了很多測試工具以及插件,可以滿足大部分測試需求。
安裝
pip install pithy-test
pip install pytest
使用一鍵生成測試項目
>>> pithy-cli init
請選擇項目類型,輸入api或者app: api
請輸入項目名稱,如pithy-api-test: pithy-api-test
開始建立pithy-api-test項目
開始渲染...生成 api/.gitignore [√]
生成 api/apis/__init__.py [√]
生成 api/apis/pithy_api.py [√]
生成 api/cfg.yaml [√]
生成 api/db/__init__.py [√]
生成 api/db/pithy_db.py [√]
生成 api/README.MD [√]
生成 api/requirements.txt [√]
生成 api/test_suites/__init__.py [√]
生成 api/test_suites/test_login.py [√]
生成 api/utils/__init__.py [√]
生成成功,請使用編輯器打開該項目
>>> tree pithy-api-test
pithy-api-test
├── README.MD
├── apis
│ ├── __init__.py
│ └── pithy_api.py
├── cfg.yaml
├── db
│ ├── __init__.py
│ └── pithy_db.py
├── requirements.txt
├── test_suites
│ ├── __init__.py
│ └── test_login.py
└── utils
└── __init__.py
4 directories, 10 files
調用HTTP登入接口示例
from pithy import request
@request(url='http://httpbin.org/post', method='post')
def post(self, key1='value1'):
"""
post method
"""
data = {
'key1': key1
}
return dict(data=data)
# 使用
response = post('test').to_json() # 解析json字元,輸出為字典
response = post('test').json # 解析json字元,輸出為字典
response = post('test').to_content() # 輸出為字元串
response = post('test').content # 輸出為字元串
response = post('test').get_cookie() # 輸出cookie對象
response = post('test').cookie # 輸出cookie對象
# 結果取值, 假設此處response = {'a': 1, 'b': { 'c': [1, 2, 3, 4]}}
response = post('13111111111', '123abc').json
print response.b.c # 通過點号取值,結果為[1, 2, 3, 4]
print response('$.a') # 通過object path取值,結果為1
for i in response('$..c[@>3]'): # 通過object path取值,結果為選中c字典裡大于3的元素
print i
優化JSON、字典使用
# 1、操作JSON的KEY
from pithy import JSONProcessor
dict_data = {'a': 1, 'b': {'a': [1, 2, 3, 4]}}
json_data = json.dumps(dict_data)
result = JSONProcessor(json_data)
print (result.a ) # 結果:1
print( result.b.a )# 結果:[1, 2, 3, 4]
# 2、操作字典的KEY
dict_data = {'a': 1, 'b': {'a': [1, 2, 3, 4]}}
result = JSONProcessor(dict_data)
print (result.a ) # 1
print( result.b.a ) # [1, 2, 3, 4]
# 3、object path取值
raw_dict = { 'key1':{
'key2':{
'key3': [1, 2, 3, 4, 5, 6, 7, 8]
}
}
}
jp = JSONProcessor(raw_dict)
for i in jp('$..key3[@>3]'):
print(i)
# 4、其它用法
dict_1 = {'a': 'a'}
json_1 = {"b": "b"}
jp = JSONProcessor(dict_1, json_1, c='c')
print(jp)
09
總結
在本文中,我們以提高腳本開發效率為前提,一步一步打造了一個簡易的測試架構,但因水準所限,并未涉及測試資料初始化清理、測試中如何MOCK等話題,前路依然任重而道遠,希望給大家一個啟發,不足之處還望多多指點,非常感謝。
往期精彩文章
喜報來了!凡貓學員薪資最高16K!
金融行業軟體測試介紹
2020年為什麼大家都開始學習自動化測試?
學習測試開發前 你需要掌握的python 代碼水準
1萬+軟體測試人員都在學的精品課程免費送,大家别錯過