什麼是Locust
Locust是一個基於Python的帶有視覺化圖形介面的測試工具。
本文有什麼
本人不是專業的測試人員,Python也是先學先用的,所以不會涉及到太多的相關專業知識。本文主要是分享自己學習使用Locust的收穫,基於官方文件和他人的部落格,結合自己的使用體驗,所以不是一篇教學或者專業文。你在這篇文章中看不到Locust的巢狀task和多locustfile測試等。
名詞解釋
locust:本意是蝗蟲,在測試過程中看成是一個使用者即可(後面所謂的HttpLocust只是帶有特殊性質的使用者)。
hatch rate:孵化率,表示每秒鐘產生的使用者數量,可以模擬一個逐漸增加的過程,可以根據曲線來考察介面的處理峰值。
TaskSet:顧名思義,是任務的集合,代表著每個使用者具有的行為,也就是你想要讓使用者對整個後臺功能做什麼。
max_wait/min_wait:最大等待時間/最小等待時間,這是使用者的休息時間,使用者在執行任務的過程中,在這兩個值確定的區間內隨機選擇一個時間進行休息,模擬真實使用者的緩衝,同時也可以保證某些帶有評率限制的介面能夠安全呼叫,否則可能會出現很多錯誤甚至被封IP等。
locustfile.py:檔名稱當然可以隨便定義,但是這個檔案的功能,相當於建立了一個使用者的模型,需要產生的大量使用者都將以這個檔案定義為原型。
locustfile
locust以這個指令碼作為使用者的原型孵化使用者,這個指令碼包括兩個部分,TaskSet子類和Locust子類。TaskSet子類是用於描述使用者行為的,提供on_start方法執行前置操作。Locust子類主要是使用HttpLocust子類,因為現在的測試主要是針對http的介面,當然也可以通過一些特別的模組實現websocket的測試(雖然感覺websocket的測試也沒有什麼必要)
Locust/HttpLocust
這個類主要定義使用者的目標(host),相當於一個baseURL,使用者的行為(請求)都會相對於這個目標進行。定義使用者的休息間隔(max_wait/min_wait)。定義使用者的行為(TaskSet)。一個普通的HttpLocust可能是以下這樣的:
class WebsiteUser(HttpLocust):
host = "http://juejin.im"
max_wait = 3000
min_wait = 3000
task_set = UserAction
複製程式碼
你還可以在這個類裡面通過類的靜態變數來提供一個所有使用者都能使用的變數。比如一個queue,一個list等。由於類的靜態變數的特性,每個使用者修改這個值可以實現一個遞增的index之類的需求,引數化的邏輯可以藉助這一點。
TaskSet
這個類負責定義每個使用者的實際動作,一個使用者可能有很多動作,註冊啦,買東西啦,撤銷啦等等。on_start方法是給每個使用者開始這些動作之前的一個前置準備,可以在這個方法中初始化一些東西,比如登入或者連結websocket。
TaskSet中可以通過self.client來使用HttpSession class,也就是使用者的http請求能力的供應者,可以理解成使用者的瀏覽器,用來傳送請求和接受響應的。self.client是自帶cookie和session的能力的,完全相當於一個瀏覽器環境。也就是說self.client傳送了一個請求後,比如登入請求,成功後再用這個self.client傳送別的請求,會帶上登入請求成功後伺服器返回的cookie。
TaskSet還提供了一種神奇的東西ResponseContextManager class,當self.client傳送的請求帶有catch_response=True的引數,像下面這樣:
with self.client.get("/", catch_response=True) as response:
if response.content == "":
response.failure("No data")
# response.success()
複製程式碼
一般的情況下使用這樣的即可:
response = self.client.get("/",data=data)
複製程式碼
這種普通的請求在響應狀態碼小於400的情況下都認為成功,然而現在大部分介面都是通過返回的資料來表達你的動作O不OK,這就需要ResponseContextManager通過success和failure來手動修正正確與錯誤的顯示。
TaskSet中self.locust可以取到HttpLocust的例項,也就是說你在HttpLocust中的某些變數,你是可以使用的。當然如何使用還是取決於你的測試邏輯。
有了以上這些東西,基本就可以開始定義自己的動作了。首先你需要引入locust提供的task裝飾器,TaskSet子類中的例項方法如果被task裝飾,那麼就會認為是一個需要執行的動作。@task(weight)是支援權重安排的,也就是說多項任務可以按照一定的頻繁度來呼叫,更接近使用者的某些實際體驗,比如購物過程中翻頁可能要比新增購物車頻繁。
一個普通的TaskSet像是這樣:
class ForumPage(TaskSet):
@task(100)
def read_thread(self):
pass
@task(7)
def create_thread(self):
pass
複製程式碼
雖然兩個任務什麼都沒幹,但是執行的頻率可以看出來誰更頻繁。沒有特殊的設定下,兩個任務按照權重隨機的順序執行,也就是有可能在短期內無法按照實際的權重執行,也就是說每個任務之間最好不要有順序以來(但是可以通過一些操作來保證順序)。
一個普通的task像是這樣:
@task
def test_login(self):
# 這裡通過類的靜態變數來控制一個全域性的變數,保證唯一的index
# WebsiteUser這就是Locust子類
WebsiteUser.index += 1
data = {
# 這裡組織你的傳送資料
# 通過self.locust來訪問所屬的Locust子類
"uid": self.locust.index
}
with self.client.post("/loginl", data=data,
catch_response=True) as response:
# response提供了一些快捷的方法和屬性,讓你判斷請求的情況
if response.ok:
# 如果響應的content是json格式的,可以幫你轉成字典
result = response.json()
# 通過返回的資料來手動判斷是否成功
if result['code'] == 200:
response.success()
else:
response.failure(result['msg'])
else:
response.failure(bytes.decode(response.content))
複製程式碼
我們跑起來
有了locustfile.py就可以開始跑起來了,有視覺化介面還是闊以的。命令列跑起來我就不多說了,瀏覽器開啟localhost:8089,會讓你輸入總使用者量和孵化率。加入你輸入200和1,就代表每一秒產生一個使用者,直到200個使用者都產生。注意:到達總量時統計資料會重置。
由於使用者的行為是儘可能模擬實際情況,所以考察資料的時候應當清楚一些。200個使用者,假設有兩個task,比重1:1,min_wait和max_wait都是1000ms,那麼理想情況下某一時刻,200個使用者都傳送了一個請求,且這個請求的響應時間可以忽略,那麼我們可以認為一秒的併發是200,等待時間是1s,故併發應當穩定在200。所以你需要考慮響應時間和等待時間,選擇合適的使用者數有可能才到達所需要的併發量。
我們需要滿足其他的需求
引數化
我們在測試一個網站的功能時,我們通過locust模擬的使用者需要不同的賬號來進行操作,因此我們需要在TaskSet中做一些改造。之前我們說過了TaskSet擁有on_start方法執行前置動作,且self.client可以保持cookie和session,這不剛好可以滿足登陸後執行操作的需求嗎。那麼下面一個問題就是如果使用不同的賬號,這使用Python也是比較容易實現的。我們在WebsiteUser中定義一個類靜態變數,比如下面這樣:
class WebsiteUser(HttpLocust):
host = "http://juejin.im"
max_wait = 3000
min_wait = 3000
task_set = UserAction
# 這個屬性在locust啟動時被初始化好
# 使用WebsiteUser.user_index來非競爭的更新,實現一個不重複的遞增序列
user_index = 0
複製程式碼
使用遞增序列的方式,可以快速註冊一大批使用者,只需要確定好使用者名稱和密碼如果使用這個index進行產生即可。當然,如果不想使用這樣的方式,還有一種就是使用資料結構,比如queue:
class WebsiteUser(HttpLocust):
host = 'http://juejin.im'
task_set = UserBehavior
user_data_queue = queue.Queue()
# 一共300w使用者準備
for index in range(1500000, 4500000):
data = {
# 準備你的註冊或者登陸資料
}
user_data_queue.put_nowait(data)
min_wait = 500
max_wait = 500
複製程式碼
@task
def test_register(self):
try:
# 用queue來使每個使用者取得不同的賬戶資訊
data = self.locust.user_data_queue.get()
except queue.Empty:
# 當然了,只出佇列會導致資料跑空
# 如果需要迴圈,用完的資料從佇列的另一頭塞回去就可以重複利用了
print('account data run out, test ended.')
exit(0)
with self.client.post('/register', data=data,
catch_response=True) as response:
result = response.json()
if result['code'] == 200:
response.success()
else:
response.failure(result['msg'])
複製程式碼
引數化的一些高階用法可以參見深入淺出開源效能測試工具Locust(指令碼增強)會給你不少啟發。
非http測試
少數情況我們想考量websocket的效能,比如同時連線的客戶端以及推送到接受的延遲情況。Python強大的模組庫肯定是有websocket相關的,通過搜尋和嘗試,pip2安裝的websocket模組和pip3的好像不一樣,使用pip3安裝的實踐成功。
一般來說我們在on_start方法中初始化連線:
self.ws = websocket.WebSocket()
self.ws.connect("ws://xxx")
複製程式碼
並且將ws例項作為例項屬性方便後續的操作。注意:websocket連線端在測試過程中斷掉也是極有可能的,所以提供一個重連的方法可能會有必要。
有了ws的連線之後呢,一般需要傳送訂閱資訊讓伺服器來主動推送資料,如果ws在接受了訂閱後會先返回一條訂閱的結果,如果在視覺化介面上通過資料統計出來,這就需要locust提供的hook。
from locust import TaskSet, task, HttpLocust, events
將events匯入進來,然後觸發一些事件,皆可以讓locust統計相應的資料:
events.request_success.fire(
request_type="wss",
name="send_entrust",
response_time=100,
response_length=300
)
複製程式碼
request_type相當於請求的型別,可以隨便填。name相當於請求url,response_time相當於響應事件,response_length相當於響應體大小。實際上self.client.post()等也會使用事件觸發來統計資料。比如ResponseContextManager中success的實現:
def success(self):
"""
Report the response as successful
Example::
with self.client.get("/does/not/exist",
catch_response=True) as response:
if response.status_code == 404:
response.success()
"""
events.request_success.fire(
request_type=self.locust_request_meta["method"],
name=self.locust_request_meta["name"],
response_time=self.locust_request_meta["response_time"],
response_length=self.locust_request_meta["content_size"],
)
self._is_reported = True
複製程式碼
這樣的話,設計一個task來傳送訂閱並接受推送。作為前端人員來說,websocket都是用回撥來處理推送,那麼Python裡面的websocket怎麼辦呢?實踐成功的websocket模組連線後,也就是從connect()之後,self.ws本身是一個可迭代的,雖然每次next都是呼叫resv()方法,在迴圈裡就好使。而self.ws.resv()這個函式是很神奇的,呼叫開始後他會等到接受推送了再返回,巨集觀角度來說這個函式的執行時間,應當等於伺服器推送的間隔,一次可以判斷推送是否出現了延遲。且這個函式如果返回的是空字串,大致可以認為連線斷掉,邏輯上增加統計和重連,能夠保證一些測試準確度。 給出一個我實際使用的task:
@task
def test_ws(self):
# 這裡可以使用這一個task來完成傳送訂閱和處理訂閱
if not self.send_flag:
self.ws.send("""{
# 這裡組織你需要傳送的訂閱資料
}
""")
result = json.loads(self.ws.recv())
# 這裡來處理訂閱的結果,如果訂閱成功改變標誌位,讓這個task只執行接受
if result['code'] == 200:
self.send_flag = True
events.request_success.fire(
request_type="wss",
name="send_entrust",
response_time=100,
response_length=300)
else:
events.request_failure.fire(
request_type="wss",
name="send_entrust",
response_time=100,
exception=Exception(self.ws.status),
)
else:
flag = True
# 由於推送是不間斷的直到傳送取消訂閱,故使用死迴圈來不斷統計
while flag:
# self.ws本身是個迭代器,next和呼叫resv()是一樣的,都可以
start_time = time.time()
resv = next(self.ws)
# 推送間隔就可以當做統計資料進行顯示
total_time = int((time.time() - start_time) * 1000)
if resv != '':
events.request_success.fire(
request_type="wss",
name="resv_entrust",
response_time=total_time,
response_length=59)
else:
# 如果中斷了可以增加重連方案
events.request_failure.fire(
request_type="wss",
name="resv_entrust",
response_time=total_time,
exception=Exception(""),
)
flag = False
else:
print("no resv")
複製程式碼
總結
Locust總體來說還是比較容易上手的,提供的功能大概能夠滿足一般的測試需求。實踐的過程中遇到過一些些問題,比如請求的數量不一定在頁面上統計的及時,使用者數也有可能會斷掉一兩個,具體原因不明。本人也不是專門的測試人員, 也不是Python native speaker,所以對於Python的理解和對測試分析的觀點不一定對,只是分享一下我在使用的過程中如何思考問題如何解決問題,解決方案不一定最優,也有可能會造成很多困擾,但這是我的學習過程,想和大家分享,願意和大家討論並繼續學習。