1. 介绍

性能工具对比
LoadRunner 是非常有名的商业性能测试工具,功能非常强大。使用也比较复杂,目前大多介绍性能测试的书籍都以该工具为基础,甚至有些书整本都在介绍 LoadRunner 的使用。
Jmeter 同样是非常有名的开源性能测试工具,功能也很完善,在本书中介绍了它作为接口测试工具的使用。但实际上,它是一个标准的性能测试工具。关于Jmeter相关的资料也非常丰富,它的官方文档也很完善。
Locust 同样是性能测试工具,虽然官方这样来描述它 “An open source load testing tool.” 。但其它和前面两个工具有着较大的不同。相比前面两个工具,功能上要差上不少,但它也并非优点全无。
- Locust 完全基本 Python 编程语言,采用 Pure Python 描述测试脚本,并且 HTTP 请求完全基于 Requests 库。除了 HTTP/HTTPS 协议,Locust 也可以测试其它协议的系统,只需要采用Python调用对应的库进行请求描述即可。
- LoadRunner 和 Jmeter 这类采用进程和线程的测试工具,都很难在单机上模拟出较高的并发压力。Locust 的并发机制摒弃了进程和线程,采用协程(gevent)的机制。协程避免了系统级资源调度,由此可以大幅提高单机的并发能力。
正是基于这样的特点,使我选择使用Locust工具来做性能测试,另外一个原因是它可以让我们换一种方式认识性能测试,可能更容易看清性能测试的本质。
我想已经成功的引起了你的兴趣,那么接下来就跟着来学习Locust的使用吧。
2. Locust 安装
方式一:通过 pip 命令安装
pip install locust
python setup.py install

3. Locust 使用入门
编写第一个性能测试脚本
from locust import HttpLocust, TaskSet, task
# 定义用户行为
class UserBehavior(TaskSet):
@task
def baidu_index(self):
self.client.get("/")
class WebsiteUser(HttpLocust):
task_set = UserBehavior
min_wait = 3000
max_wait = 6000
WebsiteUser类用于设置性能测试。
- task_set :指向一个定义的用户行为类。
- min_wait :执行事务之间用户等待时间的下界(单位:毫秒)。
- max_wait :执行事务之间用户等待时间的上界(单位:毫秒)。
执行性能测试脚本
locust -f ./locustfile.py --host=https://www.baidu.com
#[2019-07-04 15:40:11,348] tsbc.leaptocloud/INFO/locust.main: Starting web monitor at *:8089
#[2019-07-04 15:40:11,348] tsbc.leaptocloud/INFO/locust.main: Starting Locust 0.11.1
- -f 指定性能测试脚本文件。
- --host 指定被测试应用的URL的地址,注意访问百度使用的HTTPS协议。
通过浏览器访问:http://127.0.0.1:8089(Locust启动网络监控器,默认为端口号为: 8089)

设置模拟用户数。
设置孵化率 每秒产生(启动)的虚拟用户数。
点击 “启动” 按钮,开始运行性能测试。

