一、簡介
本文從一個簡單的登錄接口測試入手沈善,一步步調(diào)整優(yōu)化接口調(diào)用姿勢乡数,然后簡單討論了一下接口測試框架的要點,最后介紹了一下我們目前正在使用的接口測試框架pithy闻牡。期望讀者可以通過本文對接口自動化測試有一個大致的了解净赴。
二、引言
1罩润、為什么要做接口自動化測試玖翅?
在當前互聯(lián)網(wǎng)產(chǎn)品迭代頻繁的背景下,回歸測試的時間越來越少,很難在每個迭代都對所有功能做完整回歸金度。但接口自動化測試因其實現(xiàn)簡單应媚、維護成本低,容易提高覆蓋率等特點猜极,越來越受重視中姜。
2、為什么要自己寫框架呢跟伏?
使用requets + unittest很容易實現(xiàn)接口自動化測試丢胚,而且requests的api已經(jīng)非常人性化,非常簡單受扳,但通過封裝以后(特別是針對公司內(nèi)特定接口)携龟,再加上對一些常用工具的封裝,可以進一步提高業(yè)務腳本編寫效率勘高。
三峡蟋、環(huán)境準備
確保本機已安裝python2.7以上版本,然后安裝如下庫:
pip install flask
pip install requests
后面我們會使用flask寫一個用來測試的接口相满,使用requests去測試层亿。
加入我們,立美,匿又,交流!群建蹄。碌更。642830685,領取最新軟件測試大廠面試資料和Python自動化洞慎、接口痛单、框架搭建學習資料!一起學習交流
四劲腿、測試接口準備
下面使用flask實現(xiàn)兩個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)
最后執(zhí)行如下命令:
python demo.py
響應如下:
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
大家可以看到服務已經(jīng)起來了丹允。
接口信息
登錄接口
-請求url
/login
-請求方法
post
-請求參數(shù)
-響應信息
詳情接口
-請求url
/info
-請求方法
get
-請求cookies
-響應信息
五郭厌、編寫接口測試
測試思路
使用requests [使用鏈接] 庫模擬發(fā)送HTTP請求。
使用python標準庫里unittest寫測試case雕蔽。
腳本實現(xiàn)
#!/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'
六折柠、優(yōu)化
封裝接口調(diào)用
寫完這個測試登錄腳本,你或許會發(fā)現(xiàn)萎羔,在整個項目的測試過程液走,登錄可能不止用到一次碳默,如果每次都這么寫贾陷,會不會太冗余了? 對嘱根,確實太冗余了髓废,下面做一下簡單的封裝,把登錄接口的調(diào)用封裝到一個方法里该抒,把調(diào)用參數(shù)暴漏出來,示例腳本如下:
#!/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凑保,在這一個版本中冈爹,我們不但在把登錄接口的調(diào)用封裝成了一個實例方法,實現(xiàn)了復用欧引,而且還把host(self.base_url)提取了出來频伤,但問題又來了,登錄之后芝此,登錄接口的http響應會把session以 cookie的形式set到客戶端憋肖,之后的接口都會使用此session去請求,還有婚苹,就是在接口調(diào)用過程中岸更,希望可以把日志打印出來,以便調(diào)試或者出錯時查看膊升。
好吧怎炊,我們再來改一版。
保持cookies&增加log信息
使用requests庫里的同一個Session對象(它也會在同一個Session 實例發(fā)出的所有請求之間保持 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 urljoin
class DemoApi(object):
def __init__(self, base_url):
self.base_url = base_url
# 創(chuàng)建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糟港、請求參數(shù):')
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 response
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):
"""
測試獲取詳情信息
"""
self.app.login(self.username, self.password)
response = self.app.info()
assert response['code'] == 200
assert response['msg'] == 'success'
assert response['data'] == 'info'
大功告成剥汤,我們把多個相關接口調(diào)用封裝到一個類中颠放,使用同一個requests Session實例來保持cookies,并且在調(diào)用過程中打印出了日志吭敢,我們所有目標都實現(xiàn)了碰凶,但再看下腳本,又會感覺不太舒服鹿驼,在每個方法里欲低,都要寫一遍print 1、2畜晰、3… 要拼url砾莱、還要很多細節(jié)等等,但其實我們真正需要做的只是拼出關鍵的參數(shù)(url參數(shù)凄鼻、body參數(shù)或者傳入headers信息)腊瑟,可不可以只需定義必須的信息,然后把其它共性的東西都封裝起來呢块蚌,統(tǒng)一放到一個地方去管理闰非?
封裝重復操作
來,我們再整理一下我們的需求:
首先峭范,不想去重復做拼接url的操作财松。
然后,不想每次都去手工打印日志虎敦。
不想和requests session打交道游岳。
只想定義好參數(shù)就直接調(diào)用。
我們先看一下實現(xiàn)后其徙,腳本可能是什么樣:
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
調(diào)用登錄接口的日志:
******************************************************
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參數(shù)
{
"password": "123456",
"username": "admin"
}
6闹获、響應結(jié)果
{
"code": 200,
"msg": "success"
}
在這里期犬,我們使用python的裝飾器功能,把公共特性封裝到裝飾器中去實現(xiàn)”芊蹋現(xiàn)在感覺好多了龟虎,沒什么多余的東西了,我們可以專注于關鍵參數(shù)的構(gòu)造沙庐,剩下的就是如何去實現(xiàn)這個裝飾器了鲤妥,我們先理一下思路:
1佳吞、獲取裝飾器參數(shù)
2、獲取函數(shù)/方法參數(shù)
3棉安、把裝飾器和函數(shù)定義的參數(shù)合并
4底扳、拼接url
5、處理requests session贡耽,有則使用衷模,無則新生成一個
6、組裝所有參數(shù)蒲赂,發(fā)送http請求并打印日志
因篇幅限制阱冶,源碼不再列出,有興趣的同學可以查看已經(jīng)實現(xiàn)的源代碼凳宙。
七熙揍、擴展
http接口請求的姿勢我們定義好了,我們還可以做些什么呢氏涩?
1、[x] 非HTTP協(xié)議接口
2有梆、[x] 測試用例編寫
3是尖、[x] 配置文件管理
4、[x] 測試數(shù)據(jù)管理
5泥耀、[x] 工具類編寫
6饺汹、[x] 測試報告生成
7、[x] 持續(xù)集成
8痰催、[x] 等等等等
需要做的還是挺多的兜辞,要做什么不要做什么,或者先做哪個夸溶,我覺得可以根據(jù)以下幾點去判斷:
是否有利于提高團隊生產(chǎn)效率逸吵?
是否有利于提高測試質(zhì)量?
有沒有現(xiàn)成的輪子可以用缝裁?
下面就幾項主要的點進行一下說明扫皱,限于篇幅,不再展開了捷绑。
測試報告
這個應該是大家最關心的了韩脑,畢竟這是測試工作的產(chǎn)出;
目前python的主流單元測試框均有report插件粹污,因此不建議自己再編寫段多,除非有特殊需求的。
pytest:推薦使用pytest-html和allure pytest壮吩。
unittest:推薦使用HTMLTestRunner进苍。
持續(xù)集成
持續(xù)集成推薦使用Jenkins蕾总,運行環(huán)境、定時任務琅捏、觸發(fā)運行生百、郵件發(fā)送等一系列功能均可以在Jenkins上實現(xiàn)。
測試用例編寫
推薦遵守如下規(guī)則:
原子性:每個用例保持獨立柄延,彼此不耦合蚀浆,以降低干擾。
專一性:一個用例應該專注于驗證一件事情搜吧,而不是做很多事情市俊,一個測試點不要重復驗證。
穩(wěn)定性:絕大多數(shù)用例應該是非常穩(wěn)定的滤奈,也就是說不會經(jīng)常因為除環(huán)境以外的因素掛掉摆昧,因為如果在一個測試項目中有很多不穩(wěn)定的用例的話,測試結(jié)果就不能很好的反應項目質(zhì)量蜒程。
分類清晰:有相關性的用例應寫到一個模塊或一個測試類里绅你,這樣做即方便維護,又提高了報告的可讀性昭躺。
測試工具類
這個可以根據(jù)項目情況去做忌锯,力求簡化一些類庫的使用,數(shù)據(jù)庫訪問领炫、日期時間偶垮、序列化與反序列化等數(shù)據(jù)處理,或者封裝一些常用操作帝洪,如隨機生成訂單號等等似舵,以提高腳本編寫效率。
測試數(shù)據(jù)管理
常見的方式有寫在代碼里葱峡、寫在配置文件里(xml砚哗、yaml、json族沃、.py频祝、excel等)、寫在數(shù)據(jù)庫里等脆淹,該處沒有什么好推薦的常空,建議根據(jù)個人喜好,怎么方便怎么來就可以盖溺。
八漓糙、pithy測試框架介紹
pithy意為簡潔有力的,意在簡化自動化接口測試烘嘱,提高測試效率昆禽。
目前實現(xiàn)的功能如下:
1蝗蛙、一鍵生成測試項目
2、http client封裝
3醉鳖、thrift接口封裝
4捡硅、簡化配置文件使用
5、優(yōu)化JSON盗棵、日期等工具使用
編寫測試用例推薦使用pytest壮韭,pytest提供了很多測試工具以及插件,可以滿足大部分測試需求纹因。
安裝
pip install pithy-test
pip install pytest
使用
一鍵生成測試項目
>>> pithy-cli init
請選擇項目類型,輸入api或者app: api
請輸入項目名稱,如pithy-api-test: pithy-api-test
開始創(chuàng)建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
調(diào)用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對象
# 結(jié)果取值, 假設此處response = {'a': 1, 'b': { 'c': [1, 2, 3, 4]}}
response = post('13111111111', '123abc').json
print response.b.c # 通過點號取值,結(jié)果為[1, 2, 3, 4]
print response('$.a') # 通過object path取值,結(jié)果為1
for i in response('$..c[@>3]'): # 通過object path取值,結(jié)果為選中c字典里大于3的元素
print i
優(yōu)化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 # 結(jié)果:1
print result.b.a # 結(jié)果:[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)
九惊畏、總結(jié)
在本文中恶耽,我們以提高腳本開發(fā)效率為前提,一步一步打造了一個簡易的測試框架陕截,但因水平所限驳棱,并未涉及測試數(shù)據(jù)初始化清理、測試中如何MOCK等話題农曲,前路依然任重而道遠,希望給大家一個啟發(fā)驻债,不足之處還望多多指點乳规,非常感謝。