Python之Locust官方文档翻译02

发布时间 2023-10-18 18:05:23作者: hook_what

Writing a 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 模块,它可以从其他文件或包中导入代码。

class QuickstartUser(HttpUser):

这里,我们为将要模拟的用户定义了一个类。它从 HttpUser 继承而来,HttpUser 为每个用户提供一个 client 属性,这是 HttpSession 的一个实例,可用于向我们想要加载测试的目标系统发出 HTTP 请求。当测试启动时,locust 将为它模拟的每个用户创建此类的一个实例,并且每个用户都将开始在自己的绿色 gevent 线程内运行。

要使文件成为有效的 locustfile,它必须至少包含一个从 User 继承的类。

wait_time = between(1, 5)

我们的类定义了一个 wait_time,它将使模拟用户在每个任务(见下文)执行后等待1到5秒钟。有关更多信息,请参见 wait_time属性

@task
def hello_world(self):
    ...

@task修饰的方法是 locust 文件的核心。对于每个运行的用户,Locust 创建一个 greenlet (微线程) ,它将调用这些方法。

@task
def hello_world(self):
    self.client.get("/hello")
    self.client.get("/world")

@task(3)
def view_items(self):
...

我们通过用@task修饰两个方法来声明两个任务,其中一个方法被赋予了更高的权重(3)。当我们的 QuickstartUser运行时,它将选择一个声明的任务(在本例中是 hello_worldview_item)并执行它。任务是随机挑选的,但是你可以给它们不同的权重。上述配置将使 Locust 选择 view_item 的可能性是 hello_world 的三倍。当任务完成执行后,用户将在等待期间休眠(在本例中为1到5秒)。等待时间过后,它会选择一个新任务并不断重复这个任务。

注意,只有用@task修饰的方法才会被选中,因此您可以按照自己喜欢的方式定义自己的内部助手方法。

self.client.get("/hello")

self.client属性使得能够进行将由 Locust 记录的 HTTP 调用。有关如何发出其他类型请求、验证响应等的信息,请参见Using the HTTP Client

注意:HttpUser 不是真正的浏览器(browser),因此不会解析 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_item 任务中,我们使用一个可变的查询参数来加载10个不同的 URL。但是为了不在 Locust 的统计数据中获得10个单独的条目(因为统计数据是在 URL 上分组的),我们使用列 name参数将所有这些请求分组在一个名为"/item"的条目下。

def on_start(self):
    self.client.post("/login", json={"username":"foo", "password":"bar"})

此外,我们还声明了一个 on_start 方法。当每个模拟用户启动时,会先调用具有此名称的方法,然后再执行其他任务。有关更多信息,请参见 on_start 和 on_stop 方法

Auto-generating a locustfile

您可以使用 har2locust 基于浏览器记录(HAR-file)生成 locustfiles。

它对于不习惯编写自己的 locustfile 的初学者特别有用,而且对于更高级的用例也是高度可定制的。

Har2locust 还处于测试阶段,它可能不会总是生成正确的 locustfile,而且它的界面可能会在不同版本之间发生变化。

User class

用户类(user class)表示系统的一种类型的用户/场景。测试运行时,您指定要模拟的并发用户数,Locust 将为每个用户创建一个实例。您可以向这些类/实例添加任何属性,但有一些属性对 Locust 具有特殊意义:

wait_time 属性

用户类(User)的 wait_time 方法让我们很方便地在每个任务执行后引入延迟。如果没有指定 wait_time,那么下一个任务将在当前任务完成时立即执行。

constant(wait_time):一段固定的时间
between(min_wait, max_wait):最小值和最大值之间的随机时间

例如,要让每个用户在每次任务执行之间等待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(task_runs_per_second):自适应时间,确保任务每秒运行(最多) X 次。
constant_pacing(wait_time):自适应时间,确保任务每 X 秒运行一次(最多)

