如何编写 locust 的代码

一.编写 Locust 代码

locustfile是普通的python文件。唯一的要求是,它声明至少一个从类User继承的类。

1. 用户类别

一个用户类别代表一个用户(如果愿意,则代表一群Locust)。Locust将为每个正在模拟的用户生成User类的一个实例。 User类可以定义一些公共属性。

wait_time属性

除了task属性,还应该声明一个wait_time方法。它用于确定模拟用户在执行任务之间等待多长时间。 Locust带有一些内置函数,这些函数返回一些常见的wait_time方法。


最常见的是两者之间。它用于使模拟用户在每次执行任务后等待介于最小值和最大值之间的随机时间。其他内置的等待时间函数是constant和constant_pacing。


使用以下locustfile,每个用户将在任务之间等待5到15秒:

from locust import User, task, between
class MyUser(User):
    @task
    def my_task(self):
        print("executing my_task")

    wait_time = between(5, 15)

wait_time方法应返回秒数(或几分之一秒),并且也可以在TaskSet类上声明,在这种情况下,它将仅用于该TaskSet。

也可以直接在User或TaskSet类上声明自己的wait_time方法。例如,下面的User类将休眠一秒钟,然后休眠两个,然后休眠三个,依此类推。

class MyUser(User):
    last_wait_time = 0

    def wait_time(self):
        self.last_wait_time += 1
        return self.last_wait_time

    ...

2.weight属性

如果文件中存在多个用户类,并且在命令行上未指定任何用户类,则Locust将产生相等数量的每个用户类。您还可以通过将它们作为命令行参数传递,来指定要从同一locustfile中使用哪些用户类:

locust -f locust_file.py WebUser MobileUser

如果您希望模拟更多特定类型的用户,则可以在这些类上设置一个weight属性。举例来说,网络用户的可能性是移动用户的三倍:

class WebUser(User):
    weight = 3
    ...

class MobileUser(User):
    weight = 1
    ...

3. host 属性

