ApiTestEngine 集成 Locust 實現更好的性能測試體驗

ApiTestEngine不是接口測試框架麼,也能實現性能測試?python

是的,你沒有看錯,ApiTestEngine集成了Locust性能測試框架,只需一份測試用例,就能同時實現接口自動化測試和接口性能測試,在不改變Locust任何特性的狀況下,甚至比Locust自己更易用。git

若是你尚未接觸過Locust這款性能測試工具,那麼這篇文章可能不適合你。但我仍是強烈推薦你瞭解一下這款工具。簡單地說,Locust是一款採用Python語言編寫實現的開源性能測試工具,簡潔、輕量、高效,併發機制基於gevent協程,能夠實現單機模擬生成較高的併發壓力。關於Locust的特性介紹和使用教程,我以前已經寫過很多,大家能夠在個人博客中找到對應文章github

若是你對實現的過程沒有興趣,能夠直接跳轉到文章底部,看最終實現效果章節。web

靈感來源

在當前市面上的測試工具中,接口測試和性能測試基本上是兩個涇渭分明的領域。這也意味着,針對同一個系統的服務端接口,咱們要對其實現接口自動化測試和接口性能測試時,一般都是採用不一樣的工具,分別維護兩份測試腳本或用例。bash

以前我也是這麼作的。可是在作了一段時間後我就在想,不論是接口功能測試,仍是接口性能測試,核心都是要模擬對接口發起請求,而後對接口響應內容進行解析和校驗;惟一的差別在於,接口性能測試存在併發的概念,至關於模擬了大量用戶同時在作接口測試。併發

既然如此,那接口自動化測試用例和接口性能測試腳本理應能夠合併爲一套,這樣就能夠避免重複的腳本開發工做了。app

在開發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的源碼,能夠看到Locustmain.py中具備一個load_locustfile方法,能夠加載Python格式的文件,並提取出其中的locust_classes(也就是Locust的子類);後續,就是將locust_classes做爲參數傳給LocustRunner了。

若採用這種思路,咱們就能夠實現一個相似load_locustfile的方法,將YAML/JSON文件中的內容動態生成locust_classes,而後再傳給LocustRunner。這裏面會涉及到動態地建立類和添加方法,好處是不須要生成locustfile.py中間文件,而且能夠實現最大的靈活性,但缺點在於須要改變Locust的源碼,即從新實現Locustmain.py中的多個函數。雖然難度不會太大,但考慮到後續須要與Locust的更新保持一致,具備必定的維護工做量,便放棄了該種方案。

第二種,就是生成locustfile.py這樣一箇中間文件,而後將文件路徑傳給Locust。這樣的好處在於咱們能夠不改變Locust的任何地方,直接對其進行使用。與Locust的傳統使用方式差別在於,以前咱們是在Terminal中經過參數啓動Locust,而如今咱們是在ApiTestEngine框架中經過Python代碼啓動Locust

具體地,我在setup.pyentry_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 -Vlocusts -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,在不一樣的進程中分別調用Locustmain()函數,實現邏輯十分簡單。

最終實現效果

通過前面的優化,採用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!

相關文章
相關標籤/搜索