前言
之前看過相關的測試資料準備的文章,坦白說,看完之後,能記住只有2個:
- api準備資料;
- 資料庫插入;
而最近,同學恰好也問到這問題:
當時回覆就如上面的答覆,現在回頭想想,的確沒想到好的方案,在腦海裡,有一個所謂的"終極方案",就是讀取介面文件,自動生成測試資料,理論上可行,但一直沒去做,懶;
有啥辦法
做過單元/介面測試的同學都知道,其中有一個環節就是測試資料準備
,而這一步是不可或缺的一步,也是需要花費大量時間投入的一步;
測試介面前就必須準備好該介面需要處理的資料,而資料又有可能依賴其他的資料,這就提高了準備資料的複雜度與難度;
那到底有什麼辦法?
- 基於GUI操作生成測試資料;
- 基於API呼叫生成測試資料;
- 基於資料庫操作生成資料;
- 基於第三方庫自建資料;
- 結合多種方式生成資料;
- 匯入線上/測試資料;
GUI操作生成資料
基於GUI操作生成資料,是指使用自動化指令碼或者人工執行業務流程生成資料。
現在需要測試登入功能,這就需要準備一個已經註冊的使用者,
此時,可以通過GUI操作來建立一個使用者(無論是手工還是自動化指令碼),
然後再用新建的使用者測試登入;
複製程式碼
這種方式簡單直接,並且資料來源於真實的業務流程,一定程度保證了資料的準確性。
然而,缺點也很明顯:
- 建立資料的效率低:每次的GUI操作只生成一條資料,並且操作非常耗時;
- 易封裝:通過GUI操作生成資料的過程,其實就是在開發自動化case的過程,加大了工作量;
- 成功率不高:GUI的變化直接會導致資料生成失敗;
- 引入了其他依賴:資料生成成功的前提,依賴於業務流程的正確性。
一般情況下,基本不會使用這種方式生成資料,除非沒有其他更好的方式來建立可靠的資料。
不過,操作GUI生成資料是其他兩種方式API呼叫和運算元據庫的基礎,因為可以知道一條測試資料建立的過程;
API呼叫生成資料
實際上使用GUI操作生成資料,本質上就是在呼叫API。
使用GUI介面註冊使用者時,實際上呼叫了createUser的API。
複製程式碼
要注意的是,一次GUI操作可能呼叫了多個API,一般情況下,都把呼叫API生成資料的過程封裝成資料準備函式;
也許會有疑問,到底要怎樣才知道呼叫了哪些api?
- 直接問開發;
- 看原始碼;
- 模擬一遍,抓包;
這種方式優勢在於:
- 保證資料準確性;
- 執行效率高;
- 封裝成函式更靈活可控;
這種方式也不是十全十美,缺點在於:
- 並不是所有資料建立都有對應的API;
- 業務很複雜的情況下,需要呼叫多個API,增加複雜性;
- 需要海量資料時,即使使用了併發,效率也盡如人意;
- API依賴性;
因此,業界往往還會通過資料庫的CRUD操作生成測試資料;
資料庫操作生成資料
資料庫生成資料一般做法是,將建立資料需要的SQL封裝成函式,然後再進行呼叫。
這樣就能直接通過資料庫操作,將測試資料插入系統資料庫。
還是使用者登入,直接往userTable和userRoleTable兩張表插入資料,即可完成註冊。
複製程式碼
這樣做的前提是,需要知道修改了哪些資料庫業務表;
這種方式的優勢在於:
- 效率高,能在短時間內生成批量資料;
缺陷也很明顯:
- 維護成本高,當涉及到很多張表的時候,封裝的資料準備函式就需要大量時間來維護;
- 資料容易缺失,一個業務操作設計到的表往往不止一張,容易遺漏;
- 健壯性差,SQL語句變化時,封裝的函式必須實時同步更新,維護成本很高;
第三方庫生成資料
這種方式就比較直接,直接使用程式碼封裝成函式生成資料。
拿python為例,可以自己結合random()之類的函式隨機生成資料,還可以使用faker這樣的第三方庫:
from faker import Factory
fake = Factory().create('zh_CN')
def random_phone_number():
'''隨機手機號'''
return fake.phone_number()
def random_name():
"""隨機姓名"""
return fake.name()
def random_address():
"""隨機地址"""
return fake.address()
def random_email():
"""隨機email"""
return fake.email()
複製程式碼
結合多種方式來生成數
實際上,實際應用中都採用多種方式相結合的方式生成測試資料。
最典型的應用場景是,先通過API呼叫或者第三方庫生成基礎的測試資料,然後使用資料庫的CRUD操作生成符合特殊需求的資料。
比如:
# 註冊新使用者並進行綁卡
1. 使用封裝的faker庫隨機生成姓名,手機號,郵箱等資訊,並呼叫createUser API進行註冊;
2. 查詢userTableb表獲得使用者名稱,然後呼叫bindCard API實現綁卡。
其中,bindCard API中使用的userID即為上一步createUser API中產生的使用者ID;
3. 如有需要,通過資料庫操作更新其他資訊。
複製程式碼
以上就是一個常用的建立測試資料的過程;
當然也可以在測試用例執行前通api建立資料,執行後清除資料的方式;
匯入線上/測試資料
這個就是直匯入線上/測試資料,優點是更加貼近使用者,出現問題,可直接模擬,但一般都不提供這種方式,就不細說了;
資料建立時機
準備測試資料的時候,都有什麼痛點?
- 耗時長,導致用例執行時間長;
- 執行測試時可能會出現原先資料被修改而無法複用的情況;
- 環境不穩定導致資料異常;
正因上面的原因,資料準備不能隨時進行,因為,建立時機很重要;
實時建立
指測試用例時實時建立需要的測試資料,所有資料都必須在測試用例開始前實時準備,比如api方式;
優點:
- 不依賴測試用例外的資料;
- 保證資料的準確性和可控性;
缺點:
- 耗時長;
- 維護成本高;
- 資料存在複雜關聯性;
- 依賴性;
提前建立
指在準備測試環境時就預先將需要的資料提前準備好,比如資料庫插入;
優點:
- 節省用例執行時間;
- 不會因為環境問題導致資料無法建立;
缺點:
- 髒資料;
所謂的髒資料,是指資料在被實際使用前,已經被進行了非預期的修改;
而髒資料可能的來源是:
- 被其他使用,並修改了狀態;
- 手工測試時不小心修改了資料;
- 除錯過程修改了資料;
如何解決:
- 維護一份資料,執行後復原;
- 資料分類,不同資料區段來分配使用物件,比如0-100是A團隊,100-200是B團隊,通過流程保證;
該方式不適用於只能一次性使用的場景;
如何抉擇
穩定不常變化的資料,或是公用資料,建議使用提前建立的方式(資料庫),一般來說,適用於介面測試環節;
只能一次性使用,或經常變化的資料,又因環境不一致,建議使用實時建立的方式(API);
一般來說,介面測試就是用實時建立的方式,用例執行前構造資料,執行後清除資料,這樣就能儘可能保證用例之間相互不影響,也避免髒資料的產生;
複製程式碼
適用場景
一般來說,介面測試,都用
資料準備的方法
大多數採用的方法
大多數企業採用的方法就是,將測試資料準備的操作封裝成函式;
舉個例子:
def post(self,url,data,code,msg):
resp= requests.post(readconfig('url', 'url')+url, data=data)
self.assertEqual(200, resp.status_code)
self.assertEqual(code, resp.json()['code'])
self.assertIn(msg, json.dumps(resp.json()['msg']).decode("unicode-escape"))
return resp
複製程式碼
這樣就可以把資料建立相關操作封裝成函式,業務方只需要直接呼叫函式即可;
但,致命的問題是,引數非常多,也非常複雜;如上面的例子,就需要4個引數,而實際工作,可能會多達十幾個;
而絕大部分情況下,只需要個別引數,其他引數可以使用預設值即可;
那樣,程式碼就會演變成這樣:
def xx(A='',B=True,C="xx"):
...
return jbtest(A,B,C)
def xxx():
...
return jbtest(A,B,C)
def xxxx(A=''):
...
return jbtest(A,B,C)
複製程式碼
這樣封裝,對於一些常用的資料組合,可以通過一次呼叫就生成需要的資料;
對於不常用的資料,可以直接調預設的函式來建立,這樣就可以更靈活處理;
但是,也有弊端:
- 引數越多,封裝的函式數量隨之增加,最終可能演變成上百個函式;
- 可維護性差,底層函式會影響所有封裝的函式,動一發而牽全身;
大公司怎麼玩
既然上面的方法有問題,那能否優化下?同時,大公司怎麼玩?
想想老東家,談不上是小公司,但基本也是用上面的方式,前段時間看了茹炳晟老師也有提及到這點,就是引入Builder Pattern
封裝方式;
Builder Pattern
基本概念
到底什麼是Builder Pattern
,翻譯過來是建造者模式,目的就是將一個物件的構建與它的表示分離,使得同樣的構建過程可以建立不同的表示;
沒看懂?直接來例子:
不引入Builder Pattern,買車的條件有產地,座位數,油耗:
Car.buy(Country="",Seats="",FuelConsumption="")
隨著條件越多,傳參隨之增加;
引入Builder Pattern:
例子1:買一輛車,沒其他要求:
Car.buy();
例子2:買一輛車,中國產的:
Car.withBuildCountry("China").buy();
例子3:買一輛車,中國產的,7座的:
Car.withBuildCountry("China").withSeats("Seven").buy();
複製程式碼
明白了嗎?核心就是在使用者不知道物件的建造過程和細節的情況下,可以直接建立物件;
這3個例子,可以反向說明解決了什麼問題:
- 方便使用者建立物件時,不需要知道實現過程,只需要給出指定物件的型別和內容即可;
- 程式碼複用性 & 封裝性,將構建過程和細節進行封裝;
1. 工廠(建造者模式):負責製造汽車(組裝過程和細節在工廠內)
2. 汽車購買者(使用者):你只需要說出你需要的型號(物件的型別和內容),然後直接購買就可以使用了
(不需要知道汽車是怎麼組裝的(車輪、車門、發動機、方向盤等等))
複製程式碼
結構圖
組成
建造者模式包含如下角色: Builder:抽象建造者 ConcreteBuilder:具體建造者 Director:指揮者 Product:產品角色
職責
角色 | 職責 |
---|---|
Builder | 建立一個Product物件的各個部件指定抽象介面 |
ConcreteBuilder | 實現Builder的介面以構造和裝配該產品的各個部件,定義並明確它所建立的表示,提供一個檢索產品的介面; |
Director | 構造一個使用Builder介面的物件; |
Product | 表示被構造的物件,包含定義組成部件的類; |
換種說法
- 指揮者(Director)直接和客戶(Client)進行需求溝通;
- 溝通後指揮者將客戶建立產品的需求劃分為各個部件的建造請求(Builder);
- 將各個部件的建造請求委派到具體的建造者(ConcreteBuilder);
- 各個具體建造者負責進行產品部件的構建;
- 最終構建成具體產品(Product)。
優點
- 將一個物件分解為各個元件,相對獨立,不受影響;
- 將物件元件的構造封裝起來,客戶端不需要知道內部細節;
- 可以控制整個物件的生成過程;
缺點
- 對不同型別的物件需要實現不同的具體構造器的類,這可能大大增加類的數量;
- 使用範圍受限制,只適用於產品組成功能相似的產品,即可複用;
什麼時候適用建造者模式
- 生成的產品物件有複雜的內部結構;
- 生成的產品物件的屬性相互依賴,建造者模式可以強迫生成順序;
- 在物件建立過程中會使用到系統中的一些其它物件,這些物件在產品物件的建立過程中不易得到;
例子1-微信公眾號訊息推送
相信大家在使用微信時,也都收到過訊息推送吧,來看看官網提供的一個例項:
{
"touser":"OPENID",
"template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
"url":"http://weixin.qq.com/download",
"miniprogram":{
"appid":"xiaochengxuappid12345",
"pagepath":"index?foo=bar"
},
"data":{
"first":{
"value":"恭喜你購買成功!",
"color":"#173177"
},
"keynote1":{
"value":"巧克力",
"color":"#173177"
},
"keynote2":{
"value":"39.8元",
"color":"#173177"
},
"keynote3":{
"value":"2014年9月22日",
"color":"#173177"
},
"remark":{
"value":"歡迎再次購買!",
"color":"#173177"
}
}
}
複製程式碼
具體引數請自行到公眾號開發平臺查詢,這裡思考的是,怎麼設計通用模板?
方法很多,但是這裡給出建造者模式的做法,建立builder類(為了方便,去掉了miniprogram引數)::
# -*- coding: utf-8 -*-
from collections import OrderedDict
import json
# 模版中“data”節點的各個元素的資料結構
class Metadata:
def __init__(self, value, color):
self.value = value
self.color = color
# 微信訊息的建造器
class MessageBuilder:
__contentDict = OrderedDict() # 定義整個模版的資料結構,保持新增的順序
__dataDict = OrderedDict() # 定義data節點的資料結構,保持新增的順序
__dataNoteNext = 1 # data節點要新增的下一個元素的序號
def __init__(self, touser, template_id, url):
self.__contentDict['touser'] = touser
self.__contentDict['template_id'] = template_id
self.__contentDict['url'] = url
self.__contentDict['data'] = self.__dataDict
def add_first_data(self, value, color):
data = Metadata(value, color)
self.__dataDict['first'] = data
return self
def add_remark_data(self, value, color):
data = Metadata(value, color)
self.__dataDict['remark'] = data
return self
def add_note_data(self, value, color):
data = Metadata(value, color)
self.__dataDict['keynote' + str(self.__dataNoteNext)] = data
self.__dataNoteNext += 1
return self
def build(self):
# 為列印出來看的方便,這裡將json序列化後的結果縮排2個空格,並且不把中文轉為unicode
return json.dumps(self.__contentDict, default=lambda o: o.__dict__, indent=2, ensure_ascii=False)
複製程式碼
有兩點要說明下:
- 建造者內部的字典採用OrderedDict,是為了保持順序與微信示例一致;
- 建造者每個方法都返回了本物件的引用;
建造者有了,就來生成訊息吧,想起上幾天fc的通知:
模擬作如上兩條微信訊息:
if __name__ == '__main__':
pickup_builder = MessageBuilder('jb', 'template_id_pickup', '') \
.add_first_data('您有一個快遞在蜂巢櫃裡等你來取哦!', '#173177') \
.add_note_data('123456', '#173177') \
.add_note_data('jb快遞', '#173177') \
.add_note_data('789456123', '#173177') \
.add_note_data('15914255XXX', '#173177') \
.add_note_data('廣州', '#173177') \
.add_remark_data('元宵節快到了,人不在家,也要把愛寄回家~', '#173177')
print('生成取件通知微信訊息')
print(order_builder.build())
print()
takeout_builder = MessageBuilder('user222222', 'template_id_takeout', '') \
.add_first_data('您的包裹已被取出啦', '#173177') \
.add_note_data('jb快遞', '#173177') \
.add_note_data('78954', '#173177') \
.add_note_data('15914255XXX', '#173177') \
.add_note_data('廣州', '#173177') \
.add_remark_data('點選詳情檢視物流進度', '#173177')
print('生成取出微信訊息')
print(send_builder.build())
複製程式碼
這樣看下來,是不是程式碼清晰多了,而且可複用,好像很不錯的感覺~
例子2-組建身體
該例子來源於此處:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import abc
class Builder(object):
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def create_header(self):
pass
@abc.abstractmethod
def create_body(self):
pass
@abc.abstractmethod
def create_hand(self):
pass
@abc.abstractmethod
def create_foot(self):
pass
class Thin(Builder):
def create_header(self):
print '瘦子的頭'
def create_body(self):
print '瘦子的身體'
def create_hand(self):
print '瘦子的手'
def create_foot(self):
print '瘦子的腳'
class Fat(Builder):
def create_header(self):
print '胖子的頭'
def create_body(self):
print '胖子的身體'
def create_hand(self):
print '胖子的手'
def create_foot(self):
print '胖子的腳'
class Director(object):
def __init__(self, person):
self.person = person
def create_preson(self):
self.person.create_header()
self.person.create_body()
self.person.create_hand()
self.person.create_foot()
if __name__=="__main__":
thin = Thin()
fat = Fat()
director_thin = Director(thin)
director_fat = Director(fat)
director_thin.create_preson()
director_fat.create_preson()
複製程式碼
上面類的設計如下圖,
指揮者Director 呼叫建造者Builder的物件,具體的建造過程是在Builder的子類中實現的;
回到正文,理解完建造者模式
,突然發現,好像跟上面的封裝概念相似的?
def xx(A='',B=True,C="xx"):
...
return jbtest(A,B,C)
def xxx():
...
return jbtest(A,B,C)
def xxxx(A=''):
...
return jbtest(A,B,C)
複製程式碼
是的,Builder Pattern
也是封裝方式,一般來說,會基於原有的封裝再二次封裝,這樣的好處就是業務方無需關心內部邏輯,營造用的好爽的感覺,而Builder Pattern
內部還是使用api或者資料庫的方式來創造資料,只是進行易用性封裝而已;
對業務方來說,是用的爽,對於維護者來說,苦的一逼,詳情請看上面的缺點,簡單就是維護成本高,容易出現動一發而牽全身,一般來說,只有大廠才會做這事;
平臺化
建造者模式是一種設計的思路,因此可適用於不同語言,但不同公司使用的語言不一樣,有Java、Python、php等等,因此,同一套程式碼,不同環境,就不適用了;
因此,解決這問題的核心在於封裝成api,並且結合GUI介面,做成平臺的形式,也就是所謂的測試資料平臺;
但目前來看,業界沒看到類似開源的例子,可能都是內部使用;
憧憬
雖然建立資料越來越方便了,但每次都需要建立資料,部分可能還是重複資料;
能否建立前先搜尋,如果有符合條件的資料,直接返回,沒有再建立資料,這樣的話,測試資料也會越來越龐大,便於平臺化後的資料複用;
不過,這只是想而已,目前來說,jb自認沒這能力寫搜尋邏輯,但一直希望,讓自動化更自動;
比如介面測試,可以直接解析介面文件,根據每個欄位型別,自動生成資料,這樣連資料建立都不需要了;
複製程式碼
小結
本文主要介紹資料建立相關的內容,大部分在資料建立,有兩種方法:
- 直接使用暴露全部引數的資料準備函式,好處是靈活,弊端是每次呼叫前都需要準備大量資料;
- 使用封裝函式,會更加靈活,但是可維護性差;
因此會引入建造者模式
的概念,本質上也是使用api跟運算元據庫兩種方式來建立資料,只是基於原來的封裝再進行二次易用性封裝,優點在於業務方可以快速生成需要的資料;
並且介紹了後續平臺化的想法,以及個人的一些憧憬;
建造者模式不是萬能的,依然對使用場景有限制,用的不好,就會導致易用性差的情況;
溫馨提示,當用例執行完畢,需要把公共資料復原,儘可能減少對其他業務方的干擾;
如果需要記錄使用的資料,可單獨把測試過程的資料入庫,以便後面出現問題後有記錄復現跟進;
最後,謝謝大家~