locustfile.py
是 locust 運行的腳本文件,就像 jmeter 的 jmx 文件一樣。
locustfile 是普通的 python 文件。
唯一的要求是它聲明至少一個類,我們稱之爲用戶類屬於User
的子類。
用戶類 User class
一個用戶類別代表一個用戶(如果你願意,也可以認爲是一羣蝗蟲)。
Locust 將爲每個正在模擬的用戶生 User 類的一個實例。User 類通常必須定義如下這些屬性。
wait_time 屬性
除了tasks
屬性外,還應該聲明一種wait_time
方法。它用於確定虛擬用戶在執行任務之間等待多長時間。
wait_time
,也就是常見的性能工具中提到的 “思考時間”,模擬用戶在實際操作過程中的停頓。比如用戶點擊一個商品,進入商品詳情後,用戶會看看商品描述、商品圖片等,對系統來說這一段時間此用戶並未產生任何壓力。
Locust 帶有一些內置函數,這些函數返回一些常用的 wait_time 方法。
最常見的一種是between
。它用於使模擬用戶在每次執行任務後等待介於最小值和最大值之間的隨機時間。
使用以下 locustfile,每個用戶將在任務之間等待 5 到 15 秒:
from locust import User, task, between
class MyUser(User):
@task
def my_task(self):
print("executing my_task")
wait_time = between(5, 15)
wait_time 方法應返回秒數(或幾分之一秒),也可以在 TaskSet 類上聲明,在這種情況下,它將僅用於該 TaskSet。
也可以直接在 User 或 TaskSet 類上聲明自己的 wait_time 方法。下面的 User 類將開始休眠一秒鐘,然後休眠1, 2, 3,依此類推。
class MyUser(User):
last_wait_time = 0
def wait_time(self):
self.last_wait_time += 1
return self.last_wait_time
...
weight 屬性(可以理解爲執行比例或任務執行權重)
如果文件中存在多個用戶類,並且在命令行上未指定任何用戶類,則 Locust 將產生相等數量的每個用戶類。你還可以通過將它們作爲命令行參數傳遞,來指定要從同一 locustfile 中使用哪些用戶類:
$ locust -f locust_file.py WebUser MobileUser
如果你希望模擬更多特定類型的用戶,則可以在這些類上設置一個 weight 屬性。像下面的例子,WebUser 的可能性是 MobileUser 的三倍:
class WebUser(User):
weight = 3
...
class MobileUser(User):
weight = 1
...
host 屬性
host 屬性是要加載的主機的 URL 前綴(即“ http://google.com”)。通常,當 Locust 啓動時,這是在Locust 的 Web UI 或命令行中使用--host
選項指定的。
如果在 user 類中聲明瞭 host 屬性,則在命令行或 Web 請求中未指定--host
的情況下將使用該屬性。
tasks 屬性
user 類可以使用@task
裝飾器聲明爲任務的方法,但也可以指定任務使用tasks
屬性,該屬性在下面詳細介紹。
Tasks
啓動負載測試後,將爲每個模擬用戶創建一個 User 類的實例,並且它們將在其自己的 green 線程中運行。這些用戶運行時,他們選擇執行的任務(task 方法),休眠一會兒,然後選擇一個新任務,依此類推。
這些任務是普通的 python 可調用對象。 如果我們正在對拍賣網站進行負載測試,則它們可以執行諸如“加載起始頁”,“搜索某些產品”,“競標” 之類的場景。
聲明 tasks
使用task
裝飾器爲 User 類(或 TaskSet)聲明任務的典型方法。
Here is an example:
以下是一個典型的示例:
from locust import User, task, constant
class MyUser(User):
wait_time = constant(1)
@task
def my_task(self):
print("User instance (%r) executing my_task" % self)
@task
具有可選的weight
參數,可用於指定任務的執行率。在以下示例中,task2 被選爲task1 的機率是兩倍:
from locust import User, task, between
class MyUser(User):
wait_time = between(5, 15)
@task(3)
def task1(self):
pass
@task(6)
def task2(self):
pass
tasks 屬性
使用@task
裝飾器聲明任務是一種快捷的方式,通常也是聲明任務的最佳方法。但是,也可以通過設置tasks
屬性(使用來定義 User 或 TaskSet 的任務)@task
裝飾器實際上只會填充 tasks 屬性)。
tasks 屬性可以是 Task 的列表,也可以是 <Task:int>
dict 的列表,其中 Task 是可以調用的 python 或 TaskSet 類(在下文中有更多介紹)。如果任務是普通的 python 函數,則它們會收到一個參數,即正在執行任務的 User 實例。
以下是聲明普通 python 函數爲 User 任務的示例:
from locust import User, constant
def my_task(l):
pass
class MyUser(User):
tasks = [my_task]
wait_time = constant(1)
如果將 task 屬性指定爲列表,則每次執行任務時,都會從 tasks 屬性中隨機選擇該任務。但是,如果 tasks 是一個 dict 而非 list,將可調用對象作爲 key,將 ints 作爲值,將隨機選擇要執行的任務,但將 int 作爲執行比率。因此,任務看起來像這樣:
{my_task: 3, another_task: 1}
my_task would be 3 times more likely to be executed than another_task.
my_task 執行的可能性是其他任務的 3 倍。
在內部,上面的 dict 實際上將擴展爲一個看起來像這樣的列表(並且 task 屬性被更新):
[my_task, my_task, my_task, another_task]
然後使用 Python 的random.choice()
從列表中選擇任務。
標記 tasks
通過使用標記<locust.tag>
裝飾器標記任務,您可以使用--tags
和--exclude-tags
參數來選擇在測試期間執行哪些任務。參考以下示例:
from locust import User, constant, task, tag
class MyUser(User):
wait_time = constant(1)
@tag('tag1')
@task
def task1(self):
pass
@tag('tag1', 'tag2')
@task
def task2(self):
pass
@tag('tag3')
@task
def task3(self):
pass
@task
def task4(self):
pass
如果你使用--tags tag1
開始此測試,則在測試過程中將僅執行 task1 和 task2。如果你以--tags tag2 tag3
開始測試,則只會執行 task2 和 task3。
--exclude-tags
的行爲恰恰相反。因此,如果你以--exclude-tags tag3
開始測試,則只會執行 task1,task2 和 task4。
排除總是勝於包含。因此,如果一個任務同時被指定執行和排除,則該任務將不會執行。
TaskSet class
由於實際的網站通常以分層的方式構建,包括多個子部分,因此 Locust 具有 TaskSet 類。Locust 任務不僅可以是Python 可調用的,還可以是 TaskSet 類。TaskSet 是 Locust 任務的集合,將像直接在 User 類上聲明的任務一樣執行,使用戶在兩次任務執行之間處於休眠狀態。這是一個帶有 TaskSet 的 Locust 文件的簡短示例:
from locust import User, TaskSet, between
class ForumSection(TaskSet):
@task(10)
def view_thread(self):
pass
@task(1)
def create_thread(self):
pass
@task(1)
def stop(self):
self.interrupt()
class LoggedInUser(User):
wait_time = between(5, 120)
tasks = {ForumSection: 2}
@task
def index_page(self):
pass
也可以使用@task
裝飾器直接在 User/TaskSet 類下內聯 TaskSet:
class MyUser(User):
@task(1)
class MyTaskSet(TaskSet):
...
TaskSet 類的任務方法可以是其他 TaskSet 類,從而可以將它們嵌套任何數量的級別。這使我們能夠定義更能模擬實際用戶使用的場景。
例如,我們可以使用以下結構定義 TaskSet:
- Main user behaviour
- Index page
- Forum page
- Read thread
- Reply
- New thread
- View next page
- Browse categories
- Watch movie
- Filter movies
- About page
- 主場景
- 首頁
- 論壇頁
- 閱讀帖子
- 回覆帖子
- 創建帖子
- 查看下一頁
- 瀏覽分類
- 觀看視頻
- 過濾視頻
- 查看幫助
當正在運行的虛擬用戶線程選擇 TaskSet 類執行時:
- 將創建該類的實例,然後從該 TaskSet 類中選取一個任務並執行;
- 然後將根據
wait_time
方法指定的等待時間休眠; - 然後從 TaskSet 的任務方法中選擇一個新的任務執行;
- 然後再次等待,依次類推。
中斷 TaskSet
有關 TaskSet 的重要一件事是,它們永遠不會停止執行其任務,也就意味着任務控制權不會自動回到父 User/TaskSet。開發人員必須通過調用TaskSet.interrupt()
方法來完成此操作。
-
interrupt
(self, reschedule=True)中斷 TaskSet 並將執行控制移交給父 TaskSet。如果 reschedule 爲 True(默認值),則父 User 將立即重新計劃並執行新任務。
在以下示例中,如果我們沒有調用self.interrupt()
的停止任務,則模擬用戶一旦進入論壇任務集中就永遠不會停止運行該任務:
class RegisteredUser(User):
@task
class Forum(TaskSet):
@task(5)
def view_thread(self):
pass
@task(1)
def stop(self):
self.interrupt()
@task
def frontpage(self):
pass
使用中斷功能,我們可以與任務權重一起定義虛擬用戶離開論壇的可能性。
TaskSet 和 User 類中的任務之間的差異
與直接駐留在User下的任務相比,駐留在TaskSet下的任務的一個區別是,執行時傳遞的參數是對 TaskSet 實例的引用,而不是對 User 實例的引用。可以通過TaskSet.user
從 TaskSet 實例內部訪問 User 實例。TaskSets 還包含一個方便的client
屬性,該屬性引用 User 實例上的 client 屬性。
引用 User 實例或父 TaskSet 實例
TaskSet 實例包含一個 user
屬性指向引用它的 User 實例,parent
屬性指向其父類 TaskSet 實例。
標記任務集
你可以使用標籤<locust.tag>
裝飾器爲 TaskSet 進行標記,其方式與普通任務類似,如上述<tagging-tasks>
所述,但是有一些細微差別值得一提。標記 TaskSet 會將標記自動應用於所有 TaskSet 的任務。此外,如果被標記的 TaskSet 存在嵌套 TaskSet,即使沒有標記嵌套的子 TaskSet,Locust 也會執行該任務。
順序任務集
SequentialTaskSet
是 TaskSet,但其任務將按照聲明的順序執行。SequentialTaskSet 類上任務的權重將被忽略。可以將 SequentialTaskSets 嵌套在 TaskSet 中,反之亦然。
def function_task(taskset):
pass
class SequenceOfTasks(SequentialTaskSet):
@task
def first_task(self):
pass
tasks = [functon_task]
@task
def second_task(self):
pass
@task
def third_task(self):
pass
在上面的示例中,任務以聲明的順序執行:
first_task
function_task
second_task
third_task
然後它將再次從first_task
開始循環。
on_start 和 on_stop 方法
User 類和 TaskSet 類都可以聲明 on_start
方法和 on_stop
方法。
User 類會在啓動運行時運行 on_start
方法,並在停止運行時執行 on_stop
方法。
對於 TaskSet 來說,on_start
會在虛擬用戶開始執行該 TaskSet 時運行,而在虛擬用戶停止執行 TaskSet 時執行 on_stop
方法(即當 interrupt()
被調用時,或者虛擬用戶被幹掉時)。
測試開始與結束事件
如果你需要在開始或結束負載測試時需要執行某些代碼,你可以使用 test_start
和 test_stop
事件。你可以在 locustfile 模塊級別爲這些事件設置監聽器:
from locust import events
@events.test_start.add_listener
def on_test_start(**kwargs):
print("A new test is starting")
@events.test_stop.add_listener
def on_test_stop(**kwargs):
print("A new test is ending")
在運行 Locust 分佈式執行時,只會在主節點中觸發test_start
和test_stop
事件。
構造 HTTP 請求
到目前爲止,我們僅介紹了用戶的任務計劃部分。爲了實際負載測試系統,我們需要發出 HTTP 請求。HttpLocust
的存在就是爲了幫助我們做到這一點。當使用 HttpLocust
類時,每個該類的實例會獲得一個 client
屬性(HttpSession
的實例,HttpSession
用來構造 HTTP 請求)。
-
class
HttpUser
(*args, **kwargs)一個虛擬用戶表示要要進行負載測試的系統的一系列 HTTP 請求。此用戶的行爲由其任務定義,可以通過在方法上使用
@task decorator
或通過設置tasks attribute
進行定義。此類在實例化時創建了一個 client 屬性,該屬性是一個 HTTP 客戶端,用於在請求之間保持用戶的 session。
從 HttpUser 類繼承時,我們可以使用其 client 屬性對服務器發出 HTTP 請求。以下是一個 locustfile 的示例,可用於對具有兩個 URL 的站點 / 和 /about/ 進行負載測試:
from locust import HttpUser, task, between
class MyUser(HttpUser):
wait_time = between(5, 15)
@task(2)
def index(self):
self.client.get("/")
@task(1)
def about(self):
self.client.get("/about/")
使用上面的 User 類,每個虛擬用戶將在請求之間等待 5 到 15 秒,而 / 發出的請求將是 /about/ 的兩倍。
使用 HTTP client
每個 HttpUser 實例在 client 屬性中都包含 HttpSession
的實例。HttpSession 類其實是 requests.Session
的子類,用來構造 HTTP 請求,並會收集統計信息,可以使用 get
, post
, put
, delete
, head
, patch
和 options
方法。
HttpSession 的實例將在請求之間保留 cookie,以便可用於登錄網站並在請求之間保持會話。client 屬性可以被 User 實例中的 TaskSet 實例引用,以便輕鬆地從任意地方提取 client 併發出HTTP請求。
以下一個簡單的示例,它向 /about 發出 GET 請求(在這種情況下,我們假設 self 是一個 TaskSet
實例或者 HttpUser
實例:
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 對象。該請求將在用戶統計信息中報告爲失敗。返回的虛擬響應的 content 屬性將設置爲None,其 status_code 將爲 0。
手動控制請求的成功與失敗
默認情況下,除非 HTTP 響應代碼爲 OK(<400),否則請求將被標記爲失敗。
大多數情況下,此默認值是你想要的。但是有時,例如,當測試預期返回 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()
使用動態參數將對請求的分組
Web 站點上的 URL 中包含某種動態參數是很常見的。通常,將這些 URL 歸爲“用戶”統計信息是有意義的。這可以通過向HttpSession's
不同的請求方法傳遞 name 參數來完成。
示例如下:
# Statistics for these requests will be grouped under: /blog/?id=[id]
for i in range(10):
self.client.get("/blog?id=%i" % i, name="/blog?id=[id]")
HTTP 代理設置
爲了提高性能,我們通過將 request.Session 的 trust_env 屬性設置爲 False,將請求配置爲不在環境中查找 HTTP 代理設置。
如果你不希望這樣做,可以手動將locust_instance.client.trust_env
設置爲True
。有關更多詳細信息,請參閱requests文檔。
如何構建測試代碼
重要的是要記住:locustfile.py
只是由 Locust 導入的普通 Python 模塊。從該模塊中,你可以像導入任何 Python 程序一樣自由地導入其他 python 代碼。當前的工作目錄會自動添加到 python 的 sys.path 中,因此可以使用 python 的 import 語句導入駐留在工作目錄中的所有 python 文件/模塊/軟件包。
對於小型測試,將所有測試代碼保存在一個locustfile.py
中應該可以正常工作,但是對於大型測試套件,你可能需要將代碼拆分爲多個文件和目錄。
當然,如何構造測試源代碼完全取決於你,但是我們建議你遵循 Python 最佳實踐。以下是一個虛構的 Locust 項目的示例文件結構:
-
Project root
-
common/
__init__.py
auth.py
config.py
locustfile.py
requirements.txt
(用到第三方擴展庫及其版本說明保存在 requirements.txt)
-
具有多個不同 locustfiles 的項目也可以將它們保存在單獨的子目錄中:
-
Project root
-
common/
__init__.py
auth.py
config.py
-
locustfiles/
api.py
website.py
requirements.txt
-
使用上述任何項目結構,你的 locustfile 都可以使用以下方法導入公共庫:
import common.auth