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!

相关文章
相关标签/搜索