例如,如果您希望 Locust 在峰值负载下每秒运行500次任务迭代,那么可以使用 wait_time = constant_throughput(0.1)和5000的用户计数。
等待时间只能限制吞吐量,不能启动新用户到达目标。因此,在我们的示例中,如果任务迭代的时间超过10秒,吞吐量将小于500。
等待时间是在任务执行之后应用的,所以如果您有一个高的刷新率/上升,您可能会在上升期间超过您的目标。
等待时间应用于任务,而不是请求。例如,如果指定 wait_time = constant_throughput(2) 并在任务中执行两个请求,那么每个用户的请求速率/RPS(Request Per Second,每秒请求数) 将为4。

也可以直接在类上声明自己的 wait_time 方法。例如,下面的 User 类将睡眠一秒钟,然后两秒钟,然后三秒钟,等等。

class MyUser(User):
    last_wait_time = 0

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

    ...

weight and fixed_count 属性

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

$ locust -f locust_file.py WebUser MobileUser

如果希望模拟更多特定类型的用户,可以在这些类上设置 weight 属性。举个例子,WebUser是MobileUser的三倍:

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

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

还可以设置 fixed_count 属性。在这种情况下,将忽略 weight 属性,并且将产生精确计数的用户,精准的控制请求计数,与总用户计数无关:
在下面的示例中,将只生成一个 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 启动时,这是在 Locust 的 web UI 中或在命令行中使用 --host 选项指定的。

如果在用户类中声明一个 host 属性,那么在命令行或 Web 请求中没有指定 --host 的情况下将使用该属性。

tasks属性

User 类可以使用@task 装饰符将任务声明为其下的方法,但也可以使用 task 属性指定任务,后面将对此进行更详细的描述。

environment属性

对用户正在其中运行的环境environment的引用。使用它与环境或其中包含的runner进行交互。例如,阻止任务中runner的方法:

self.environment.runner.quit()

如果在独立的 locust 实例上运行,这将停止整个运行。如果在工作节点上运行,它将停止该特定节点。

on_start 和 on_stop 方法

用户(和任务集)(Users (and TaskSets))可以声明 on_start 方法 和/或 on_stop 方法。用户将在开始运行时调用其 on_start 方法,在停止运行时调用其 on_stop 方法。对于 TaskSet,当模拟用户开始执行该 TaskSet 时调用 on_start 方法,当模拟用户停止执行该 TaskSet 时调用 on_stop (当调用interrupt()或用户被杀死时)。

Tasks

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

这些任务都是普通的 Python 调用,如果我们在测试一个拍卖网站的负载,它们可以执行“加载起始页面”、“搜索某个产品”、“出价”等操作。