性能测试参数分析
- Type: 请求的类型,例如GET/POST。
- Name:请求的路径。这里为百度首页,即:https://www.baidu.com/
- request:当前请求的数量。
- fails:当前请求失败的数量。
- Median:中间值,单位毫秒,一半的服务器响应时间低于该值,而另一半高于该值。
- Average:平均值,单位毫秒,所有请求的平均响应时间。
- Min:请求的最小服务器响应时间,单位毫秒。
- Max:请求的最大服务器响应时间,单位毫秒。
- Content Size:单个请求的大小,单位字节。
4. Locust 脚本开发进阶
Locust 中类的定义和使用
HttpLocust 类
在Locust类
中,具有一个client
属性,它对应着虚拟用户作为客户端所具备的请求能力,也就是我们常说的请求方法。通常情况下,我们不会直接使用Locust
类,因为其client
属性没有绑定任何方法。因此在使用Locust
时,需要先继承Locust类
,然后在继承子类中的client
属性中绑定客户端的实现类。
对于常见的HTTP(S)
协议,Locust
已经实现了HttpLocust
类,其client
属性绑定了HttpSession
类,而HttpSession
又继承自requests.Session
。因此在测试HTTP(S)
的Locust脚本
中,我们可以通过client
属性来使用Python requests
库的所有方法,包括GET/POST/HEAD/PUT/DELETE/PATCH
等,调用方式也与requests
完全一致。另外,由于requests.Session
的使用,因此client
的方法调用之间就自动具有了状态记忆的功能。常见的场景就是,在登录系统后可以维持登录状态的Session
,从而后续HTTP请求操作都能带上登录态。
而对于HTTP(S)
以外的协议,我们同样可以使用Locust
进行测试,只是需要我们自行实现客户端。在客户端的具体实现上,可通过注册事件的方式,在请求成功时触发events.request_success
,在请求失败时触发events.request_failure
即可。然后创建一个继承自Locust类
的类,对其设置一个client
属性并与我们实现的客户端进行绑定。后续,我们就可以像使用HttpLocust类
一样,测试其它协议类型的系统。
在Locust类
中,除了client
属性,还有几个属性需要关注下:
-
task_set
: 指向一个TaskSet
类,TaskSet
类定义了用户的任务信息,该属性为必填; -
max_wait/min_wait
: 每个用户执行两个任务间隔时间的上下限(毫秒),具体数值在上下限中随机取值,若不指定则默认间隔时间固定为1秒; -
host
:被测系统的host,当在终端中启动locust
时没有指定--host
参数时才会用到; -
weight
:同时运行多个Locust类
时会用到,用于控制不同类型任务的执行权重。
测试开始后,每个虚拟用户(Locust实例
)的运行逻辑都会遵循如下规律:
- 先执行
WebsiteTasks
中的on_start
(只执行一次),作为初始化; - 从
WebsiteTasks
中随机挑选(如果定义了任务间的权重关系,那么就是按照权重关系随机挑选)一个任务执行; - 根据
Locust类
中min_wait
和max_wait
定义的间隔时间范围(如果TaskSet类
中也定义了min_wait
或者max_wait
,以TaskSet
中的优先),在时间范围中随机取一个值,休眠等待; - 重复
2~3
步骤,直至测试任务终止。
from locust import HttpLocust, TaskSet, task
class UserTask(TaskSet):
@task
def tc_index(self):
self.client.get("/")
class UserOne(HttpLocust):
task_set = UserTask
weight = 1
min_wait = 1000
max_wait = 3000
stop_timeout = 5
host = "https://www.baidu.com"
class UserTwo(HttpLocust):
weight = 2
task_set = UserTask
host = "https://www.baidu.com"
一个Locust实例被挑选执行的权重,数值越大,执行频率越高。在一个 locustfile.py 文件中可以同时定义多个 HttpLocust 子类,然后分配他们的执行权重,例如:
然后在终端启动测试:
locust -f locustfile.py UserOne UserTwo
TaskSet 类
性能测试工具要模拟用户的业务操作,就需要通过脚本模拟用户的行为。在前面的比喻中说到,TaskSet类
好比蝗虫的大脑,控制着蝗虫的具体行为。
具体地,TaskSet类
实现了虚拟用户所执行任务的调度算法,包括规划任务执行顺序(schedule_task
)、挑选下一个任务(execute_next_task
)、执行任务(execute_task
)、休眠等待(wait
)、中断控制(interrupt
)等等。在此基础上,我们就可以在TaskSet
子类中采用非常简洁的方式来描述虚拟用户的业务测试场景,对虚拟用户的所有行为(任务)进行组织和描述,并可以对不同任务的权重进行配置。
在TaskSet
子类中定义任务信息时,可以采取两种方式,@task装饰器
和tasks属性
。
采用@task装饰器
定义任务信息时,描述形式如下:
class UserBehavior(TaskSet):
@task(1)
def test_job1(self):
self.client.get('/job1')
@task(2)
def test_job2(self):
self.client.get('/job2')
from locust import TaskSet, task
class UserBehavior(TaskSet):
@task(1)
def test_job1(self):
self.client.get('/job1')
@task(2)
def test_job2(self):
self.client.get('/job2')
tasks属性
定义任务信息时,描述形式如下:def test_job1(obj):
obj.client.get('/job1')
def test_job2(obj):
obj.client.get('/job2')
class UserBehavior(TaskSet):
tasks = {test_job1:1, test_job2:2}
# tasks = [(test_job1,1), (test_job1,2)] # 两种方式等价
from locust import TaskSet
def test_job1(obj):
obj.client.get('/job1')
def test_job2(obj):
obj.client.get('/job2')
class UserBehavior(TaskSet):
tasks = {test_job1:1, test_job2:2}
# tasks = [(test_job1,1), (test_job1,2)] # 两种方式等价
test_job2
的频率是test_job1
的两倍。1:1
。class UserBehavior(TaskSet):
@task
def test_job1(self):
self.client.get('/job1')
@task
def test_job2(self):
self.client.get('/job2')
from locust import TaskSet, task
class UserBehavior(TaskSet):
@task
def test_job1(self):
self.client.get('/job1')
@task
def test_job2(self):
self.client.get('/job2')
def test_job1(obj):
obj.client.get('/job1')
def test_job2(obj):
obj.client.get('/job2')
class UserBehavior(TaskSet):
tasks = [test_job1, test_job2]
from locust import TaskSet
def test_job1(obj):
obj.client.get('/job1')
def test_job2(obj):
obj.client.get('/job2')
class UserBehavior(TaskSet):
tasks = [test_job1, test_job2]
TaskSequence类
@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
class MyTaskSequence(TaskSequence):
@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
@seq_task
使用@task
装饰器进行组合,当然您也可以在TaskSequences中嵌套TaskSet,反之亦然。公共库
sys.path.append(os.getcwd())
在导入任何公共库之前写入您的locust文件 - 这将使项目成为root(即当前正在工作)目录)可导入。__init__.py
-
common/
__init__.py
config.py
auth.py
-
locustfiles/
__init__.py
web_app.py
api.py
ecommerce.py
使用上述项目结构,您的locust文件可以使用以下命令导入公共库:
import common.auth
sys.path.append(os.getcwd())
import common.auth
HttpSession类
每个发出请求的方法还需要两个额外的可选参数,这些参数是特定于Locust的,并且在python-requests中不存在。这些是:
- name - (可选)可以指定在Locust的统计信息中用作标签而不是URL路径的参数。这可用于将请求的不同URL分组到Locust统计信息中的单个条目中。
- catch_response - (可选)布尔参数,如果设置,可用于发出请求,返回上下文管理器作为with语句的参数。这将允许根据响应内容将请求标记为失败,即使响应代码正常(2xx)。相反也有效,可以使用catch_response来捕获请求,然后将其标记为成功,即使响应代码不是(即500或404)。
request
(方法,网址,名称=无,catch_response = False,** kwargs )
构造并发送一个requests.Request
。返回requests.Response
对象。
参数: |
|
---|
脚本增强
设置响应断言
from locust import HttpLocust, TaskSet, task
class UserTask(TaskSet):
@task
def job(self):
with self.client.get('/', catch_response = True) as response:
if response.status_code == 200:
response.failure('Failed!')
else:
response.success()
class User(HttpLocust):
task_set = UserTask
min_wait = 1000
max_wait = 3000
host = "https://www.baidu.com"
catch_response = True :布尔类型,如果设置为 True, 允许该请求被标记为失败。
通过 client.get() 方法发送请求,将整个请求的给 response, 通过 response.status_code 得请求响应的 HTTP 状态码。如果不为 200 则通过 response.failure('Failed!') 打印失败!
Locust关联
在某些请求中,需要携带之前从Server端返回的参数,因此在构造请求时需要先从之前的Response中提取出所需的参数。
from lxml import etree
from locust import TaskSet, task, HttpLocust
class UserBehavior(TaskSet):
@staticmethod
def get_session(html):
tree = etree.HTML(html)
return tree.xpath("//div[@class='btnbox']/input[@name='session']/@value")[0]
@task(10)
def test_login(self):
html = self.client.get('/login').text
username = 'user@compay.com'
password = '123456'
session = self.get_session(html)
payload = {
'username': username,
'password': password,
'session': session
}
self.client.post('/login', data=payload)
class WebsiteUser(HttpLocust):
host = 'http://debugtalk.com'
task_set = UserBehavior
min_wait = 1000
max_wait = 3000
Locust 参数化
from locust import TaskSet, task, HttpLocust
import queue
class UserBehavior(TaskSet):
@task
def test_register(self):
try:
data = self.locust.user_data_queue.get()
except queue.Empty:
print('account data run out, test ended.')
exit(0)
print('register with user: {}, pwd: {}'\
.format(data['username'], data['password']))
payload = {
'username': data['username'],
'password': data['password']
}
self.client.post('/register', data=payload)
class WebsiteUser(HttpLocust):
host = 'http://debugtalk.com'
task_set = UserBehavior
user_data_queue = queue.Queue()
for index in range(100):
data = {
"username": "test%04d" % index,
"password": "pwd%04d" % index,
"email": "test%04d@debugtalk.test" % index,
"phone": "186%08d" % index,
}
user_data_queue.put_nowait(data)
min_wait = 1000
max_wait = 3000
HttpRunner使用(v2.x)
介绍
YAML/JSON
脚本,即可实现自动化测试、性能测试、线上监控、持续集成等多种测试需求。安装
pip install httprunner
在 HttpRunner 安装成功后,系统中会新增如下 5 个命令:
httprunner、hrun、ate 三个命令完全等价,功能特性完全相同,个人推荐使用hrun
命令。
运行如下命令,若正常显示版本号,则说明 HttpRunner 安装成功。
(locust) [root@tsbc api]# hrun -V
2.1.3
快速使用
抓包

