性能測試工具Locust–(2)編寫locustfile
- 2020 年 3 月 4 日
- 筆記
locustfile是普通的python文件。唯一的要求是至少聲明一個類(我們稱之為locust類),該類繼承自locust類。
Locust類
一個Locust類代表一個用戶(或者一個集群Locust)。Locust將為每個正在模擬的用戶生成(孵化)一個Locust類實例。Locust類通常應該定義一些屬性。
task_set屬性
task_set屬性應該指向一個TaskSet類,這個類定義了用戶的行為,下面將對其進行更詳細的描述。
wait_time屬性
除了task_set屬性,還應該聲明一個wait_time
方法。它用於確定模擬用戶在執行任務之間將等待多長時間。Locust提供了一些內置的函數,返回一些常用的wait_time方法。
最常見的是 between
。它用於使模擬用戶在每次執行任務後等待介於最小值和最大值之間的隨機時間。其他內置的等待時間函數是constant
和constant_pacing
。
使用以下locustfile,每個用戶將在任務之間等待5到15秒:
from locust import Locust, TaskSet, task, between class MyTaskSet(TaskSet): @task def my_task(self): print("executing my_task") class User(Locust): task_set = MyTaskSet wait_time = between(5, 15)
wait_time方法應該返回秒數(或幾分之一秒),也可以在TaskSet類上聲明,在這種情況下,它將僅用於該TaskSet。
也可以直接在Locust或TaskSet類上聲明自己的wait_time方法。接下來的Locust類將開始休眠1秒鐘,然後休眠1秒,2秒,3秒,等等。
class MyLocust(Locust): task_set = MyTaskSet last_wait_time = 0 def wait_time(self): self.last_wait_time += 1 return self.last_wait_time
weight (權重)屬性
如果文件中存在多個locust類,並且命令行中沒有指定locust,則每個新生成的locust將從現有locust中隨機選擇。否則,你可以從相同的文件中像下面這樣指定使用哪個locust: $ locust -f locust_file.py WebUserLocust MobileUserLocust
如果你想讓這些locust中的一個執行得更頻繁,你可以給這些類設置一個權重屬性。例如,Web用戶是mobile用戶的三倍:
class WebUserLocust(Locust): weight = 3 ... class MobileUserLocust(Locust): weight = 1 ...
host屬性
host屬性host屬性是要載入的URL前綴(https://cn.bing.com);通常,是在Locust的Web UI或命令行中指定的,在啟動Locust時使用--host
。 如果在locust類中聲明了一個host屬性,則在命令行或Web請求中未指定--host
的情況下將使用該屬性。。
TaskSet類
如果Locust類代表蝗蟲群,則可以說TaskSet類代表蝗蟲的大腦。每個Locust類必須設置一個task_set屬性,該屬性指向TaskSet。
顧名思義,TaskSet是任務的集合。這些任務是普通的python可調用對象,並且,如果我們正在對拍賣網站進行負載測試,則可以完成諸如「載入起始頁」,「搜索某些產品」和「競標」之類的工作。
啟動負載測試時,派生的Locust類的每個實例將開始執行其TaskSet。接下來的情況是每個TaskSet將選擇一個任務並調用它。然後,它將等待Locust類的wait_time方法指定的秒數(除非已直接在TaskSet上聲明了wait_time方法,在這種情況下,它將使用自己的方法)。 然後它將再次選擇要調用的新任務,再次等待,依此類推。
聲明任務
TaskSet聲明任務的典型方式是使用task
裝飾器。
這裡有一個例子:
from locust import Locust, TaskSet, task class MyTaskSet(TaskSet): @task def my_task(self): print("Locust instance (%r) executing my_task" % (self.locust)) class MyLocust(Locust): task_set = MyTaskSet
@task使用可選的weight參數,該參數可用於指定任務的執行率。在以下示例中,task2的執行量是task1的兩倍:
from locust import Locust, TaskSet, task from locust.wait_time import between class MyTaskSet(TaskSet): wait_time = between(5, 15) @task(3) def task1(self): pass @task(6) def task2(self): pass class MyLocust(Locust): task_set = MyTaskSet
tasks屬性
使用@task裝飾器來聲明任務是一種方便的方法,通常也是最好的方法。但是,也可以通過設置tasks
屬性來定義TaskSet的任務(使用@task裝飾器實際上只填充tasks屬性)。
tasks屬性要麼是python可調用項的列表,要麼是 字典。這些任務是接收一個參數的python可調用函數——正在執行任務的TaskSet類實例。 這些任務是接收一個參數的python可調用函數——正在執行任務的TaskSet類實例。
from locust import Locust, TaskSet def my_task(l): pass class MyTaskSet(TaskSet): tasks = [my_task] class MyLocust(Locust): task_set = MyTaskSet
如果將tasks屬性指定為列表,那麼每次執行任務時,都將從tasks屬性中隨機選擇該任務。但是,如果任務是字典(將可調用對象作為鍵,將整數作為值),則將隨機選擇要執行的任務,但將int值作為比率。因此,任務看起來像這樣: {my_task: 3, another_task: 1}
my_task被執行的可能性是other_task的3倍。
TaskSets可嵌套
TaskSet的一個非常重要的特性是它們可以嵌套,因為真實的網站通常以分層的方式構建,包含多個子部分。因此,嵌套任務集將允許我們定義一種行為,以更現實的方式來模擬用戶。例如,我們可以使用以下結構定義· – —– TaskSet:
- Main user behaviour
- Watch movie
- Filter movies
- Read thread
- Reply
- New thread
- View next page
- Index page
- Forum page
- Browse categories
- About page
嵌套TaskSet的方式就像使用task屬性指定任務時一樣,但不是引用python函數,而是引用另一個TaskSet:
class ForumPage(TaskSet): @task(20) def read_thread(self): pass @task(1) def new_thread(self): pass @task(5) def stop(self): self.interrupt() class UserBehaviour(TaskSet): tasks = {ForumPage:10} @task def index(self): pass
因此,在上面的示例中,如果在執行UserBehaviour TaskSet時選擇了要執行的ForumPage,則ForumPage TaskSet將開始執行。然後,ForumPage TaskSet將選擇其自己的任務之一,執行它,等待,依此類推。
關於上述示例,需要注意一點,那就是在ForumPage的stop方法中調用self.interrupt()。這樣做實際上是停止執行ForumPage任務集,並在UserBehaviour實例中繼續執行。如果我們在ForumPage的某處沒有調用interrupt()方法,Locust將永遠不會停止運行已經啟動的ForumPage任務。但是通過使用中斷功能,我們可以與任務權重一起定義模擬用戶離開論壇的可能性。
也可以使用@task裝飾器在類中內聯聲明嵌套的TaskSet,就像聲明普通任務時一樣:
class MyTaskSet(TaskSet): @task class SubTaskSet(TaskSet): @task def my_task(self): pass
引用Locust實例,或父TaskSet實例
TaskSet實例的屬性locust指向它的locust實例,屬性parent指向它的父TaskSet(它將在基本TaskSet中指向Locust實例)。
TaskSequence類
TaskSequence類是一個TaskSet,但是它的任務是按順序執行的。 要定義此順序,您應該執行以下操作:
@seq_task(1) def first_task(self): pass @seq_task(2) def second_task(self): pass @seq_task(3) @task(10) def third_task(self): pass
在上面的示例中,執行順序定義為先執行first_task,然後執行second_task,最後執行Third_task 10次。可以看到,可以使用@task裝飾器組合@seq_task,當然也可以將TaskSet嵌套在TaskSequences中,反之亦然。
Setups, Teardowns, on_start, 和on_stop
Locust還支援Locust級setup
和teardown
,TaskSet級setup
和teardown
,TaskSet
'、 on_start
和on_stop
.
Setups 和 Teardowns
setup
和teardown
,無論是在Locust
還是TaskSet
上運行,都是只運行一次的方法。setup
是在任務開始運行之前運行,而teardown
是在所有任務完成並退出Locust之後運行的。這使你能夠在Locust任務運行之前執行一些準備工作(如創建資料庫),並在Locust退出之前進行清理(如刪除資料庫)。
要使用它,只需在Locust或TaskSet類上聲明一個setup或teardown。這些方法將為你運行。
on_start和on_stop方法
TaskSet類可以聲明on_start
方法或on_stop
方法。 當模擬用戶開始執行該TaskSet類時,將調用on_start
方法;而當TaskSet停止時,將調用on_stop <locust.core.TaskSet.on_stop()
方法。
事件順序
由於許多設置和清除操作是相互依賴的,因此以下是它們的執行順序:
- Locust setup (一次)
- TaskSet setup (一次)
- TaskSet on_start (每個locust一次)
- TaskSet tasks…
- TaskSet on_stop (每個locust一次)
- TaskSet teardown (一次)
- Locust teardown (一次) 通常,setup和teardown方法應該是互補的。
發送HTTP請求
到目前為止,我們僅介紹了Locust用戶的任務調度部分。為了實際測試系統,我們需要發送HTTP請求。為了幫助我們做到這一點,存在HttpLocust
類。當使用這個類時,每個實例獲得一個client屬性,該屬性將是HttpSession的一個實例,可用於發送HTTP請求。
HttpLocust類
表示一個HTTP「用戶」,該「用戶」將被孵化並攻擊要載入測試的系統。 此用戶的行為由task_set屬性定義,該屬性應指向TaskSet
類。 該類在實例化時創建一個client屬性,該屬性是一個HTTP client ,支援在請求之間保持用戶會話。
client= None
在Locust實例化後創建的HttpSession實例。客戶端支援cookie,因此在HTTP請求之間的會話。
在繼承HttpLocust類時,我們可以使用它的client屬性對伺服器發出HTTP請求。 這是一個Locust文件的例子,可以用兩個URL負載測試站點 ;/ 和 /about/:
from locust import HttpLocust, TaskSet, task, between class MyTaskSet(TaskSet): @task(2) def index(self): self.client.get("/") @task(1) def about(self): self.client.get("/about/") class MyLocust(HttpLocust): task_set = MyTaskSet wait_time = between(5, 15)
使用上面的Locust類,每個模擬用戶將在請求之間等待5到15秒,並且 / 被請求的時間將是 /about/ 的兩倍。 細心的讀者會發現奇怪的是,我們可以使用TaskSet中的self.client而不是self.locust.client來引用HttpSession實例。我們可以這樣做是因為TaskSet
類有一個方便的屬性client,該屬性僅返回self. custt .client。
使用HTTP客戶端
HttpLocust的每個實例在client屬性中都有一個HttpSession
實例。HttpSession類實際上是request.Session
的子類,可用於發出HTTP請求,該請求將使用get,post,put,put,delete,head,patch和options方法將其統計數據報給Locust。HttpSession實例將在請求之間保存cookie,以便用於登錄網站並在請求之間保持會話。也可以從Locust實例的TaskSet實例中引用client屬性,以便輕鬆地檢索客戶端並發出HTTP請求。
下面是一個簡單的示例,它向 / about 路徑發出GET請求(在這種情況下,我們假設self是TaskSet
或HttpLocust
類的實例:
response = self.client.get("/about") print("Response status code:", response.status_code) print("Response content:", response.text)
這裡有一個POST請求的例子:
response = self.client.post("/login", {"username":"testuser", "password":"secret"} )
安全模式
HTTP客戶端配置為以safe_mode運行。這樣做的目的是,由於連接錯誤、超時或類似原因而失敗的任何請求都不會引發異常,而是返回一個空的虛擬Response對象。該請求將在Locust的統計資訊中標記為失敗。返回的虛擬Response的content屬性將設置為None,其status_code=0。
手動控制請求是成功還是失敗
默認情況下,除非HTTP響應程式碼為OK(<400),否則請求將被標記為失敗的請求。大多數情況下,此默認值是你想要的。但是,有時(例如,在測試URL端點時,你期望返回404,或者在測試一個設計糟糕的系統時,即使出現錯誤也可能返回200 OK)——需要手動控制Locust是否應該將請求標記為成功或失敗。
通過使用catch_response參數和with語句,即使響應程式碼正確,也可以將請求標記為失敗:
with self.client.get("/", catch_response=True) as response: if response.content != b"Success": response.failure("Got wrong response")
正如可以將具有OK響應程式碼的請求標記為失敗一樣,也可以將catch_response參數與with語句一起使用,以標記導致HTTP錯誤程式碼的請求在統計中仍被報告為成功:
with self.client.get("/does_not_exist/", catch_response=True) as response: if response.status_code == 404: response.success()
將帶有動態參數的URL請求分組
網站的url包含一些動態參數的頁面是很常見的。通常在Locust的統計資訊中將這些URL分組在一起是很有意義的。這可以通過將名稱參數傳遞給HttpSession的不同請求方法來完成。
例如:
# 這些請求的統計數據將歸入以下類別: /blog/?id=[id] for i in range(10): self.client.get("/blog?id=%i" % i, name="/blog?id=[id]")
公共庫–Common libraries
通常,將共享公共庫的多個locustfiles分組。在這種情況下,在項目中根目錄定義為調用Locust的目錄非常重要,建議所有的locust文件都位於項目根目錄下。
平面文件結構如下:
- project root
commonlib_config.py
commonlib_auth.py
locustfile_web_app.py
locustfile_api.py
locustfile_ecommerce.py
locustfile可以使用以下命令導入公共庫,例如:import commonlib_auth
。但是,這種方法並不能清楚地將公共庫與Locust文件分開。
子目錄可以是一種更簡潔的方法(請參見下面的示例),但locust只導入與運行的locustfile所在目錄相關的模組。如果希望從項目根目錄(即運行locust命令的位置)導入,請確保在loucst文件導入任何公共庫之前編寫sys.path.append(os.getcwd())
,這將使項目根目錄(即當前工作目錄)可導入。
- project root
__init__.py
common/
- `__init__.py`
- `config.py`
- `auth.py`
locustfiles/
- `__init__.py`
- `web_app.py`
- `api.py`
- `ecommerce.py`
通過以上項目結構,locust文件可以使用以下方法導入公共庫:
sys.path.append(os.getcwd()) import common.auth