host属性是要加载的主机的URL前缀(即“ http://google.com”)。通常,在Locust启动时,使用--host选项在Locust的Web UI或命令行中指定此选项。

如果在用户类中声明了主机属性,则在命令行或Web请求中未指定--host的情况下将使用该属性。


4.task 属性

用户类可以使用@task装饰器在其下声明为方法的任务,但是也可以使用task属性指定任务,这将在下面更详细地描述。


5. environment属性

对用户运行环境的引用。使用它与环境或其所包含的运行程序进行交互。例如。从任务方法中阻止跑步者:

self.environment.runner.quit()

如果在独立Locust实例上运行,则将停止整个运行。如果在工作程序节点上运行,它将停止该特定节点。


二.Tasks(任务)


启动负载测试后,将为每个模拟用户创建一个User类的实例,并且它们将在其自己的绿色线程中运行。这些用户运行时,他们选择执行的任务,休眠一会儿,然后选择一个新任务,依此类推。

这些任务是普通的python可调用对象,并且-如果我们正在对拍卖网站进行负载测试-则它们可以执行诸如“加载起始页”,“搜索某些产品”,“出价”之类的工作。

宣告任务

为用户类(或TaskSet)声明任务以使用任务装饰器的典型方法。

这是一个例子:

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带有可选的weight参数,可用于指定任务的执行率。在以下示例中,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

1.task 属性

使用@task装饰器声明任务是一种方便,通常是执行任务的最佳方法。但是,也可以通过设置task属性来定义User或TaskSet的任务(使用@task装饰器实际上只是填充task属性)。

task属性是Task的列表,或者是<Task:int> dict的列表,其中Task是可调用的python或TaskSet类(更多内容请参见下文)。如果任务是普通的python函数,则它们会收到一个参数,即执行任务的User实例。

这是声明为普通python函数的User任务的示例:

from locust import User, constant
def my_task(l):
    passclass MyUser(User):
    tasks = [my_task]
    wait_time = constant(1)

如果将task属性指定为列表,则每次执行任务时,都会从task属性中随机选择。但是,如果任务是字典-将可调用对象作为键,将整数作为值-将随机选择要执行的任务,但将整数作为比率。因此,执行以下任务:

{my_task: 3, another_task: 1}

my_task的执行可能性是another_task的3倍。


在内部,上面的dict实际上将扩展为如下所示的列表(并且task属性已更新):

[my_task, my_task, my_task, another_task]

然后使用Python的random.choice()从列表中选择任务。


2. 标记task

通过使用标记<locust.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 tag3启动它,则只会执行task2和task3。


--exclude-tags的行为完全相反。因此,如果使用--exclude-tags tag3开始测试,则将仅执行task1,task2和task4。排除总是胜于包容,因此,如果一个任务具有您已包含的标签和您已被排除的标签,则该任务将不会执行。


3.TaskSet类

由于真实的网站通常以分层的方式构建,包含多个子部分,因此Locust具有TaskSet类。蝗虫任务不仅可以是Python可调用的,还可以是TaskSet类。 TaskSet是蝗虫任务的集合,将像直接在User类上声明的任务一样执行,使用户在两次任务执行之间处于休眠状态。这是一个带有TaskSet的locustfile的简短示例:

class ForumSection(TaskSet):
    @task(10)
    def view_thread(self):
        pass

    @task(1)
    def create_thread(self):
        pass

    @task(1)
    def stop(self):
        self.interrupt()class LoggedInUser(User):
    wait_time = between(5, 120)
    tasks = {ForumSection:2}

    @task
    def index_page(self):
        pass

也可以使用@task装饰器直接在User / TaskSet类下内联TaskSet:

class MyUser(User):
    @task(1)
    class MyTaskSet(TaskSet):
        ...

TaskSet类的任务可以是其他TaskSet类,从而可以将它们嵌套任何数量的级别。这使我们能够定义一种行为,以更现实的方式模拟用户。

例如,我们可以使用以下结构定义TaskSet:

- Main user behaviour
  - Index page
  - Forum page
    - Read thread
      - Reply
    - New thread
    - View next page
  - Browse categories
    - Watch movie
    - Filter movies
  - About page

当正在运行的用户线程选择TaskSet类执行时,将创建该类的实例,然后执行将进入该TaskSet。然后发生的事情是将拾取并执行TaskSet的任务之一,然后线程将休眠一段时间,该持续时间由用户的wait_time函数指定(除非在TaskSet类上直接声明了wait_time函数,在这种情况下,它会“将使用该函数代替),然后从TaskSet的任务中选择一个新任务,然后再次等待,依此类推。


4.中断任务集

有关TaskSet的重要一件事是它们永远不会停止执行其任务,而将执行本身交还给其父级User / TaskSet。这必须由开发人员通过调用TaskSet.interrupt()方法来完成。

中断(自己,重新安排= True)

中断TaskSet并将执行控制移交给父TaskSet。

如果reschedule为True(默认),则父用户将立即重新安排并执行新任务。

在以下示例中,如果我们没有调用self.interrupt()的停止任务,则模拟用户一旦进入论坛任务集中就永远不会停止运行该任务:

    @task
    class Forum(TaskSet):
        @task(5)
        def view_thread(self):
            pass

        @task(1)
        def stop(self):
            self.interrupt()

    @task
    def frontpage(self):
        pass

使用中断功能,我们可以与任务权重一起定义模拟用户离开论坛的可能性。

5. TaskSet和User类中的任务之间的区别

与直接驻留在User下的任务相比,驻留在TaskSet下的任务的一个区别是,它们在执行时传递的参数(对于使用@task装饰器声明为方法的任务本身)是对TaskSet实例的引用。用户实例的可以通过TaskSet.user从TaskSet实例内部访问User实例。 TaskSet还包含一个便捷客户端属性,该属性引用User实例上的客户端属性。


6. 引用用户实例或父TaskSet实例

TaskSet实例的属性user指向其User实例,而属性parent指向其父TaskSet实例。


7.标签和任务集

您可以使用标记<locust.tag>装饰器标记TaskSet,方法与普通任务类似,如上面的<tagging-tasks>所述,但是有一些细微差别值得一提。标记TaskSet会将标记自动应用于所有TaskSet的任务。此外,如果您在嵌套TaskSet中标记任务,即使没有标记TaskSet,蝗虫也会执行该任务。


8.SequentialTaskSet类

SequentialTaskSet是一个TaskSet,但其任务将按照声明的顺序执行。 SequentialTaskSet类上的任务的权重将被忽略。可以将SequentialTaskSets嵌套在TaskSet中,反之亦然。

def function_task(taskset):
    passclass SequenceOfTasks(SequentialTaskSet):
    @task
    def first_task(self):
        pass

    tasks = [function_task]

    @task
    def second_task(self):
        pass

    @task
    def third_task(self):
        pass

在上面的示例中,任务以声明的顺序执行:

  1. first_task

  2. function_task

  3. second_task

  4. third_task

然后它将再次从first_task重新开始。


9. on_start和on_stop方法

用户和TaskSet类可以声明on_start方法和/或on_stop方法。用户开始运行时将调用它的on_start方法,而当用户停止运行时将调用on_stop方法。对于TaskSet,当模拟用户开始执行该TaskSet时将调用on_start方法,而当模拟用户停止执行该TaskSet时(调用interrupt()或杀死该用户)将调用on_stop。


10.test_start和test_stop事件

如果需要在负载测试的开始或停止时运行一些代码,则应使用test_start和test_stop事件。您可以在locustfile的模块级别为这些事件设置侦听器:

from locust import events
@events.test_start.add_listener
def on_test_start(**kwargs):
    print("A new test is starting")
    
@events.test_stop.add_listener
def on_test_stop(**kwargs):
    print("A new test is ending")

运行Locust分发时,只会在主节点中触发test_start和test_stop事件。

11.初始化事件

在每个Locust过程的开始时都会触发init事件。这在分布式模式下特别有用,在分布式模式下,每个工作进程(而非每个用户)都需要机会进行一些初始化。例如,假设您具有某种全局状态,则从该过程中产生的所有用户都需要:

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")

.发出Http请求

到目前为止,我们仅介绍了用户的任务计划部分。为了实际负载测试系统,我们需要发出HTTP请求。为了帮助我们做到这一点,存在HttpUser类。使用此类时,每个实例都会获得一个客户端属性,该属性将是HttpSession的一个实例,可用于发出HTTP请求。

classHttpUser(* args,** kwargs)

表示要产生并攻击要进行负载测试的系统的HTTP“用户”。

该用户的行为由其任务定义。可以通过在方法上使用@task装饰器或通过设置task属性直接在类上声明任务。

此类在实例化时创建一个客户端属性,该属性是一个HTTP客户端,支持在请求之间保持用户会话。

客户=无

在Locust实例化后创建的HttpSession实例。客户端支持cookie,因此保持HTTP请求之间的会话。

从HttpUser类继承时,我们可以使用其client属性对服务器发出HTTP请求。这是一个蝗虫文件的示例,可用于对具有两个URL的站点进行负载测试。 /和/ about /:

from locust import HttpUser, task, between
class MyUser(HttpUser):
    wait_time = between(5, 15)

    @task(2)
    def index(self):
        self.client.get("/")

    @task(1)
    def about(self):
        self.client.get("/about/")

使用上面的User类,每个模拟用户将在请求之间等待5到15秒,并且/被请求的次数是/ about /的两倍。


使用HTTP客户端

HttpUser的每个实例在client属性中都有一个HttpSession实例。 HttpSession类实际上是request.Session的子类,可用于发出HTTP请求,并将使用get,post,put,put,delete,head,patch和options方法将这些请求报告给用户的统计信息。 HttpSession实例将在请求之间保留cookie,以便可用于登录网站并在请求之间保持会话。也可以从User实例的TaskSet实例中引用client属性,以便轻松地从客户端中检索客户端并发出HTTP请求。


这是一个简单的示例,它向/ about路径发出GET请求(在这种情况下,我们假设self是TaskSet或HttpUser类的实例:

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"})


1.安全模式

HTTP客户端配置为以safe_mode运行。这样做是由于连接错误,超时或类似原因而失败的任何请求都不会引发异常,而是返回一个空的虚拟Response对象。该请求将在用户统计信息中报告为失败。返回的虚拟Response的content属性将设置为None,其status_code将为0。


2.手动控制请求是成功还是失败

如果HTTP响应代码正常(<400),则请求被视为成功,但是对响应进行一些其他验证通常很有用。


您可以使用catch_response参数,带声明和对r的调用将请求标记为失败

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()

3.使用动态参数将对请求的分组

网站上的网址中包含某种动态参数是很常见的。通常,将这些网址归为“用户”统计信息是有意义的。这可以通过将名称参数传递给HttpSession的不同请求方法来完成。

# Statistics for these requests will be grouped under: /blog/?id=[id]
for i in range(10):
    self.client.get("/blog?id=%i" % i, name="/blog?id=[id]")


4.HTTP代理设置

为了提高性能,我们通过将request.Session的trust_env属性设置为False,将请求配置为不在环境中查找HTTP代理设置。如果您不希望这样做,可以手动将locust_instance.client.trust_env设置为True。有关更多详细信息,请参阅请求文档。


5.如何构建测试代码

请记住,locustfile.py只是Locust导入的普通Python模块,这一点很重要。从该模块中,您可以像在任何Python程序中一样正常地导入其他python代码。当前的工作目录会自动添加到python的sys.path中,因此可以使用python import语句导入工作目录中的所有python文件/模块/软件包。

对于小型测试,将所有测试代码保存在一个locustfile.py中应该可以正常工作,但是对于大型测试套件,您可能需要将代码拆分为多个文件和目录。

当然,如何构造测试源代码完全取决于您,但是我们建议您遵循Python最佳实践。这是一个虚构的Locust项目的示例文件结构:

  • 项目根目录

    • __init__.py

    • auth.py

    • config.py

    • common/

    • locustfile.py

    • requirements.txt (外部Python依赖项通常保存在requirements.txt中)

具有多个不同locustfiles的项目也可以将它们保存在单独的子目录中

  • Project root

    • api.py

    • website.py

    • __init__.py

    • auth.py

    • config.py

    • common/

    • locustfiles/

    • requirements.tx

使用上述任何项目结构,您的locustfile都可以使用以下方法导入公共库:

import common.auth


全部评论(0)