生成测试用例
为了简化测试用例的编写工作,HttpRunner 实现了测试用例生成的功能。
首先,需要将抓取得到的数据包导出为 HAR 格式的文件,如:名称为 demo-quickstart.har。
然后,在命令行终端中运行如下命令,即可将 demo-quickstart.har 转换为 HttpRunner 的测试用例文件。
har2case docs/data/demo-quickstart.har -2y
INFO:root:Start to generate testcase.
INFO:root:dump testcase to YAML format.
INFO:root:Generate YAML testcase successfully: docs/data/demo-quickstart.yml
- config:
name: testcase description
variables: {}
- test:
name: /api/get-token
request:
headers:
Content-Type: application/json
User-Agent: python-requests/2.18.4
app_version: 2.8.6
device_sn: FwgRiO7CNA50DSU
os_platform: ios
json:
sign: 9c0c7e51c91ae963c833a4ccbab8d683c4a90c98
method: POST
url: http://127.0.0.1:5000/api/get-token
validate:
- eq: [status_code, 200]
- eq: [headers.Content-Type, application/json]
- eq: [content.success, true]
- eq: [content.token, baNLX1zhFYP11Seb]
- test:
name: /api/users/1000
request:
headers:
Content-Type: application/json
User-Agent: python-requests/2.18.4
device_sn: FwgRiO7CNA50DSU
token: baNLX1zhFYP11Seb
json:
name: user1
password: '123456'
method: POST
url: http://127.0.0.1:5000/api/users/1000
validate:
- eq: [status_code, 201]
- eq: [headers.Content-Type, application/json]
- eq: [content.success, true]
- eq: [content.msg, user created successfully.]
现在我们只需要知道如下几点:
- 每个 YAML/JSON 文件对应一个测试用例(testcase)
- 每个测试用例为一个list of dict结构,其中可能包含全局配置项(config)和若干个测试步骤(test)
- config 为全局配置项,作用域为整个测试用例
- test 对应单个测试步骤,作用域仅限于本身
如上便是 HttpRunner 测试用例的基本结构。
demo-quickstart.json
[
{
"config": {
"name": "testcase description",
"variables": {}
}
},
{
"test": {
"name": "/api/get-token",
"request": {
"url": "http://127.0.0.1:5000/api/get-token",
"method": "POST",
"headers": {
"User-Agent": "python-requests/2.18.4",
"device_sn": "FwgRiO7CNA50DSU",
"os_platform": "ios",
"app_version": "2.8.6",
"Content-Type": "application/json"
},
"json": {
"sign": "9c0c7e51c91ae963c833a4ccbab8d683c4a90c98"
}
},
"validate": [
{"eq": ["status_code", 200]},
{"eq": ["headers.Content-Type", "application/json"]},
{"eq": ["content.success", true]},
{"eq": ["content.token", "baNLX1zhFYP11Seb"]}
]
}
},
{
"test": {
"name": "/api/users/1000",
"request": {
"url": "http://127.0.0.1:5000/api/users/1000",
"method": "POST",
"headers": {
"User-Agent": "python-requests/2.18.4",
"device_sn": "FwgRiO7CNA50DSU",
"token": "baNLX1zhFYP11Seb",
"Content-Type": "application/json"
},
"json": {
"name": "user1",
"password": "123456"
}
},
"validate": [
{"eq": ["status_code", 201]},
{"eq": ["headers.Content-Type", "application/json"]},
{"eq": ["content.success", true]},
{"eq": ["content.msg", "user created successfully."]}
]
}
}
]
首次运行测试用例
运行测试用例的命令为hrun
,后面直接指定测试用例文件的路径即可。
$ hrun docs/data/demo-quickstart-1.yml
locusts -f docs/data/demo-quickstart-1.yml
span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }.cm-searching {background: #ffa; background: rgba(255, 255, 0, .4);}.cm-force-border { padding-right: .1px; }@media print { .CodeMirror div.CodeMirror-cursors {visibility: hidden;}}.cm-tab-wrap-hack:after { content: ""; }span.CodeMirror-selectedtext { background: none; }.CodeMirror-activeline-background, .CodeMirror-selected {transition: visibility 0ms 100ms;}.CodeMirror-blur .CodeMirror-activeline-background, .CodeMirror-blur .CodeMirror-selected {visibility:hidden;}.CodeMirror-blur .CodeMirror-matchingbracket {color:inherit !important;outline:none !important;text-decoration:none !important;}.CodeMirror-sizer {min-height:auto !important;}
-->
span::selection, .cm-s-material .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }.cm-s-material .CodeMirror-line::-moz-selection, .cm-s-material .CodeMirror-line > span::-moz-selection, .cm-s-material .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }.cm-s-material .CodeMirror-activeline-background { background: rgba(0, 0, 0, 0); }.cm-s-material .cm-keyword { color: rgba(199, 146, 234, 1); }.cm-s-material .cm-operator { color: rgba(233, 237, 237, 1); }.cm-s-material .cm-variable-2 { color: #80CBC4; }.cm-s-material .cm-variable-3 { color: #82B1FF; }.cm-s-material .cm-builtin { color: #DECB6B; }.cm-s-material .cm-atom { color: #F77669; }.cm-s-material .cm-number { color: #F77669; }.cm-s-material .cm-def { color: rgba(233, 237, 237, 1); }.cm-s-material .cm-string { color: #C3E88D; }.cm-s-material .cm-string-2 { color: #80CBC4; }.cm-s-material .cm-comment { color: #546E7A; }.cm-s-material .cm-variable { color: #82B1FF; }.cm-s-material .cm-tag { color: #80CBC4; }.cm-s-material .cm-meta { color: #80CBC4; }.cm-s-material .cm-attribute { color: #FFCB6B; }.cm-s-material .cm-property { color: #80CBAE; }.cm-s-material .cm-qualifier { color: #DECB6B; }.cm-s-material .cm-variable-3 { color: #DECB6B; }.cm-s-material .cm-tag { color: rgba(255, 83, 112, 1); }.cm-s-material .cm-error {color: rgba(255, 255, 255, 1.0); background-color: #EC5F67;}.cm-s-material .CodeMirror-matchingbracket {text-decoration: underline; color: white !important;}
-->
span::selection, .cm-s-monokai .CodeMirror-line > span > span::selection { background: rgba(73, 72, 62, .99); }.cm-s-monokai .CodeMirror-line::-moz-selection, .cm-s-monokai .CodeMirror-line > span::-moz-selection, .cm-s-monokai .CodeMirror-line > span > span::-moz-selection { background: rgba(73, 72, 62, .99); }.cm-s-monokai .CodeMirror-gutters { background: #272822; border-right: 0px; }.cm-s-monokai .CodeMirror-guttermarker { color: white; }.cm-s-monokai .CodeMirror-guttermarker-subtle { color: #d0d0d0; }.cm-s-monokai .CodeMirror-linenumber { color: #d0d0d0; }.cm-s-monokai .CodeMirror-cursor { border-left: 1px solid #f8f8f0; }.cm-s-monokai span.cm-comment { color: #75715e; }.cm-s-monokai span.cm-atom { color: #ae81ff; }.cm-s-monokai span.cm-number { color: #ae81ff; }.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute { color: #a6e22e; }.cm-s-monokai span.cm-keyword { color: #f92672; }.cm-s-monokai span.cm-builtin { color: #66d9ef; }.cm-s-monokai span.cm-string { color: #e6db74; }.cm-s-monokai span.cm-variable { color: #f8f8f2; }.cm-s-monokai span.cm-variable-2 { color: #9effff; }.cm-s-monokai span.cm-variable-3 { color: #66d9ef; }.cm-s-monokai span.cm-def { color: #fd971f; }.cm-s-monokai span.cm-bracket { color: #f8f8f2; }.cm-s-monokai span.cm-tag { color: #f92672; }.cm-s-monokai span.cm-header { color: #ae81ff; }.cm-s-monokai span.cm-link { color: #ae81ff; }.cm-s-monokai span.cm-error { background: #f92672; color: #f8f8f0; }.cm-s-monokai .CodeMirror-activeline-background { background: #373831; }.cm-s-monokai .CodeMirror-matchingbracket {text-decoration: underline; color: white !important;}
-->
li {list-style-type:decimal;}.wiz-editor-body ol.wiz-list-level2 > li {list-style-type:lower-latin;}.wiz-editor-body ol.wiz-list-level3 > li {list-style-type:lower-roman;}.wiz-editor-body li.wiz-list-align-style {list-style-position: inside; margin-left: -1em;}.wiz-editor-body blockquote {padding: 0 12px;}.wiz-editor-body blockquote > :first-child {margin-top:0;}.wiz-editor-body blockquote > :last-child {margin-bottom:0;}.wiz-editor-body img {border:0;max-width:100%;height:auto !important;margin:2px 0;}.wiz-editor-body table {border-collapse:collapse;border:1px solid #bbbbbb;}.wiz-editor-body td,.wiz-editor-body th {padding:4px 8px;border-collapse:collapse;border:1px solid #bbbbbb;min-height:28px;word-break:break-word;box-sizing: border-box;}.wiz-editor-body td > div:first-child {margin-top:0;}.wiz-editor-body td > div:last-child {margin-bottom:0;}.wiz-editor-body img.wiz-svg-image {box-shadow:1px 1px 4px #E8E8E8;}.wiz-hide {display:none !important;}
-->