HTTP 接口測試很簡單,不管工具、框架、還是平臺(tái),只要很的好的幾個(gè)點(diǎn)就是好工具。
測試數(shù)據(jù)問題:比如刪除接口,重復(fù)執(zhí)行還能保持結(jié)果一致,必定要做數(shù)據(jù)初始化。
接口依賴問題:B 接口依賴 A 的返回值,C 接口依賴 B 接口的返回值。
加密問題:不同的接口加密規(guī)則不一樣。有些用到時(shí)間戳、md5、base64、AES,如何提供種能力。
斷言問題:有些接口返回的結(jié)構(gòu)體很復(fù)雜,如何靈活的做到斷言。
對于以上問題,工具和平臺(tái)要么不支持,要么很麻煩,然而框架是最靈活的。
unittest/pytest + requests/https 直接上手寫代碼就好了,既簡單又靈活。
那么同樣是寫代碼,A 框架需要 10 行,B 框架只需要 5 行,然而又不失靈活性,那我當(dāng)然是選擇更少的了,畢竟,人生苦短嘛。
seldom 適合個(gè)人接口自動(dòng)化項(xiàng)目,它有以下優(yōu)勢。
可以寫更少的代碼
自動(dòng)生成 HTML/XML 測試報(bào)告
支持參數(shù)化,減少重復(fù)的代碼
支持生成隨機(jī)數(shù)據(jù)
支持 har 文件轉(zhuǎn) case
支持?jǐn)?shù)據(jù)庫操作
這些是 seldom 支持的功能,我們只需要集成 HTTP 接口庫,并提供強(qiáng)大的斷言即可。seldom 2.0 加入了 HTTP 接口自動(dòng)化測試支持。
Seldom 兼容 Requests API 如下:
seldom
requests
self.get()
requests.get()
self.post()
requests.post()
self.put()
requests.put()
self.delete()
requests.delete()
Seldom VS Request+unittest
先來看看 unittest + requests 是如何來做接口自動(dòng)化的:
import unittest
import requests
class TestAPI(unittest.TestCase):
? ? def test_get_method(self):
? ? ? ? payload = {'key1': 'value1', 'key2': 'value2'}
? ? ? ? r = requests.get("http://httpbin.org/get", params=payload)
? ? ? ? self.assertEqual(r.status_code, 200)
if __name__ == '__main__':
? ? unittest.main()
這其實(shí)已經(jīng)非常簡潔了。同樣的用例,用 seldom 實(shí)現(xiàn)。
# test_req.py
import seldom
class TestAPI(seldom.TestCase):
? ? def test_get_method(self):
? ? ? ? payload = {'key1': 'value1', 'key2': 'value2'}
? ? ? ? self.get("http://httpbin.org/get", params=payload)
? ? ? ? self.assertStatusCode(200)
if __name__ == '__main__':
? ? seldom.main()
主要簡化點(diǎn)在,接口的返回?cái)?shù)據(jù)的處理。當(dāng)然,seldom 真正的優(yōu)勢在斷言、日志和報(bào)告。
har to case
對于不熟悉 Requests 庫的人來說,通過 Seldom 來寫接口測試用例還是會(huì)有一點(diǎn)難度。于是,seldom 提供了har 文件轉(zhuǎn) case 的命令。
首先,打開 fiddler 工具進(jìn)行抓包,選中某一個(gè)請求。
然后,選擇菜單欄:file -> Export Sessions -> Selected Sessions...
選擇導(dǎo)出的文件格式。
點(diǎn)擊next 保存為demo.har 文件。
最后,通過seldom -h2c 轉(zhuǎn)為demo.py 腳本文件。
> seldom -h2c .\demo.har
.\demo.py
2021-06-14 18:05:50 [INFO] Start to generate testcase.
2021-06-14 18:05:50 [INFO] created file: D:\.\demo.py
demo.py 文件。
import seldom
class TestRequest(seldom.TestCase):
? ? def start(self):
? ? ? ? self.url = "http://httpbin.org/post"
? ? def test_case(self):
? ? ? ? headers = {"User-Agent": "python-requests/2.25.0", "Accept-Encoding": "gzip, deflate", "Accept": "application/json", "Connection": "keep-alive", "Host": "httpbin.org", "Content-Length": "36", "Origin": "http://httpbin.org", "Content-Type": "application/json", "Cookie": "lang=zh"}
? ? ? ? cookies = {"lang": "zh"}
? ? ? ? self.post(self.url, json={"key1": "value1", "key2": "value2"}, headers=headers, cookies=cookies)
? ? ? ? self.assertStatusCode(200)
if __name__ == '__main__':
? ? seldom.main()
運(yùn)行測試
打開 debug 模式seldom.run(debug=True) 運(yùn)行上面的用例。
> python .\test_req.py
2021-04-29 18:19:39 [INFO] A run the test in debug mode without generating HTML report!
2021-04-29 18:19:39 [INFO]
? ? ? ? ? ? ? __? ? __
? ________? / /___/ /___? ____ ____
? / ___/ _ \/ / __? / __ \/ __ ` ___/
(__? )? __/ / /_/ / /_/ / / / / / /
/____/\___/_/\__,_/\____/_/ /_/ /_/
-----------------------------------------
? ? ? ? ? ? ? ? ? ? ? ? ? ? @itest.info
test_get_method (test_req.TestAPI) ...
----------- Request ?? ---------------
url: http://httpbin.org/get? ? ? ? method: GET
----------- Response ??? -------------
type: json
{'args': {'key1': 'value1', 'key2': 'value2'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.22.0', 'X-Amzn-Trace-Id': 'Root=1-608a883c-7b355ba81fcd0d287566405a'}, 'origin': '183.178.27.36', 'url': 'http://httpbin.org/get?key1=value1&key2=value2'}
ok
----------------------------------------------------------------------
Ran 1 test in 0.619s
OK
通過日志/報(bào)告都可以清楚的看到。
請求的方法
請求 url
響應(yīng)的類型
響應(yīng)的數(shù)據(jù)
更強(qiáng)大的斷言
斷言接口返回的數(shù)據(jù)是我們在做接口自動(dòng)化很重要的工作。
assertJSON
接口返回結(jié)果如下:
{
? "args":{
? ? "hobby":[
? ? ? "basketball",
? ? ? "swim"
? ? ],
? ? "name":"tom"
? }}
我的目標(biāo)是斷言name 和 hobby 部分的內(nèi)容。seldom 可以針對JSON文件進(jìn)行斷言。
import seldom
class TestAPI(seldom.TestCase):
? ? def test_assert_json(self):
? ? ? ? payload = {'name': 'tom', 'hobby': ['basketball', 'swim']}
? ? ? ? self.get("http://httpbin.org/get", params=payload)
? ? ? ? assert_json = {'args': {'hobby': ['swim', 'basketball'], 'name': 'tom'}}
? ? ? ? self.assertJSON(assert_json)
運(yùn)行日志
test_get_method (test_req.TestAPI) ...
----------- Request ?? ---------------
url: http://httpbin.org/get? ? ? ? method: GET
----------- Response ??? -------------
type: json
{'args': {'hobby': ['basketball', 'swim'], 'name': 'tom'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.22.0', 'X-Amzn-Trace-Id': 'Root=1-608a896d-48fac4f6139912ba01d2626f'}, 'origin': '183.178.27.36', 'url': 'http://httpbin.org/get?name=tom&hobby=basketball&hobby=swim'}
?? Assert data has not key: headers
?? Assert data has not key: origin
?? Assert data has not key: url
ok
----------------------------------------------------------------------
Ran 1 test in 1.305s
OK
seldom 還會(huì)提示你還有哪些字段沒有斷言。
assertPath
接口返回?cái)?shù)據(jù)如下:
{
? "args":{
? ? "hobby":
? ? ? ["basketball","swim"],
? ? "name":"tom"
? }}
seldom 中可以通過 path 進(jìn)行斷言:
import seldom
class TestAPI(seldom.TestCase):
? ? def test_assert_path(self):
? ? ? ? payload = {'name': 'tom', 'hobby': ['basketball', 'swim']}
? ? ? ? self.get("http://httpbin.org/get", params=payload)
? ? ? ? self.assertPath("name", "tom")
? ? ? ? self.assertPath("args.hobby[0]", "basketball")
assertSchema
有時(shí)并不關(guān)心數(shù)據(jù)本身是什么,而是需要斷言數(shù)據(jù)的類型。 assertSchema 是基于 jsonschema 實(shí)現(xiàn)的斷言方法。
jsonschema: https://json-schema.org/learn/
接口返回?cái)?shù)據(jù)如下:
{
? "args":{
? ? "hobby":
? ? ? ["basketball","swim"],
? ? "name":"tom",
? ? "age":"18"
? }}
seldom 中可以通過利用jsonschema 進(jìn)行斷言:
import seldom
class TestAPI(seldom.TestCase):
? ? def test_assert_schema(self):
? ? ? ? payload = {"hobby": ["basketball", "swim"], "name": "tom", "age": "18"}
? ? ? ? self.get("/get", params=payload)
? ? ? ? schema = {
? ? ? ? ? ? "type": "object",
? ? ? ? ? ? "properties": {
? ? ? ? ? ? ? ? "args": {
? ? ? ? ? ? ? ? ? ? "type": "object",
? ? ? ? ? ? ? ? ? ? "properties": {
? ? ? ? ? ? ? ? ? ? ? ? "age": {"type": "string"},
? ? ? ? ? ? ? ? ? ? ? ? "name": {"type": "string"},
? ? ? ? ? ? ? ? ? ? ? ? "hobby": {
? ? ? ? ? ? ? ? ? ? ? ? ? ? "type": "array",
? ? ? ? ? ? ? ? ? ? ? ? ? ? "items": {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? "type": "string"
? ? ? ? ? ? ? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? },
? ? ? ? }
? ? ? ? self.assertSchema(schema)
是否再次感受到了 seldom 提供的斷言非常靈活,強(qiáng)大。
接口數(shù)據(jù)依賴
在場景測試中,我們需要利用上一個(gè)接口的數(shù)據(jù),調(diào)用下一個(gè)接口。
import seldom
class TestRespData(seldom.TestCase):
? ? def test_data_dependency(self):
? ? ? ? """
? ? ? ? Test for interface data dependencies
? ? ? ? """
? ? ? ? headers = {"X-Account-Fullname": "bugmaster"}
? ? ? ? self.get("/get", headers=headers)
? ? ? ? self.assertStatusCode(200)
? ? ? ? username = self.response["headers"]["X-Account-Fullname"]
? ? ? ? self.post("/post", data={'username': username})
? ? ? ? self.assertStatusCode(200)
seldom 提供了self.response用于記錄上個(gè)接口返回的結(jié)果,直接拿來用即可。
數(shù)據(jù)驅(qū)動(dòng)
seldom 本來就提供的有強(qiáng)大的數(shù)據(jù)驅(qū)動(dòng),拿來做接口測試非常方便。
@data
import seldom
from seldom import data
class TestDDT(seldom.TestCase):
? ? @data([
? ? ? ? ("key1", 'value1'),
? ? ? ? ("key2", 'value2'),
? ? ? ? ("key3", 'value3')
? ? ])
? ? def test_data(self, key, value):
? ? ? ? """
? ? ? ? Data-Driver Tests
? ? ? ? """
? ? ? ? payload = {key: value}
? ? ? ? self.post("/post", data=payload)
? ? ? ? self.assertStatusCode(200)
? ? ? ? self.assertEqual(self.response["form"][key], value)
@file_data
創(chuàng)建data.json數(shù)據(jù)文件
{
"login":[
? ? ["admin","admin123"],
? ? ["guest","guest123"]
]}
通過file_data實(shí)現(xiàn)數(shù)據(jù)驅(qū)動(dòng)。
import seldom
from seldom import file_data
class TestDDT(seldom.TestCase):
? ? @file_data("data.json", key="login")
? ? def test_data(self, username, password):
? ? ? ? """
? ? ? ? Data-Driver Tests
? ? ? ? """
? ? ? ? payload = {username: password}
? ? ? ? self.post("http://httpbin.org/post", data=payload)
? ? ? ? self.assertStatusCode(200)
? ? ? ? self.assertEqual(self.response["form"][username], password)
更過數(shù)據(jù)文件 (csv/excel/yaml),參考
隨機(jī)生成測試數(shù)據(jù)
seldom 提供隨機(jī)生成測試數(shù)據(jù)方法,可以生成一些常用的數(shù)據(jù)。
import seldom
from seldom import testdata
class TestAPI(seldom.TestCase):
? ? def test_data(self):
? ? ? ? phone = testdata.get_phone()
? ? ? ? payload = {'phone': phone}
? ? ? ? self.get("http://httpbin.org/get", params=payload)
? ? ? ? self.assertPath("args.phone", phone)
更過類型的測試數(shù)據(jù),參考
數(shù)據(jù)庫操作
seldom 支持 sqlite3、MySQL 數(shù)據(jù)庫操作。
sqlite3
MySQL
delete_data()
delete_data()
insert_data()
insert_data()
select_data()
select_data()
update_data()
update_data()
init_table()
init_table()
close()
close()
連接數(shù)據(jù)庫
連接 sqlit3 數(shù)據(jù)庫
from seldom.db_operation import SQLiteDB
db = SQLiteDB(r"D:\learnAPI\db.sqlite3")
連接 MySQL 數(shù)據(jù)庫(需要)
安裝 pymysql 驅(qū)動(dòng)
> pip install pymysql
鏈接
from seldom.db_operation import MySQLDB
db = MySQLDB(host="127.0.0.1",
? ? ? ? ? ? port="3306",
? ? ? ? ? ? user="root",
? ? ? ? ? ? password="123",
? ? ? ? ? ? database="db_name")
操作方法
delete_data
刪除表數(shù)據(jù)。
db.delete_data(table="user", where={"id":1})
insert_data
插入一條數(shù)據(jù)。
data = {'id': 1, 'username': 'admin', 'password': "123"},
db.insert_data(table="user", data=data)
select_data
查詢表數(shù)據(jù)。
result = db.select_data(table="user", where={"id":1, "name": "tom"})
print(result)
update_data
更新表數(shù)據(jù)。
db.update_data(table="user", data={"name":"new tom"}, where={"name": "tom"})
init_table
批量插入數(shù)據(jù),在插入之前先清空表數(shù)據(jù)。
datas = {
? ? 'api_event': [
? ? ? ? {'id': 1, 'name': '紅米Pro發(fā)布會(huì)'},
? ? ? ? {'id': 2, 'name': '可參加人數(shù)為0'},
? ? ? ? {'id': 3, 'name': '當(dāng)前狀態(tài)為0關(guān)閉'},
? ? ? ? {'id': 4, 'name': '發(fā)布會(huì)已結(jié)束'},
? ? ? ? {'id': 5, 'name': '小米5發(fā)布會(huì)'},
? ? ],
? ? 'api_guest': [
? ? ? ? {'id': 1, 'real_name': 'alen'},
? ? ? ? {'id': 2, 'real_name': 'has sign'},
? ? ? ? {'id': 3, 'real_name': 'tom'},
? ? ]
}
db.init_table(datas)
close
關(guān)閉數(shù)據(jù)庫連接。
db.close()
最后,基于 seldom 實(shí)現(xiàn)接口自動(dòng)化測試的項(xiàng)目:https://github.com/defnngj/pyrequest2