@task decorator(@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属性

另一种定义 User 任务的方法是设置 tasks 属性。

tasks 属性是 Tasks 的列表,或者是 <Task: int> dict,其中 Task 是 Python 可调用类或 TaskSet 类。如果任务是一个普通的 python 函数,它们会收到一个参数,这个参数就是执行任务的 User 实例。

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

from locust import User, constant

def my_task(user):
    pass

class MyUser(User):
    tasks = [my_task]
    wait_time = constant(1)

如果将任务属性指定为列表,则每次执行任务时,将从任务属性中随机选择任务属性。但是,如果任务是 dict( 使用可调用函数作为键,使用 int 作为值),将要执行的任务将会使用int的值作为比率来进行随机选择。任务是这样的:

{my_task: 3, another_task: 1}

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

在内部,上面的 dict 实际上将扩展为一个列表(任务属性也将更新) ,如下所示:

[my_task, my_task, my_task, another_task]

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

@tag decorator

通过使用@tag 装饰器对任务进行标记,您可以使用 --tag--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

如果您使用 --tag tag1 开始这个测试,那么在测试期间将只执行 task1和 task2。如果使用 --tag tag2 tag3 开始测试,则只执行 task2和 task3。

--exclude-tags 将以完全相反的方式运行。因此,如果使用 --exclude-tags tag3 开始测试,那么只有 task1、 task2和 task4會被执行。排除标记--exclude-tags的优先级高于包含标记--tag,所以如果一个任务有一个您已经包含的标记和一个您已经排除的标记,它将不会被执行。

Events

如果您希望在测试中运行一些安装代码,通常将其放在 locustfile 的模块级就足够了,但有时您需要在运行过程中的特定时间执行某些操作。为了满足这种需要,Locust 提供了事件挂钩。

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

Other events

请参阅 extending locust using event hooks 以获得其他事件以及如何使用它们的更多示例。

HttpUser class

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 attribute / HttpSession

clientHttpSession 的实例。HttpSessionrequests.Session 的子类/包装器。因此它的特性都有很好的文档记录,并且应该为许多人所熟悉。HttpSession 添加的主要是将请求结果报告到 Locust 中(success/fail, response time, response length, name)。

它包含所有 HTTP 方法的方法: getpostput、 ..。

就像requests.Session,它保存请求之间的 cookie,所以它可以很容易地用于登录到网站。

发出一个 POST 请求,查看响应并隐式地重用我们为第二个请求获得的任何会话 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 捕获 Session 抛出的任何 requests.RequestException (由连接错误、超时或类似情况引起) ,然后改为返回一个虚拟 Response 对象,其 status_code 设置为0,content 设置为 None。

Validating responses

如果 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-block 之外捕获异常来避免记录请求。或者你可以抛出一个locust exception,就像下面的例子一样,让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

FastHttpUser提供了一个现成的 rest 方法,但是你也可以自己来做:

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

Grouping requests

对于网站来说,拥有 URL 包含某种动态参数的页面是非常普遍的。通常在用户的统计数据中将这些 URL 分组在一起是有意义的。这可以通过向 HttpSession 的不同请求方法传递 name 参数来完成。

例如:

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

有些情况下不可能将参数传递给请求函数,例如在与包装了请求会话的库/SDK 交互时。通过设置 client.request_name 属性提供了另一种分组请求的方法。

# Statistics for these requests will be grouped under: /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

如果希望使用最小样板来链接多个分组,可以使用 client.rename_reques()上下文管理器。

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

使用 catch_response 和直接访问 request_meta,您甚至可以根据响应中的内容重命名请求。

with self.client.get("/", catch_response=True) as resp:
    resp.request_meta["name"] = resp.json()["name"]

HTTP Proxy settings

为了提高性能,我们通过设置requests.Sessiontrust_env 属性为 False 将请求配置为不在环境中查找 HTTP 代理设置。如果不想这样,可以手动设置 locust_instance.client.trust_envTrue。有关进一步的详细信息,请参阅请求的文档。

Connection pooling

当每个 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 documentation

TaskSets

TaskSet 是一种结构化层次化网站/系统测试的方法。你可以在这里了解更多

Examples

这里有很多 locustfile 示例

如何组织测试代码

务必记住 locustfile.py 只是 Locust 导入的一个普通 Python 模块。从这个模块中,您可以自由地导入其他 Python 代码,就像在任何 Python 程序中一样。当前的工作目录会自动添加到 python 的 sys.path 中,因此可以使用 python import 语句导入工作目录中的任何 python 文件/模块/包。

对于小型测试,将所有测试代码保存在一个 locustfile.py 中应该没有问题,但是对于较大的测试套件,您可能希望将代码分割成多个文件和目录。

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

  • Project root
    • common/
      • __init__.py
      • auth.py
      • config.py
    • locustfile.py
    • requirements.txt (外部 Python 依赖项通常保存在 requments.txt 中)

具有多个 locustfile 的项目还可以将它们保存在单独的子目录中:

  • Project root
    • common/
      • __init__.py
      • auth.py
      • config.py
    • my_locustfiles/
      • api.py
      • website.py
    • requirements.txt

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

import common.auth