作者:皮皮哥
上一篇文章中,我們介紹了graphql的概念以及優劣勢。基於前一篇文章,我們就來聊一聊graphql應該怎麼測,它的官方python客戶端sgqlc該怎麼用,以及介紹一下我自己開發的graphy測試框架。
舉個例子
首先讓我們來看一個graphql請求與響應的例子。
請求:
query { MGetMainAccelerDomain(ids: ["1"]) { id name core { business_type is_main owner } distribute { items { domain { id name } } } }}響應:
{ "data": { "MGetMainAccelerDomain": [ { "id": "1", "name": "www.baidu.com", "core": { "business_type": "", "is_main": false, "owner": "" }, "distribute": { "items": [] } } ] }}我們會發現:
graphql雖然自由度很高,但還是定義了請求入口,在這裡是query下的MGetMainAccelerDomain,它的返回是AccelerDomain的數組。
它的自由度體現在,請求時可能要求這個對象的不固定的欄位(可以是全部,也可以只是必選)。
它的自由度還體現在,請求欄位可能有下級欄位繼續拓撲,甚至可能出現遞歸,例如這裡的domain.distribute.items[]又是domain類型了。
graphql測試思路
於是我們可以歸納graphql接口的測試思路:
預期的graphql測試框架
然後讓我們思考一下,一個合理的graphql測試框架應該需要哪些功能。
sgqlc:官方的python客戶端
sgqlc是graphql推薦的官方python客戶端,它的github地址是:https://github.com/profusion/sgqlc。在這裡我就用官方的例子來說明它的能力:
【爬取模型與生成模型類】
python3 -m sgqlc.introspection \--exclude-deprecated \--exclude-description \-H "Authorization: bearer ${GH_TOKEN}" \https://api.github.com/graphql \github_schema.json
sgqlc-codegen github_schema.json github_schema.py這樣生成的模型類就是github_schema了。
【構造請求與發送】
from sgqlc.operation import Operationfrom github_schema import github_schema as schema
op = Operation(schema.Query)
issues = op.repository(owner=owner, name=name).issues(first=100)issues.nodes.number()issues.nodes.title()issues.page_info.__fields__('has_next_page')issues.page_info.__fields__(end_cursor=True)
print(op)
data = endpoint(op)
repo = (op + data).repositoryfor issue in repo.issues.nodes: print(issue)【sgqlc的問題】
使用的過程中我遇到幾個問題,或者說不順手的地方:
graphy:自研的graphql測試框架
於是,我便開始嘗試自己開發一個自己覺得順手的graphql測試框架(using python),我給它起名graphy。讓我們先看一個demo。
【模型與生成】
python3 graphy/graphy_gen.py "http://10.227.10.13:7794/graphql"\ http_test/graphy_test/example.py生成的模型類如下:
入口類有Query和Mutation兩種。入口類中會用_fields來記錄它可以進行操作,以及對應的返回類型。
class Query(graphy.Query): _description = "xxx" _fields = { "FilterSubDomain" : "_listof_SubDomain", "GetSubDomain" : "SubDomain" } def __init__(self, schema=None, depth=None): self.FilterSubDomain : List[SubDomain] self.GetSubDomain : SubDomain return super().__init__(schema=schema, depth=depth)同理,一個節點類會使用_fields的靜態字典來記錄它具備的欄位/類型;同時也生成了__init__方法,會自動為對象賦予對應的欄位,以及變量類型。
class AuthInfo(graphy.Node): _description = "認證信息" _fields = { "id" : "int", "name" : "str", "description" : "str", "params" : "_listof_AuthParam", } def __init__(self, args=None): self.id : int self.name : str self.description : str self.params : List[AuthParam] return super().__init__(args=args)如果遇到了數組類型,我會用一個_listof_xxx的類型來描述:
class _listof_AuthParam(graphy.List): _description = "List type of AuthParam" _item_type_name = "AuthParam"同時也處理了INPUT,Enum等類型。
所有的類型都可以不斷向下拓撲,直到遇到了基礎類型,也就是STRING,INT,FLOAT,BOOL,ID;它們會被對應成python的基礎類型。
至此,我們解決了第一個問題,所有的graphql節點你都知道它的類型是什麼,並且在IDE中可以hint。
【構造請求】
query = Query(schema="http_test.graphy_test.example_schema", depth=1)query.MGetMainDomain({"ids": [79417]})print(query._g())
query { MGetMainDomain(ids: [79417]) { id name }}query = Query(depth=2) query.MGetMainDomain({"ids": [79417]})._d(2) #要求在這個節點下拓撲2層query.MGetMainDomain._f(["id"])query.MGetMainDomain._a({"ids": [79417]})query.MGetMainDomain({"ids": [79417]})query.MGetMainDomain._d(2)._a({"ids": [79417]})其他能力,例如Union,Variables等的使用,這裡就不再展開了。
【獲取響應並反序列化】
client = graphy_client.Client(url="xxx")
query = Query(schema="xxx", depth=2)domains = query.MGetMainDomain({"ids": [79417]})print(query._g())
client.send(query)
print(query)'''"MGetDomain":[ { "name": "www.baidu.com", "id": 171 }, ...]'''
print(domains[0].name)'''www.baidu.com'''至此,我們解決了第二個問題,就是響應結構體無法被反序列為對象的問題;我們還額外實現了,query這個對象即是請求對象,又是響應對象。
graphy的實現思路
這裡分享一下graphy的實現思路,僅供大家參考。
【對象初始化】
首先,在對象初始化時,graphy會為該對象維護兩種信息:
【請求與響應】
在實際請求時,一個請求的隱變量以及它的遞歸子節點,會被用來生成特殊格式的graphql請求字符串;當被測graphql接口返回後,這個響應的json字典,會被反序列化並回填到可見變量中。
這樣我們就實現了請求和響應使用同一個對象,並且外層並不需要感知這些,使用起來沒有負擔。
後續todo
graphy目前還是我們項目組自用的框架,由於代碼寫的有點醜陋目前還沒有提交給公司作為公共pip包;很多東西其實你不寫一個亂糟糟的第一版,你是不知道怎麼才能寫得更優雅的。
實際上還有一些graphql的特性我沒有支持,例如Aliases,Fragments,Directives等等。
由於graphy可以感知模型,並且請求和響應應該是全量並且一致的,所以graphql非常適合做測試代碼自動生成,以及流量錄製回放測試;這個我在後面也會進行嘗試。