ApiTestEngine
不是介面測試框架麼,也能實現效能測試?
是的,你沒有看錯,ApiTestEngine
整合了Locust
效能測試框架,只需一份測試用例,就能同時實現介面自動化測試和介面效能測試,在不改變Locust
任何特性的情況下,甚至比Locust
本身更易用。
如果你還沒有接觸過Locust
這款效能測試工具,那麼這篇文章可能不適合你。但我還是強烈推薦你瞭解一下這款工具。簡單地說,Locust
是一款採用Python
語言編寫實現的開源效能測試工具,簡潔、輕量、高效,併發機制基於gevent
協程,可以實現單機模擬生成較高的併發壓力。關於Locust
的特性介紹和使用教程,我之前已經寫過不少,你們可以在我的部落格中找到對應文章。
如果你對實現的過程沒有興趣,可以直接跳轉到文章底部,看最終實現效果
章節。
靈感來源
在當前市面上的測試工具中,介面測試和效能測試基本上是兩個涇渭分明的領域。這也意味著,針對同一個系統的服務端介面,我們要對其實現介面自動化測試和介面效能測試時,通常都是採用不同的工具,分別維護兩份測試指令碼或用例。
之前我也是這麼做的。但是在做了一段時間後我就在想,不管是介面功能測試,還是介面效能測試,核心都是要模擬對介面發起請求,然後對介面響應內容進行解析和校驗;唯一的差異在於,介面效能測試存在併發的概念,相當於模擬了大量使用者同時在做介面測試。
既然如此,那介面自動化測試用例和介面效能測試指令碼理應可以合併為一套,這樣就可以避免重複的指令碼開發工作了。
在開發ApiTestEngine
的過程中,之前的文章也說過,ApiTestEngine
完全基於Python-Requests
庫實現HTTP的請求處理,可以在編寫介面測試用例時複用到Python-Requests
的所有功能特性。而之前在學習Locust
的原始碼時,發現Locust
在實現HTTP請求的時候,也完全是基於Python-Requests
庫。
在這一層關係的基礎上,我提出一個大膽的設想,能否通過一些方式或手段,可以使ApiTestEngine
中編寫的YAML/JSON
格式的介面測試用例,也能直接讓Locust
直接呼叫呢?
靈感初探
想法有了以後,就開始探索實現的方法了。
首先,我們可以看下Locust
的指令碼形式。如下例子是一個比較簡單的場景(擷取自官網首頁)。
from locust import HttpLocust, TaskSet, task
class WebsiteTasks(TaskSet):
def on_start(self):
self.client.post("/login", {
"username": "test_user",
"password": ""
})
@task
def index(self):
self.client.get("/")
@task
def about(self):
self.client.get("/about/")
class WebsiteUser(HttpLocust):
task_set = WebsiteTasks
min_wait = 5000
max_wait = 15000複製程式碼
在Locust
的指令碼中,我們會在TaskSet
子類中描述單個使用者的行為,每一個帶有@task
裝飾器的方法都對應著一個HTTP請求場景。而Locust
的一個很大特點就是,所有的測試用例指令碼都是Python
檔案,因此我們可以採用Python實現各種複雜的場景。
等等!模擬單個使用者請求,而且還是純粹的Python語言,我們不是在介面測試中已經實現的功能麼?
例如,下面的程式碼就是從單元測試中擷取的測試用例。
def test_run_testset(self):
testcase_file_path = os.path.join(
os.getcwd(), 'examples/quickstart-demo-rev-3.yml')
testsets = utils.load_testcases_by_path(testcase_file_path)
results = self.test_runner.run_testset(testsets[0])複製程式碼
test_runner.run_testset
是已經在ApiTestEngine
中實現的方法,作用是傳入測試用例(YAML/JSON
)的路徑,然後就可以載入測試用例,執行整個測試場景。並且,由於我們在測試用例YAML/JSON
中已經描述了validators
,即介面的校驗部分,因此我們也無需再對介面響應結果進行校驗描述了。
接下來,實現方式就非常簡單了。
我們只需要製作一個locustfile.py
的模板檔案,內容如下。
#coding: utf-8
import zmq
import os
from locust import HttpLocust, TaskSet, task
from ate import utils, runner
class WebPageTasks(TaskSet):
def on_start(self):
self.test_runner = runner.Runner(self.client)
self.testset = self.locust.testset
@task
def test_specified_scenario(self):
self.test_runner.run_testset(self.testset)
class WebPageUser(HttpLocust):
host = ''
task_set = WebPageTasks
min_wait = 1000
max_wait = 5000
testcase_file_path = os.path.join(os.getcwd(), 'skypixel.yml')
testsets = utils.load_testcases_by_path(testcase_file_path)
testset = testsets[0]複製程式碼
可以看出,整個檔案中,只有測試用例檔案的路徑是與具體測試場景相關的,其它內容全都可以不變。
於是,針對不同的測試場景,我們只需要將testcase_file_path
替換為介面測試用例檔案的路徑,即可實現對應場景的介面效能測試。
➜ ApiTestEngine git:(master) ✗ locust -f locustfile.py
[2017-08-27 11:30:01,829] bogon/INFO/locust.main: Starting web monitor at *:8089
[2017-08-27 11:30:01,831] bogon/INFO/locust.main: Starting Locust 0.8a2複製程式碼
後面的操作就完全是Locust
的內容了,使用方式完全一樣。
優化1:自動生成locustfile
通過前面的探索實踐,我們基本上就實現了一份測試用例同時兼具介面自動化測試和介面效能測試的功能。
然而,在使用上還不夠便捷,主要有兩點:
- 需要手工修改模板檔案中的
testcase_file_path
路徑; locustfile.py
模板檔案的路徑必須放在ApiTestEngine
的專案根目錄下。
於是,我產生了讓ApiTestEngine
框架本身自動生成locustfile.py
檔案的想法。
在實現這個想法的過程中,我想過兩種方式。
第一種,通過分析Locust
的原始碼,可以看到Locust
在main.py
中具有一個load_locustfile
方法,可以載入Python格式的檔案,並提取出其中的locust_classes
(也就是Locust
的子類);後續,就是將locust_classes
作為引數傳給Locust
的Runner
了。
若採用這種思路,我們就可以實現一個類似load_locustfile
的方法,將YAML/JSON
檔案中的內容動態生成locust_classes
,然後再傳給Locust
的Runner
。這裡面會涉及到動態地建立類和新增方法,好處是不需要生成locustfile.py
中間檔案,並且可以實現最大的靈活性,但缺點在於需要改變Locust
的原始碼,即重新實現Locust
的main.py
中的多個函式。雖然難度不會太大,但考慮到後續需要與Locust
的更新保持一致,具有一定的維護工作量,便放棄了該種方案。
第二種,就是生成locustfile.py
這樣一箇中間檔案,然後將檔案路徑傳給Locust
。這樣的好處在於我們可以不改變Locust
的任何地方,直接對其進行使用。與Locust
的傳統使用方式差異在於,之前我們是在Terminal
中通過引數啟動Locust
,而現在我們是在ApiTestEngine
框架中通過Python程式碼啟動Locust
。
具體地,我在setup.py
的entry_points
中新增了一個命令locusts
,並繫結了對應的程式入口。
entry_points={
'console_scripts': [
'ate=ate.cli:main_ate',
'locusts=ate.cli:main_locust'
]
}複製程式碼
在ate/cli.py
中新增了main_locust
函式,作為locusts
命令的入口。
def main_locust():
""" Performance test with locust: parse command line options and run commands.
"""
try:
from locust.main import main
except ImportError:
print("Locust is not installed, exit.")
exit(1)
sys.argv[0] = 'locust'
if len(sys.argv) == 1:
sys.argv.extend(["-h"])
if sys.argv[1] in ["-h", "--help", "-V", "--version"]:
main()
sys.exit(0)
try:
testcase_index = sys.argv.index('-f') + 1
assert testcase_index < len(sys.argv)
except (ValueError, AssertionError):
print("Testcase file is not specified, exit.")
sys.exit(1)
testcase_file_path = sys.argv[testcase_index]
sys.argv[testcase_index] = parse_locustfile(testcase_file_path)
main()複製程式碼
若你執行locusts -V
或locusts -h
,會發現效果與locust
的特性完全一致。
$ locusts -V
[2017-08-27 12:41:27,740] bogon/INFO/stdout: Locust 0.8a2
[2017-08-27 12:41:27,740] bogon/INFO/stdout:複製程式碼
事實上,通過上面的程式碼(main_locust
)也可以看出,locusts
命令只是對locust
進行了一層封裝,用法基本等價。唯一的差異在於,當-f
引數指定的是YAML/JSON
格式的用例檔案時,會先轉換為Python格式的locustfile.py
,然後再傳給locust
。
至於解析函式parse_locustfile
,實現起來也很簡單。我們只需要在框架中儲存一份locustfile.py
的模板檔案(ate/locustfile_template
),並將testcase_file_path
採用佔位符代替。然後,在解析函式中,就可以讀取整個模板檔案,將其中的佔位符替換為YAML/JSON
用例檔案的實際路徑,然後再儲存為locustfile.py
,並返回其路徑即可。
具體的程式碼就不貼了,有興趣的話可自行檢視。
通過這一輪優化,ApiTestEngine
就繼承了Locust
的全部功能,並且可以直接指定YAML/JSON
格式的檔案啟動Locust
執行效能測試。
$ locusts -f examples/first-testcase.yml
[2017-08-18 17:20:43,915] Leos-MacBook-Air.local/INFO/locust.main: Starting web monitor at *:8089
[2017-08-18 17:20:43,918] Leos-MacBook-Air.local/INFO/locust.main: Starting Locust 0.8a2複製程式碼
優化2:一鍵啟動多個locust例項
經過第一輪優化後,本來應該是告一段落了,因為此時ApiTestEngine
已經可以非常便捷地實現介面自動化測試和介面效能測試的切換了。
直到有一天,在TesterHome
論壇討論Locust
的一個回覆中,@keithmork
說了這麼一句話。
期待有一天
ApiTestEngine
的熱度超過Locust
本身
看到這句話時我真的不禁淚流滿面。雖然我也是一直在用心維護ApiTestEngine
,卻從未有過這樣的奢望。
但反過來細想,為啥不能有這樣的想法呢?當前ApiTestEngine
已經繼承了Locust
的所有功能,在不影響Locust
已有特性的同時,還可以採用YAML/JSON
格式來編寫維護測試用例,並實現了一份測試用例可同時用於介面自動化和介面效能測試的目的。
這些特性都是Locust
所不曾擁有的,而對於使用者來說的確也都是比較實用的功能。
於是,新的目標在內心深處萌芽了,那就是在ApiTestEngine
中通過對Locust
更好的封裝,讓Locust
的使用者體驗更爽。
然後,我又想到了自己之前做的一個開源專案,debugtalk/stormer
。當時做這個專案的初衷在於,當我們使用Locust
進行壓測時,要想使用壓測機所有CPU的效能,就需要採用master-slave
模式。因為Locust
預設是單程式執行的,只能執行在壓測機的一個CPU核上;而通過採用master-slave
模式,啟動多個slave
,就可以讓不同的slave
執行在不同的CPU核上,從而充分發揮壓測機多核處理器的效能。
而在實際使用Locust
的時候,每次只能手動啟動master
,並依次手動啟動多個slave
。若遇到測試指令碼調整的情況,就需要逐一結束Locust
的所有程式,然後再重複之前的啟動步驟。如果有使用過Locust
的同學,應該對此痛苦的經歷都有比較深的體會。當時也是基於這一痛點,我開發了debugtalk/stormer
,目的就是可以一次性啟動或銷燬多個Locust
例項。這個指令碼做出來後,自己用得甚爽,也得到了Github
上一些朋友的青睞。
既然現在要提升ApiTestEngine
針對Locust
的使用便捷性,那麼這個特性毫無疑問也應該加進去。就此,debugtalk/stormer
專案便被廢棄,正式合併到debugtalk/ApiTestEngine
。
想法明確後,實現起來也挺簡單的。
原則還是保持不變,那就是不改變Locust
本身的特性,只在傳參的時候在中間層進行操作。
具體地,我們可以新增一個--full-speed
引數。當不指定該引數時,使用方式跟之前完全相同;而指定--full-speed
引數後,就可以採用多程式的方式啟動多個例項(例項個數等於壓測機的處理器核數)。
def main_locust():
# do original work
if "--full-speed" in sys.argv:
locusts.run_locusts_at_full_speed(sys.argv)
else:
locusts.main()複製程式碼
具體實現邏輯在ate/locusts.py
中:
import multiprocessing
from locust.main import main
def start_master(sys_argv):
sys_argv.append("--master")
sys.argv = sys_argv
main()
def start_slave(sys_argv):
sys_argv.extend(["--slave"])
sys.argv = sys_argv
main()
def run_locusts_at_full_speed(sys_argv):
sys_argv.pop(sys_argv.index("--full-speed"))
slaves_num = multiprocessing.cpu_count()
processes = []
for _ in range(slaves_num):
p_slave = multiprocessing.Process(target=start_slave, args=(sys_argv,))
p_slave.daemon = True
p_slave.start()
processes.append(p_slave)
try:
start_master(sys_argv)
except KeyboardInterrupt:
sys.exit(0)複製程式碼
由此可見,關鍵點也就是使用了multiprocessing.Process
,在不同的程式中分別呼叫Locust
的main()
函式,實現邏輯十分簡單。
最終實現效果
經過前面的優化,採用ApiTestEngine
執行效能測試時,使用就十分便捷了。
安裝ApiTestEngine
後,系統中就具有了locusts
命令,使用方式跟Locust
框架的locust
幾乎完全相同,我們完全可以使用locusts
命令代替原生的locust
命令。
例如,下面的命令執行效果與locust
完全一致。
$ locusts -V
$ locusts -h
$ locusts -f locustfile.py
$ locusts -f locustfile.py --master -P 8088
$ locusts -f locustfile.py --slave &複製程式碼
差異在於,locusts
具有更加豐富的功能。
在ApiTestEngine
中編寫的YAML/JSON
格式的介面測試用例檔案,直接執行就可以啟動Locust
執行效能測試。
$ locusts -f examples/first-testcase.yml
[2017-08-18 17:20:43,915] Leos-MacBook-Air.local/INFO/locust.main: Starting web monitor at *:8089
[2017-08-18 17:20:43,918] Leos-MacBook-Air.local/INFO/locust.main: Starting Locust 0.8a2複製程式碼
加上--full-speed
引數,就可以同時啟動多個Locust
例項(例項個數等於處理器核數),充分發揮壓測機多核處理器的效能。
$ locusts -f examples/first-testcase.yml --full-speed -P 8088
[2017-08-26 23:51:47,071] bogon/INFO/locust.main: Starting web monitor at *:8088
[2017-08-26 23:51:47,075] bogon/INFO/locust.main: Starting Locust 0.8a2
[2017-08-26 23:51:47,078] bogon/INFO/locust.main: Starting Locust 0.8a2
[2017-08-26 23:51:47,080] bogon/INFO/locust.main: Starting Locust 0.8a2
[2017-08-26 23:51:47,083] bogon/INFO/locust.main: Starting Locust 0.8a2
[2017-08-26 23:51:47,084] bogon/INFO/locust.runners: Client 'bogon_656e0af8e968a8533d379dd252422ad3' reported as ready. Currently 1 clients ready to swarm.
[2017-08-26 23:51:47,085] bogon/INFO/locust.runners: Client 'bogon_09f73850252ee4ec739ed77d3c4c6dba' reported as ready. Currently 2 clients ready to swarm.
[2017-08-26 23:51:47,084] bogon/INFO/locust.main: Starting Locust 0.8a2
[2017-08-26 23:51:47,085] bogon/INFO/locust.runners: Client 'bogon_869f7ed671b1a9952b56610f01e2006f' reported as ready. Currently 3 clients ready to swarm.
[2017-08-26 23:51:47,085] bogon/INFO/locust.runners: Client 'bogon_80a804cda36b80fac17b57fd2d5e7cdb' reported as ready. Currently 4 clients ready to swarm.複製程式碼
後續,ApiTestEngine
將持續進行優化,歡迎大家多多反饋改進建議。
Enjoy!