文章目录
编写一个locustfile
现在,让我们看一个更完整/更真实的测试示例:
import time
from locust import HttpUser, task, between
class QuickstartUser(HttpUser):
wait_time = between(1, 5)
@task
def hello_world(self):
self.client.get("/hello")
self.client.get("/world")
@task(3)
def view_items(self):
for item_id in range(10):
self.client.get(f"/item?id={item_id}", name="/item")
time.sleep(1)
def on_start(self):
self.client.post("/login", json={"username":"foo", "password":"bar"})
代码分解解读
import time
from locust import HttpUser, task, between
locust文件只是一个python的普通库,它可以通过import
代码来引用其他文件或包。
class QuickstartUser(HttpUser):
这里我们定义了class类来实现用户模拟操作。该类继承了HttpUser
类,HttpUser
为每个用户提供一个client
属性,该属性是HttpSession
的一个实例,可用于向我们要负载测试的目标系统发出HTTP请求。当开始测试时,locust将为每个模拟用户创建一个此类的实例,每个用户都在各自的协程中运行。
要使得测试case文件成为有效的locastfile,它必须至少包含一个从User继承的类。
wait_time = between(1, 5)
在我们的类中定义了一个wait_time
用来模拟每个任务被执行的时间间隔在1到5秒之间。更多详细信息参考wait_time
属性章节。
@task
def hello_world(self):
...
用@task
修饰的方法是locustfile的核心。对于每个正在运行的用户,Locust都会创建一个greenlet
(微线程/协程),它将调用这些方法。
@task
def hello_world(self):
self.client.get("/hello")
self.client.get("/world")
@task(3)
def view_items(self):
...
我们用@task
修饰了两个方法hello_world()
、view_items()
,声明了两个任务,其中一个方法的权重更高(3)。当我们的QuickstartUser
运行时,它将从已经声明的任务(在本例中是hello_world
、view_items
)中选择一个来执行。任务的选择是随机的,但你可以赋予它们不同的权重。上述配置将使Locust选择view_items
的可能性是hello_world
的三倍。当一个任务完成执行后,用户将在等待时间(本例是1到5秒)内休眠。在等待时间过后,它会选择一个新任务并不断重复以上操作。
请注意,只有用@task
修饰的方法才会被选中,因此你可以任意自定义内部助手方法。
self.client.get("/hello")
self.client
属性使得它可以执行HTTP调用并被Locust记录。有关如何发出其他类型的请求、验证响应等的信息,请参考使用HTTP客户端
。
批注
HttpUser
不是真正的浏览器,因此不会解析HTML响应来加载资源或呈现页面。不过它会追踪cookie
信息。
@task(3)
def view_items(self):
for item_id in range(10):
self.client.get(f"/item?id={item_id}", name="/item")
time.sleep(1)
在view_items
任务中,我们使用变量查询参数加载10个不同的URL。为了不在Locust的统计数据中获得10个单独的条目-因为统计数据是在URL上分组的-我们使用name
参数将所有这些请求分组到名为/item
的条目下。
def on_start(self):
self.client.post("/login", json={"username":"foo", "password":"bar"})
另外我们声明了一个on_start
方法。当每个模拟用户启动时都会调用此名称的方法。更多信息参考on_start and on_stop 方法
。
自动生成locustfile
你可以通过har2locust
将浏览器录制的HAR文件自动生成locustfile。
它对于不习惯于编写自己的locustfile的初学者特别有用,但对于更高级的用例来说,它也是高度可定制的。
批注
har2locust
正处于测试阶段。它可能并不一定总能生成正确的locustfile,并且它的接口可能会在不同版本之间发生变化。
User类
一个user
类表示系统里的一种用户\场景
的类型。当你执行测试时,运行指定要模拟的并发用户数,Locust会为每个用户创建一个实例。你可以为这些类/实例
添加任何你想使用的属性,不过有些属性对Locust是有特殊意义的。如:
wait_time属性
User
类的wait_time
方法实现了执行每个任务之间的延时控制。如果未指定wait_time,则任务一完成就立即执行下一个任务。
constant
: 指定一个固定的时间值between
: 在指定最小值到最大值的范围随机获取一个值
例如,要让每个用户在每次任务执行之间等待0.5到10秒:
from locust import User, task, between
class MyUser(User):
@task
def my_task(self):
print("executing my_task")
wait_time = between(0.5, 10)
constant_throughput
: 给定一个时间X以确保任务每秒运行(最多)X次。constant_pacing
: 给定一个时间X以确保每X秒运行一次(这个是constant_throughput的倒数)
批注
例如,如果你希望Locust在峰值负载下每秒运行500次任务迭代,可以使用mathematical inverse
和5000个用户数。
等待时间只能限制吞吐量,不能通过启动新用户来达到目的。因此,在我们的示例中,如果任务执行时间间隔超过10秒,那么对应的吞吐量将小于500。
等待时间是在任务执行后应用的,因此,如果有较高的繁殖率/加速,则可能会在加速过程中超过目标。
等待时间适用于任务,而不是请求。例如,如果规定了wait_time = constant_throughput(2)
和在任务中执行两个请求,则请求率(RPS)为4/User1。
也可以在自定义的类中声明自定义的wait_time
方法。例如,下面的MyUser类将休眠一秒钟,然后是两秒钟,然后三秒钟,等等。
class MyUser(User):
last_wait_time = 0
def wait_time(self):
self.last_wait_time += 1
return self.last_wait_time
...
weight和fixed_count属性
如果文件中存在多个用户类,并且命令行中未指定任何用户类,Locust将为每个用户类生成相同数量负载。还可以通过命令行传递参数的方式指定locustfile中的用户类名称来执行,如:
$locust -f locust_file.py WebUser MobileUser
如果希望模拟更多特定类型的用户,可以在这些类上设置权重属性。例如,WebUser
的可能性是MobileUser
的三倍:
class WebUser(User):
weight = 3
...
class MobileUser(User):
weight = 1
...
还可以设置fixed_count
属性。在这种情况下,权重属性将被忽略,并且将产生精确的用户数。首先生成这些用户。在下面的示例中,将只生成一个AdminUser
实例,以使某些特定的工作能够更准确地控制请求数,而不依赖于总用户数。
class AdminUser(User):
wait_time = constant(600)
fixed_count = 1
@task
def restart_app(self):
...
class WebUser(User):
...
host属性
host
属性是被测系统的一个URL前缀(如: “http://google.com”)。通常这是在Locust的web UI中或在命令行启动Locust时使用–host参数来指定的。
如果在用户类中声明了host
属性,则在命令行或web请求中未指定--host
的情况下,将默认使用该属性的值。
tasks属性
User
类可以使用@task
装饰器将任务声明为它下面的方法,但也可以使用tasks
属性指定任务,下面将详细介绍该属性使用。
environment属性
在用户运行时使用environment
属性可实现运行环境的交互,例如通过runner
属性控制阻止一个任务方法的执行:
self.environment.runner.quit()
如果在独立的Locust实例上运行,这将停止整个运行。如果在工作节点上运行,它将停止该特定节点。
on_start和on_stop方法
Users
类(TaskSets
类)可以声明on_start
方法和on_stop
方法。一个User
类当它开始运行时将会调用on_start
方法以及当它停止运行时on_stop
方法被调用。对于TaskSet
类,当一个模拟用户开始执行TaskSet
类时将会调用on_start
方法以及当模拟用户停止执行TaskSet
类(当interrupt()
被调用或运行用户被关闭)时on_stop
方法被调用。
Tasks类
当负载测试开始时,将为每个模拟用户创建User类的实例,并它们将在自己的协程中开始运行。当这些用户运行时,选择自己执行的任务,休眠一会,然后选择一个新任务继续执行,依此类推。
这些任务是python普通的可调用程序,比如我们正在对一个拍卖网站做负载测试,可以将以下的操作当作任务来定义,就像“加载一个起始页面”、“搜索商品”、“出价”等操作。
@task 装饰器
为用户添加任务的最简单方法是使用task
修饰符。
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
采用可选的权重参数,可用于指定任务的执行率。在下面的示例中,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
属性要么是一个Tasks
列表,要么是一个<Task: int>
字典,其中Task
要么是一个python可调用类,要么是一个TaskSet
类。如果task
是一个普通的python函数,那么它将会接收一个参数,即为正在执行任务的User
类实例。
下面是一个声明为普通python函数的User
任务的示例:
from locust import User, constant
def my_task(user):
pass
class MyUser(User):
tasks = [my_task]
wait_time = constant(1)
如果tasks-
属性被定义为一个列表,则每次执行任务时,都会从tasks
属性列表中随机选择一个来执行。然而如果tasks
属性是一个字典(则可调用对象作为key
值、int权重值为value
),将随机选择要执行的任务,但将int作为权重比率。就像下面的示例那样:
{my_task: 3, another_task: 1}
my_task
被执行的机率是another_task
的3倍。
而实际上上面的示例在内部底层处理会被处理成列表(同时task
属性也会被更新)为这样
[my_task, my_task, my_task, another_task]
然后通过python的random.choice()
从列表中随机选择一个任务执行。
@tag 装饰器
通过使用@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 tags3
启动测试,则只有task2
和task3
会被执行。
--exclude-tags
是以完全相反的方式来执行。如果以--exclude-tags tag3
来启动测试,则只有task1
、task2
、task4
会被执行。根据排除总是胜于包含的原则,因此,如果一个任务同时包含一个已包含的标记和一个已排除的标记,则不会执行该任务。
Events类
如果希望运行一些设置代码作为测试的一部分,通常可以将其放在locustfile的模块级别,但有时需要在运行中的特定时间执行一些操作。为实现这个需求,Locust提供了事件钩子。
test_start和test_stop
如果你需要在负载测试启动或停止时运行一些特定代码,可以使用test_start
和test_stop
事件。你可以为这些事件设置监听代码放在locustfile的模块级别:
from locust import events
@events.test_start.add_listener
def on_test_start(environment, **kwargs):
print("A new test is starting")
@events.test_stop.add_listener
def on_test_stop(environment, **kwargs):
print("A new test is ending")
init
init
事件在每个Locust进程开始时触发。这在分布式模式下尤其有用,因为每个工作进程(而不是每个用户)都需要一次初始化的机会。例如,假设你有一个全局状态,所有从该进程派生的用户都需要该状态:
from locust import events
from locust.runners import MasterRunner
@events.init.add_listener
def on_locust_init(environment, **kwargs):
if isinstance(environment.runner, MasterRunner):
print("I'm on master node")
else:
print("I'm on a worker or standalone node")
其他事件
请参考使用locust事件钩子扩展以获取其他事件以及有关如何使用它们的更多示例。
HttpUser类
HttpUser
类是最常使用的User
类。它添加了一个用于发送HTTP请求的client
属性。
from locust import HttpUser, task, between
class MyUser(HttpUser):
wait_time = between(5, 15)
@task(4)
def index(self):
self.client.get("/")
@task(1)
def about(self):
self.client.get("/about/")
client属性/HttpSession类
client
属性是HttpSession
类的一个实例。HttpSession
类是requests.Session
的子类/包装器,因此它的特性有很多文档记录,很多人都应该很熟悉。HttpSession
添加的主要是向Locust报告请求结果(成功/失败、响应时间、响应长度、名称)。
它包含所有HTTP方法的方法: get
,post
,put
,…
就像requests.Session
类那样,它在请求之间保存了cookie,因此可以很容易地用于登录网站。
response = self.client.post("/login", {"username":"testuser", "password":"secret"})
print("Response status code:", response.status_code)
print("Response text:", response.text)
response = self.client.get("/my-profile")
HttpSession捕获任何requests.RequestException
抛出的异常(如由连接错误、超时或类似原因引起),而不是返回一个虚拟的Response对象,其中status_code
设置为0
,content
设置为None
。
响应验证
如果HTTP的响应代码是OK(<400)则会认为请求成功,但是常常需要对响应做一些其他条件的验证。
通过使用catch_response
参数、with-语句
和对response.failure()
的调用结果来将请求标记为失败。如:
with self.client.get("/", catch_response=True) as response:
if response.text != "Success":
response.failure("Got wrong response")
elif response.elapsed.total_seconds() > 0.5:
response.failure("Request took too long")
即使响应代码错误,也可以将请求标记为成功:
with self.client.get("/does_not_exist/", catch_response=True) as response:
if response.status_code == 404:
response.success()
你甚至可以通过在with代码块外面捕获抛出的异常来避开记录请求结果。或者可以抛出一个Locust异常,就像下面示例那样,让Locust来捕获它。
from locust.exception import RescheduleTask
...
with self.client.get("/does_not_exist/", catch_response=True) as response:
if response.status_code == 404:
raise RescheduleTask()
REST/JSON APIs
下面是如何调用REST API
并验证响应的示例:
from json import JSONDecodeError
...
with self.client.post("/", json={"foo": 42, "bar": None}, catch_response=True) as response:
try:
if response.json()["greeting"] != "hello":
response.failure("Did not get expected value in greeting")
except JSONDecodeError:
response.failure("Response could not be decoded as JSON")
except KeyError:
response.failure("Response did not contain expected key 'greeting'")
Locust插件有一个现成的测试REST API
的类,叫做RestUser
。
分组请求
网站的一些页面通过url的动态参数来获取是很常见的。通常在User的统计信息中会将这些进行url分组统计。这可以通过将name
参数传递给HttpSession
的不同请求方法来实现。
示例:
# 这些请求的统计数据将归为: /blog/?id=[id]
for i in range(10):
self.client.get("/blog?id=%i" % i, name="/blog?id=[id]")
可能在某些场景下不能将参数传递到请求函数中,如和封装了请求会话的lib库/SDK包交互时。对请求进行分组的另一种方法是通过client.request_name
属性来设置。
# 这些请求的统计数据将归为: /blog/?id=[id]
self.client.request_name="/blog?id=[id]"
for i in range(10):
self.client.get("/blog?id=%i" % i)
self.client.request_name=None
如果想在同一个locustfile里链接多个分组,可以使用client.rename_request()
上下文管理器。
@task
def multiple_groupings_example(self):
# Statistics for these requests will be grouped under: /blog/?id=[id]
with self.client.rename_request("/blog?id=[id]"):
for i in range(10):
self.client.get("/blog?id=%i" % i)
# Statistics for these requests will be grouped under: /article/?id=[id]
with self.client.rename_request("/article?id=[id]"):
for i in range(10):
self.client.get("/article?id=%i" % i)
HTTP代理设置
为了提高性能,我们通过配置requests.Session
的trust_env
属性值为false
来使得请求配置为不查找环境中的HTTP代理设置。如果不想这样做,可以手动设置locust_instance.client.trust_env
的值为True。有关详细信息,请参考requests文档。
连接池
当每个HttpUser
创建新的HttpSession
时,每个用户实例都有自己的连接池。这与真实用户与web服务器交互的方式类似。
然而,如果你想在所有用户中共享连接,可以使用单个池管理器。将pool_manager
类属性设置为urllib3.PoolManager
的一个实例。
from locust import HttpUser
from urllib3 import PoolManager
class MyUser(HttpUser):
# All users will be limited to 10 concurrent connections at most.
pool_manager = PoolManager(maxsize=10, block=True)
有关更多配置选项,请参考urllib3文档。
TaskSets
TaskSets
是一种构建分层网站/系统测试的方法。可以通过TaskSet
类了解更多。
如何构造测试代码
重要的是要记住locustfile.py
只是一个导入了Locust模块的python文件。从这个模块中,你可以像在任何python程序那样引入其他python代码。当前工作目录会自动添加到python的sys.path
,因此可以使用pythonimport
语句导入工作目录中的任何python文件/模块/包。
对于小型的测试,将所有测试代码保存在单个locustfile.py
中应该可以,但对于较大的测试套件,可能需要将代码分割到多个文件和目录中。
当然,如何构造测试源代码完全取决于你,但我们建议你遵循Python最佳实践。下面是一个虚构的Locust项目的示例文件结构:
* Project Root
* common/
* __init__.py
* auth.py
* config.py
* locustfile.py
* requirements.txt (外部python依赖包通常都存放在requirements.txt中)
具有多个locostfile的项目也可以将它们保存在单独的子目录中:
* common/
* __init__.py
* auth.py
* config.py
* my_locustfiles/
* api.py
* website.py
* requirements.txt
使用以上任何一个项目结构,你的locustfile可以使用以下方法导入公共库:
import common.auth
constant_throughput(2)等同于每秒最大执行2次,每个任务有个2个请求,则RPS=请求数/任务*最大执行数=2x2=4 ↩︎