性能测试工具Locust–(2)编写locustfile
- 2020 年 3 月 4 日
- 筆記
locustfile是普通的python文件。唯一的要求是至少声明一个类(我们称之为locust类),该类继承自locust类。
Locust类
一个Locust类代表一个用户(或者一个集群Locust)。Locust将为每个正在模拟的用户生成(孵化)一个Locust类实例。Locust类通常应该定义一些属性。
task_set属性
task_set属性应该指向一个TaskSet类,这个类定义了用户的行为,下面将对其进行更详细的描述。
wait_time属性
除了task_set属性,还应该声明一个wait_time
方法。它用于确定模拟用户在执行任务之间将等待多长时间。Locust提供了一些内置的函数,返回一些常用的wait_time方法。
最常见的是 between
。它用于使模拟用户在每次执行任务后等待介于最小值和最大值之间的随机时间。其他内置的等待时间函数是constant
和constant_pacing
。
使用以下locustfile,每个用户将在任务之间等待5到15秒:
from locust import Locust, TaskSet, task, between class MyTaskSet(TaskSet): @task def my_task(self): print("executing my_task") class User(Locust): task_set = MyTaskSet wait_time = between(5, 15)
wait_time方法应该返回秒数(或几分之一秒),也可以在TaskSet类上声明,在这种情况下,它将仅用于该TaskSet。
也可以直接在Locust或TaskSet类上声明自己的wait_time方法。接下来的Locust类将开始休眠1秒钟,然后休眠1秒,2秒,3秒,等等。
class MyLocust(Locust): task_set = MyTaskSet last_wait_time = 0 def wait_time(self): self.last_wait_time += 1 return self.last_wait_time
weight (权重)属性
如果文件中存在多个locust类,并且命令行中没有指定locust,则每个新生成的locust将从现有locust中随机选择。否则,你可以从相同的文件中像下面这样指定使用哪个locust: $ locust -f locust_file.py WebUserLocust MobileUserLocust
如果你想让这些locust中的一个执行得更频繁,你可以给这些类设置一个权重属性。例如,Web用户是mobile用户的三倍:
class WebUserLocust(Locust): weight = 3 ... class MobileUserLocust(Locust): weight = 1 ...
host属性
host属性host属性是要加载的URL前缀(https://cn.bing.com);通常,是在Locust的Web UI或命令行中指定的,在启动Locust时使用--host
。 如果在locust类中声明了一个host属性,则在命令行或Web请求中未指定--host
的情况下将使用该属性。。
TaskSet类
如果Locust类代表蝗虫群,则可以说TaskSet类代表蝗虫的大脑。每个Locust类必须设置一个task_set属性,该属性指向TaskSet。
顾名思义,TaskSet是任务的集合。这些任务是普通的python可调用对象,并且,如果我们正在对拍卖网站进行负载测试,则可以完成诸如“加载起始页”,“搜索某些产品”和“竞标”之类的工作。
启动负载测试时,派生的Locust类的每个实例将开始执行其TaskSet。接下来的情况是每个TaskSet将选择一个任务并调用它。然后,它将等待Locust类的wait_time方法指定的秒数(除非已直接在TaskSet上声明了wait_time方法,在这种情况下,它将使用自己的方法)。 然后它将再次选择要调用的新任务,再次等待,依此类推。
声明任务
TaskSet声明任务的典型方式是使用task
装饰器。
这里有一个例子:
from locust import Locust, TaskSet, task class MyTaskSet(TaskSet): @task def my_task(self): print("Locust instance (%r) executing my_task" % (self.locust)) class MyLocust(Locust): task_set = MyTaskSet
@task使用可选的weight参数,该参数可用于指定任务的执行率。在以下示例中,task2的执行量是task1的两倍:
from locust import Locust, TaskSet, task from locust.wait_time import between class MyTaskSet(TaskSet): wait_time = between(5, 15) @task(3) def task1(self): pass @task(6) def task2(self): pass class MyLocust(Locust): task_set = MyTaskSet
tasks属性
使用@task装饰器来声明任务是一种方便的方法,通常也是最好的方法。但是,也可以通过设置tasks
属性来定义TaskSet的任务(使用@task装饰器实际上只填充tasks属性)。
tasks属性要么是python可调用项的列表,要么是 字典。这些任务是接收一个参数的python可调用函数——正在执行任务的TaskSet类实例。 这些任务是接收一个参数的python可调用函数——正在执行任务的TaskSet类实例。
from locust import Locust, TaskSet def my_task(l): pass class MyTaskSet(TaskSet): tasks = [my_task] class MyLocust(Locust): task_set = MyTaskSet
如果将tasks属性指定为列表,那么每次执行任务时,都将从tasks属性中随机选择该任务。但是,如果任务是字典(将可调用对象作为键,将整数作为值),则将随机选择要执行的任务,但将int值作为比率。因此,任务看起来像这样: {my_task: 3, another_task: 1}
my_task被执行的可能性是other_task的3倍。
TaskSets可嵌套
TaskSet的一个非常重要的特性是它们可以嵌套,因为真实的网站通常以分层的方式构建,包含多个子部分。因此,嵌套任务集将允许我们定义一种行为,以更现实的方式来模拟用户。例如,我们可以使用以下结构定义· – —– TaskSet:
- Main user behaviour
- Watch movie
- Filter movies
- Read thread
- Reply
- New thread
- View next page
- Index page
- Forum page
- Browse categories
- About page
嵌套TaskSet的方式就像使用task属性指定任务时一样,但不是引用python函数,而是引用另一个TaskSet:
class ForumPage(TaskSet): @task(20) def read_thread(self): pass @task(1) def new_thread(self): pass @task(5) def stop(self): self.interrupt() class UserBehaviour(TaskSet): tasks = {ForumPage:10} @task def index(self): pass
因此,在上面的示例中,如果在执行UserBehaviour TaskSet时选择了要执行的ForumPage,则ForumPage TaskSet将开始执行。然后,ForumPage TaskSet将选择其自己的任务之一,执行它,等待,依此类推。
关于上述示例,需要注意一点,那就是在ForumPage的stop方法中调用self.interrupt()。这样做实际上是停止执行ForumPage任务集,并在UserBehaviour实例中继续执行。如果我们在ForumPage的某处没有调用interrupt()方法,Locust将永远不会停止运行已经启动的ForumPage任务。但是通过使用中断功能,我们可以与任务权重一起定义模拟用户离开论坛的可能性。
也可以使用@task装饰器在类中内联声明嵌套的TaskSet,就像声明普通任务时一样:
class MyTaskSet(TaskSet): @task class SubTaskSet(TaskSet): @task def my_task(self): pass
引用Locust实例,或父TaskSet实例
TaskSet实例的属性locust指向它的locust实例,属性parent指向它的父TaskSet(它将在基本TaskSet中指向Locust实例)。
TaskSequence类
TaskSequence类是一个TaskSet,但是它的任务是按顺序执行的。 要定义此顺序,您应该执行以下操作:
@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
在上面的示例中,执行顺序定义为先执行first_task,然后执行second_task,最后执行Third_task 10次。可以看到,可以使用@task装饰器组合@seq_task,当然也可以将TaskSet嵌套在TaskSequences中,反之亦然。
Setups, Teardowns, on_start, 和on_stop
Locust还支持Locust级setup
和teardown
,TaskSet级setup
和teardown
,TaskSet
'、 on_start
和on_stop
.
Setups 和 Teardowns
setup
和teardown
,无论是在Locust
还是TaskSet
上运行,都是只运行一次的方法。setup
是在任务开始运行之前运行,而teardown
是在所有任务完成并退出Locust之后运行的。这使你能够在Locust任务运行之前执行一些准备工作(如创建数据库),并在Locust退出之前进行清理(如删除数据库)。
要使用它,只需在Locust或TaskSet类上声明一个setup或teardown。这些方法将为你运行。
on_start和on_stop方法
TaskSet类可以声明on_start
方法或on_stop
方法。 当模拟用户开始执行该TaskSet类时,将调用on_start
方法;而当TaskSet停止时,将调用on_stop <locust.core.TaskSet.on_stop()
方法。
事件顺序
由于许多设置和清除操作是相互依赖的,因此以下是它们的执行顺序:
- Locust setup (一次)
- TaskSet setup (一次)
- TaskSet on_start (每个locust一次)
- TaskSet tasks…
- TaskSet on_stop (每个locust一次)
- TaskSet teardown (一次)
- Locust teardown (一次) 通常,setup和teardown方法应该是互补的。
发送HTTP请求
到目前为止,我们仅介绍了Locust用户的任务调度部分。为了实际测试系统,我们需要发送HTTP请求。为了帮助我们做到这一点,存在HttpLocust
类。当使用这个类时,每个实例获得一个client属性,该属性将是HttpSession的一个实例,可用于发送HTTP请求。
HttpLocust类
表示一个HTTP“用户”,该“用户”将被孵化并攻击要加载测试的系统。 此用户的行为由task_set属性定义,该属性应指向TaskSet
类。 该类在实例化时创建一个client属性,该属性是一个HTTP client ,支持在请求之间保持用户会话。
client= None
在Locust实例化后创建的HttpSession实例。客户端支持cookie,因此在HTTP请求之间的会话。
在继承HttpLocust类时,我们可以使用它的client属性对服务器发出HTTP请求。 这是一个Locust文件的例子,可以用两个URL负载测试站点 ;/ 和 /about/:
from locust import HttpLocust, TaskSet, task, between class MyTaskSet(TaskSet): @task(2) def index(self): self.client.get("/") @task(1) def about(self): self.client.get("/about/") class MyLocust(HttpLocust): task_set = MyTaskSet wait_time = between(5, 15)
使用上面的Locust类,每个模拟用户将在请求之间等待5到15秒,并且 / 被请求的时间将是 /about/ 的两倍。 细心的读者会发现奇怪的是,我们可以使用TaskSet中的self.client而不是self.locust.client来引用HttpSession实例。我们可以这样做是因为TaskSet
类有一个方便的属性client,该属性仅返回self. custt .client。
使用HTTP客户端
HttpLocust的每个实例在client属性中都有一个HttpSession
实例。HttpSession类实际上是request.Session
的子类,可用于发出HTTP请求,该请求将使用get,post,put,put,delete,head,patch和options方法将其统计数据报给Locust。HttpSession实例将在请求之间保存cookie,以便用于登录网站并在请求之间保持会话。也可以从Locust实例的TaskSet实例中引用client属性,以便轻松地检索客户端并发出HTTP请求。
下面是一个简单的示例,它向 / about 路径发出GET请求(在这种情况下,我们假设self是TaskSet
或HttpLocust
类的实例:
response = self.client.get("/about") print("Response status code:", response.status_code) print("Response content:", response.text)
这里有一个POST请求的例子:
response = self.client.post("/login", {"username":"testuser", "password":"secret"} )
安全模式
HTTP客户端配置为以safe_mode运行。这样做的目的是,由于连接错误、超时或类似原因而失败的任何请求都不会引发异常,而是返回一个空的虚拟Response对象。该请求将在Locust的统计信息中标记为失败。返回的虚拟Response的content属性将设置为None,其status_code=0。
手动控制请求是成功还是失败
默认情况下,除非HTTP响应代码为OK(<400),否则请求将被标记为失败的请求。大多数情况下,此默认值是你想要的。但是,有时(例如,在测试URL端点时,你期望返回404,或者在测试一个设计糟糕的系统时,即使出现错误也可能返回200 OK)——需要手动控制Locust是否应该将请求标记为成功或失败。
通过使用catch_response参数和with语句,即使响应代码正确,也可以将请求标记为失败:
with self.client.get("/", catch_response=True) as response: if response.content != b"Success": response.failure("Got wrong response")
正如可以将具有OK响应代码的请求标记为失败一样,也可以将catch_response参数与with语句一起使用,以标记导致HTTP错误代码的请求在统计中仍被报告为成功:
with self.client.get("/does_not_exist/", catch_response=True) as response: if response.status_code == 404: response.success()
将带有动态参数的URL请求分组
网站的url包含一些动态参数的页面是很常见的。通常在Locust的统计信息中将这些URL分组在一起是很有意义的。这可以通过将名称参数传递给HttpSession的不同请求方法来完成。
例如:
# 这些请求的统计数据将归入以下类别: /blog/?id=[id] for i in range(10): self.client.get("/blog?id=%i" % i, name="/blog?id=[id]")
公共库–Common libraries
通常,将共享公共库的多个locustfiles分组。在这种情况下,在项目中根目录定义为调用Locust的目录非常重要,建议所有的locust文件都位于项目根目录下。
平面文件结构如下:
- project root
commonlib_config.py
commonlib_auth.py
locustfile_web_app.py
locustfile_api.py
locustfile_ecommerce.py
locustfile可以使用以下命令导入公共库,例如:import commonlib_auth
。但是,这种方法并不能清楚地将公共库与Locust文件分开。
子目录可以是一种更简洁的方法(请参见下面的示例),但locust只导入与运行的locustfile所在目录相关的模块。如果希望从项目根目录(即运行locust命令的位置)导入,请确保在loucst文件导入任何公共库之前编写sys.path.append(os.getcwd())
,这将使项目根目录(即当前工作目录)可导入。
- project root
__init__.py
common/
- `__init__.py`
- `config.py`
- `auth.py`
locustfiles/
- `__init__.py`
- `web_app.py`
- `api.py`
- `ecommerce.py`
通过以上项目结构,locust文件可以使用以下方法导入公共库:
sys.path.append(os.getcwd()) import common.auth