企业级性能测试框架完整指南
本文档全面讲解性能测试框架的架构设计、核心模块、使用方法和最佳实践。
代码讲解约定 :涉及配置或核心代码时,会从三个角度说明------为什么这样写 、在框架里该怎么用 、实际意义是什么,尽量覆盖所有相关代码,便于理解和扩展。
📑 目录
- [1. 框架概述](#1. 框架概述)
- [2. 架构设计](#2. 架构设计)
- [3. 核心模块详解](#3. 核心模块详解)
- [4. 测试用例编写](#4. 测试用例编写)
- [5. 配置系统](#5. 配置系统)
- [6. 性能监控](#6. 性能监控)
- [7. 报告系统](#7. 报告系统)
- [8. 最佳实践](#8. 最佳实践)
- [9. 高级用法](#9. 高级用法)
- [10. 扩展开发](#10. 扩展开发)
- [11. 故障排查](#11. 故障排查)
- [12. 性能优化](#12. 性能优化)
- [Python 常用库用法与面试题](#Python 常用库用法与面试题)
1. 框架概述
1.1 框架简介
这是一个基于 Locust 的企业级性能测试框架,专门设计用于对 RESTful API 进行全面的性能测试。框架采用模块化设计,提供了完整的测试工具链,包括配置管理、日志记录、断言验证、性能监控和报告生成等功能。
1.2 核心特性
✨ 企业级特性
-
模块化架构
- 清晰的模块划分
- 松耦合设计
- 易于扩展和维护
-
配置管理
- 统一的配置管理
- 支持多环境配置
- 灵活的配置覆盖
-
日志系统
- 多级别日志记录
- 日志轮转机制
- 文件和控制台双重输出
-
断言机制
- 丰富的断言方法
- JSON 路径支持
- 详细的错误信息
-
性能监控
- 实时性能指标收集
- 性能阈值告警
- 详细的统计报告
-
报告生成
- 多格式报告(HTML/CSV/JSON)
- 自动报告归档
- 可视化数据展示
🚀 技术特性
-
基于 Locust
- 分布式测试支持
- 高并发性能
- 实时监控界面
-
Python 生态
- 纯 Python 实现
- 易于集成和扩展
- 丰富的第三方库支持
-
数据驱动
- JSON 数据文件支持
- 配置文件数据支持
- 灵活的数据加载策略
1.3 适用场景
- API 性能测试:RESTful API 的性能验证
- 负载测试:系统在不同负载下的表现
- 压力测试:系统极限性能测试
- 稳定性测试:长时间运行的稳定性验证
- 容量规划:系统容量评估和规划
1.4 技术栈
- Python 3.7+:核心编程语言
- Locust 2.17.0+:性能测试框架
- 标准库:logging, json, csv, os, sys 等
2. 架构设计
2.1 整体架构
┌─────────────────────────────────────────────────────────────┐
│ 测试执行层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ HTTP方法测试 │ │ 响应验证测试 │ │ 高级功能测试 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 混合场景测试 │ │ 压力测试场景 │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 核心服务层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ BaseUser │ │ Assertions │ │ DataLoader │ │
│ │ 基础用户类 │ │ 断言模块 │ │ 数据加载器 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 基础设施层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ ConfigMgr │ │ Logger │ │ Performance │ │
│ │ 配置管理器 │ │ 日志模块 │ │ Monitor │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ReportGen │ │ Locust │ │
│ │ 报告生成器 │ │ 框架 │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 配置数据层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ config.py │ │ test_data.json│ │ Logs │ │
│ │ 配置文件 │ │ 测试数据文件 │ │ 日志文件 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
2.2 设计模式
2.2.1 单例模式(Singleton Pattern)
应用 :ConfigManager
目的:确保整个应用只有一个配置管理器实例,避免重复加载配置。
实现:
python
class ConfigManager:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(ConfigManager, cls).__new__(cls)
return cls._instance
优点:
- 节省内存
- 配置一致性
- 全局访问点
在框架里怎么用 :所有模块都用 from core.config_manager import config_manager,用同一个实例调 get()、get_base_url(),不要自己 ConfigManager();这样配置只加载一次,全局一致。
2.2.2 模板方法模式(Template Method Pattern)
应用 :BaseUser
目的:定义测试用户的基本结构,子类实现具体的测试任务。
实现:
python
class BaseUser(HttpUser):
def on_start(self):
# 模板方法:定义启动流程
self._setup_headers()
@task
def test_method(self):
# 子类实现具体测试逻辑
pass
优点:
- 代码复用
- 统一接口
- 易于扩展
在框架里怎么用 :写具体场景时继承 BaseUser,实现 @task 方法即可;on_start/on_stop 可选重写并先调 super().on_start(),这样默认的 host、wait_time、请求头都会生效。
2.2.3 策略模式(Strategy Pattern)
应用:断言方法
目的:支持不同的断言策略。
实现:
python
class ResponseAssertion:
@staticmethod
def assert_status_code(response, expected):
# 状态码断言策略
pass
@staticmethod
def assert_json_key_exists(response, key):
# JSON 断言策略
pass
优点:
- 灵活性
- 可扩展性
- 易于测试
在框架里怎么用 :在 catch_response=True 的 with 块里,按需调用 assertion.assert_status_code()、assert_json_key_exists()、assert_json_value() 等;根据返回值决定 response.success() 或 response.failure("原因");新增断言类型时在断言模块里加新方法即可。
2.3 数据流
配置文件 (config.py)
↓
ConfigManager (加载配置)
↓
BaseUser (读取配置)
↓
测试用例 (执行测试)
↓
Assertions (验证响应)
↓
PerformanceMonitor (收集指标)
↓
ReportGenerator (生成报告)
2.4 依赖关系
测试用例
├── BaseUser
│ ├── ConfigManager
│ └── Logger
├── Assertions
│ └── Logger
└── DataLoader
├── ConfigManager
└── Logger
PerformanceMonitor
└── Logger
ReportGenerator
└── Logger
3. 核心模块详解
3.1 配置管理器 (ConfigManager)
3.1.1 模块概述
ConfigManager 是框架的配置管理中心,采用单例模式实现,负责加载和管理所有配置信息。
3.1.2 核心功能
-
配置文件加载
- 动态加载
config/config.py - 支持配置文件路径自定义
- 配置文件不存在时抛出异常
- 动态加载
-
配置访问
get(key, default):获取配置值get_base_url():获取基础 URL(便捷方法)
-
单例保证
- 确保全局唯一实例
- 避免重复加载配置
- 节省内存资源
3.1.3 使用示例
python
from core.config_manager import config_manager
# 获取配置值
base_url = config_manager.get("BASE_URL", "https://httpbin.org")
log_level = config_manager.get("LOG_LEVEL", "INFO")
# 获取基础 URL
base_url = config_manager.get_base_url()
3.1.4 源码解析
python
class ConfigManager:
_instance = None # 类变量:保存唯一实例
_config = None # 类变量:保存配置模块
def __new__(cls):
"""单例模式实现"""
if cls._instance is None:
cls._instance = super(ConfigManager, cls).__new__(cls)
return cls._instance
def __init__(self):
"""初始化配置"""
if self._config is None:
self._load_config()
def _load_config(self):
"""动态加载配置文件"""
project_root = os.path.dirname(os.path.dirname(__file__))
config_path = os.path.join(project_root, "config", "config.py")
spec = importlib.util.spec_from_file_location("config", config_path)
config_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(config_module)
self._config = config_module
def get(self, key, default=None):
"""获取配置值"""
return getattr(self._config, key, default)
3.1.5 逐段代码说明:ConfigManager 为什么这样写、怎么用、什么意义
单例模式(_instance、new)
- 为什么这样写 :配置只需加载一次(读 config.py、执行模块),若每次
ConfigManager()都 new 一个实例,会重复加载、多份配置,浪费内存且可能不一致。用单例保证全局只有一个实例。 - 怎么用 :任何地方都用
from core.config_manager import config_manager,用这个现成实例调config_manager.get(...)、config_manager.get_base_url(),不要 在用例或场景里再写ConfigManager()。 - 意义:全框架共用同一份配置,改 config.py 或环境变量后,所有读取处自动生效。
_load_config() 动态加载 config.py
- 为什么这样写 :配置用 Python 文件(config.py)而不是 JSON,可以写常量、简单运算、注释;用
importlib.util.spec_from_file_location动态加载,不依赖包结构,路径固定为"项目根/config/config.py"。 - 怎么用 :你只需保证
config/config.py存在且里面定义好BASE_URL、WAIT_TIME_MIN等变量;框架启动时自动执行一次 _load_config,之后都从内存里的 _config 读。 - 意义:配置集中在一个文件,改 BASE_URL、日志级别、超时等都在 config.py 里改,无需改业务代码。
get(key, default=None)
- 为什么这样写 :config 是"模块对象",配置项是模块里的变量名(如 BASE_URL),用
getattr(self._config, key, default)按名字取;没有则返回 default,避免 KeyError。 - 怎么用 :
config_manager.get("BASE_URL")、config_manager.get("WAIT_TIME_MIN", 1)(第二个参数是默认值)。key 必须和 config.py 里变量名完全一致(大小写敏感)。 - 意义:统一入口取配置,调用方不关心配置是文件还是环境变量,且可带默认值防止缺配时报错。
get_base_url()
- 为什么这样写:BASE_URL 是最常用的一项,单独方法便于阅读、也便于以后扩展(如按环境切换 URL)。
- 怎么用 :在 BaseUser 里写
host = config_manager.get_base_url(),Locust 会用这个 host 和请求路径拼成完整 URL。不要在用例里再拼一次域名。 - 意义:所有请求的根地址来自同一配置,换环境只改 config.py 的 BASE_URL。
3.2 日志模块 (Logger)
3.2.1 模块概述
Logger 提供了完善的日志记录功能,支持多级别日志、日志轮转和双重输出(控制台+文件)。
3.2.2 核心功能
-
多级别日志
- DEBUG:详细信息
- INFO:一般信息
- WARNING:警告信息
- ERROR:错误信息
- CRITICAL:严重错误
-
日志轮转
- 文件大小限制(默认 10MB)
- 自动备份(默认保留 5 个备份)
- 防止日志文件过大
-
双重输出
- 控制台输出(INFO 及以上)
- 文件输出(DEBUG 及以上)
3.2.3 使用示例
python
from core.logger import logger
# 记录不同级别的日志
logger.debug("调试信息:变量值 = %s", value)
logger.info("测试开始:用户数 = %d", user_count)
logger.warning("性能告警:响应时间超过阈值")
logger.error("测试失败:%s", error_message)
logger.critical("严重错误:系统异常")
3.2.4 配置说明
在 config/config.py 中配置:
python
# 日志级别
LOG_LEVEL = "INFO" # DEBUG, INFO, WARNING, ERROR, CRITICAL
# 日志格式
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# 日志文件配置
LOG_FILE_ENABLED = True
LOG_FILE_PATH = "logs/performance_test.log"
LOG_FILE_MAX_BYTES = 10485760 # 10MB
LOG_FILE_BACKUP_COUNT = 5
3.2.5 日志轮转机制
当日志文件达到 LOG_FILE_MAX_BYTES 大小时:
- 当前日志文件重命名为
performance_test.log.1 - 创建新的
performance_test.log文件 - 旧的备份文件依次重命名(
.1→.2→.3→ ...) - 超过
LOG_FILE_BACKUP_COUNT的备份文件被删除
3.2.6 日志配置每项该怎么用、为什么、什么意义
| config.py 中的变量 | 为什么这样写 | 怎么用 | 实际意义 |
|---|---|---|---|
| LOG_LEVEL | 不同阶段需要不同详细程度:开发用 DEBUG,压测用 INFO,线上用 WARNING。 | 在 config.py 里设成 "INFO" 或 "DEBUG";logger 会只输出 >= 该级别的日志。 |
控制台/文件里不会刷屏,又能按需看详细。 |
| LOG_FORMAT | 统一格式便于 grep、解析;含时间、级别、消息即可。 | 一般不用改;若需要加线程/进程 ID,可在这里改格式字符串。 | 日志可读、可检索。 |
| LOG_FILE_ENABLED | 有时只想要控制台(如 CI 里),关掉文件可减少 I/O。 | 设为 False 则只输出到控制台;True 则同时写文件。 | 按场景选择是否落盘。 |
| LOG_FILE_PATH | 日志文件位置集中配置,避免写死在代码里。 | 路径相对项目根或绝对路径均可;目录不存在时需框架自动创建。 | 统一存放,便于收集和归档。 |
| LOG_FILE_MAX_BYTES / LOG_FILE_BACKUP_COUNT | 单文件太大会难打开、难传输;轮转后保留 N 个备份即可追溯近期问题。 | 默认 10MB、5 个备份;压测时间长可适当调大或增加备份数。 | 控制磁盘占用且保留历史。 |
3.3 基础用户类 (BaseUser)
3.3.1 模块概述
BaseUser 是所有测试用户类的基类,继承自 Locust 的 HttpUser,提供了统一的配置和生命周期管理。
3.3.2 核心功能
-
自动配置
- 自动从配置文件读取
BASE_URL - 自动设置
wait_time(等待时间)
- 自动从配置文件读取
-
生命周期管理
on_start():用户启动时执行on_stop():用户停止时执行
-
请求头设置
- 默认请求头配置
- 支持自定义请求头
3.3.3 使用示例
python
from core.base_user import BaseUser
from locust import task
class MyTestUser(BaseUser):
@task
def test_get(self):
with self.client.get("/api/endpoint", name="GET请求", catch_response=True) as response:
if response.status_code == 200:
response.success()
else:
response.failure("状态码错误")
3.3.4 源码解析
python
class BaseUser(HttpUser):
# 从配置文件读取等待时间
wait_time = between(
config_manager.get("WAIT_TIME_MIN", 1),
config_manager.get("WAIT_TIME_MAX", 3)
)
# 从配置文件读取基础 URL
host = config_manager.get_base_url()
def on_start(self):
"""用户启动时的初始化"""
logger.info(f"用户启动: {self.__class__.__name__}")
self._setup_headers()
def on_stop(self):
"""用户停止时的清理"""
logger.info(f"用户停止: {self.__class__.__name__}")
def _setup_headers(self):
"""设置默认请求头"""
self.client.headers.update({
"User-Agent": "Locust-Performance-Test-Framework/1.0",
"Accept": "application/json",
"Content-Type": "application/json"
})
3.3.5 逐段代码说明:BaseUser 为什么这样写、怎么用、什么意义
wait_time = between(..., ...)
- 为什么这样写 :Locust 每次执行完一个 task 后会等待一段时间再执行下一个,模拟真实用户"思考/操作间隔";用
between(min, max)表示随机在 min~max 秒之间。从 config 读 WAIT_TIME_MIN/MAX 便于调参(如压测时改小、仿真时改大)。 - 怎么用 :在 config.py 里设置
WAIT_TIME_MIN = 1、WAIT_TIME_MAX = 3;你的 User 类继承 BaseUser 后无需再写 wait_time,会自动用配置。若某个场景要固定间隔,可在子类里重写wait_time = constant(2)。 - 意义:用户行为更接近真实,且等待时间可配置。
host = config_manager.get_base_url()
- 为什么这样写 :Locust 的 HttpUser 用
host作为请求的"根 URL",你写的路径如/get会拼成host + "/get"。从 config 读保证所有虚拟用户打的是同一环境。 - 怎么用 :不要在自己的 User 类里再设 host;保证 config.py 里 BASE_URL 正确(如
https://httpbin.org,不要末尾多斜杠)。若需多环境,可在启动前改 config 或通过环境变量注入 BASE_URL。 - 意义:压测目标统一、可配置,避免写死在代码里。
on_start() / on_stop()
- 为什么这样写:Locust 在每个虚拟用户"诞生"时调 on_start,"销毁"时调 on_stop。在 on_start 里做登录、加载数据、设置请求头等;在 on_stop 里做登出、清理。
- 怎么用 :子类若需要自己的初始化,重写
def on_start(self): super().on_start(); ...你的逻辑,先调 super 再写自己的(这样 _setup_headers 一定会执行)。on_stop 同理。 - 意义:每个用户有独立会话(如自己的 token),且生命周期清晰。
_setup_headers()
- 为什么这样写:很多接口要求 Content-Type、Accept 或自定义 User-Agent,在基类里统一设置,子类不用重复写;Locust 的 self.client 会把这些 headers 带到每次请求。
- 怎么用 :若你的接口需要额外头(如 Authorization),在子类 on_start 里再写
self.client.headers["Authorization"] = "Bearer xxx";不要删掉 BaseUser 里已有的,用 update 追加即可。 - 意义:公共头一处维护,子类只加业务相关头。
3.4 断言模块 (Assertions)
3.4.1 模块概述
Assertions 提供了丰富的断言方法,用于验证 HTTP 响应的正确性。
3.4.2 核心功能
-
状态码断言
- 验证 HTTP 状态码是否匹配
-
JSON 断言
- 验证 JSON 键是否存在(支持点号分隔路径)
- 验证 JSON 值是否匹配
-
文本断言
- 验证响应文本是否包含指定内容
3.4.3 使用示例
python
from core.assertions import assertion
# 状态码断言
if assertion.assert_status_code(response, 200, "GET请求"):
response.success()
# JSON 键断言(支持嵌套路径)
if assertion.assert_json_key_exists(response, "data.user.id", "用户信息"):
response.success()
# JSON 值断言
if assertion.assert_json_value(response, "status", "success", "状态验证"):
response.success()
# 文本断言
if assertion.assert_text_contains(response, "success", "响应验证"):
response.success()
3.4.4 JSON 路径支持
支持点号分隔的路径,例如:
python
# 简单路径
assertion.assert_json_key_exists(response, "id", name)
# 嵌套路径
assertion.assert_json_key_exists(response, "data.user.id", name)
# 深层嵌套
assertion.assert_json_key_exists(response, "data.users.0.name", name)
3.4.5 断言方法详解
assert_status_code
python
def assert_status_code(response, expected, name=""):
"""
断言 HTTP 状态码
参数:
response: Locust 响应对象
expected: 期望的状态码
name: 请求名称(用于日志)
返回:
True: 断言通过
False: 断言失败
"""
assert_json_key_exists
python
def assert_json_key_exists(response, key, name=""):
"""
断言 JSON 响应中存在指定键
参数:
response: Locust 响应对象
key: 键名(支持点号分隔,如 "data.user.id")
name: 请求名称
返回:
True: 键存在
False: 键不存在
"""
assert_json_value
python
def assert_json_value(response, key, expected_value, name=""):
"""
断言 JSON 响应中指定键的值等于期望值
参数:
response: Locust 响应对象
key: 键名(支持点号分隔)
expected_value: 期望的值
name: 请求名称
返回:
True: 值匹配
False: 值不匹配
"""
assert_text_contains
python
def assert_text_contains(response, text, name=""):
"""
断言响应文本包含指定内容
参数:
response: Locust 响应对象
text: 期望包含的文本
name: 请求名称
返回:
True: 包含
False: 不包含
"""
3.4.6 每个断言方法该怎么用、为什么、什么意义
| 方法 | 为什么需要 | 怎么用 | 实际意义 |
|---|---|---|---|
| assert_status_code(response, expected, name) | Locust 默认只按 HTTP 状态码是否 2xx 判成功,业务可能约定 200 才成功、201 为创建成功等,需要显式校验。 | 在 with self.client.get(..., catch_response=True) as response: 里,先 if assertion.assert_status_code(response, 200, "GET请求"): 再 response.success(),否则 response.failure("原因")。 |
精确区分"HTTP 成功"和"业务成功",统计里失败原因更清晰。 |
| assert_json_key_exists(response, key, name) | 接口约定返回里一定有某字段(如 data、id),缺少说明接口异常或契约变更。 | key 支持点号路径如 "data.user.id";返回 True 表示存在,False 表示不存在,据此 success/failure。 |
快速发现"结构不对"的响应,便于定位是前端还是后端问题。 |
| assert_json_value(response, key, expected_value, name) | 业务字段值需符合预期(如 status=="success"、code==0),仅存在不够。 | 先按 key 取到值,再与 expected_value 比较;相等返回 True。用于关键业务字段校验。 | 保证压测不仅"能调通",而且"结果对"。 |
| assert_text_contains(response, text, name) | 少数接口返回 HTML 或纯文本,没有 JSON,只能用"是否包含某串"判断。 | 对 response.text 做 text in response.text;包含则 True。 |
非 JSON 接口也能做简单校验。 |
统一用法模式 :所有断言都返回 True/False,不抛异常;你在 catch_response=True 的 with 块里根据返回值决定 response.success() 还是 response.failure("具体原因"),这样 Locust 的失败统计里会显示你写的原因,便于排查。name 参数会出现在日志里,建议写请求含义(如 "登录"、"查询订单")。
3.5 数据加载器 (DataLoader)
3.5.1 模块概述
DataLoader 负责加载和管理测试数据,支持从 JSON 文件和配置文件加载数据。
3.5.2 核心功能
-
JSON 文件加载
- 从
data/目录加载 JSON 文件 - 自动创建目录(如果不存在)
- 错误处理和日志记录
- 从
-
配置文件数据
- 从
config/config.py读取测试数据 - 支持列表和字典类型数据
- 从
-
便捷方法
get_users():获取用户数据列表
3.5.3 使用示例
python
from core.data_loader import data_loader
# 加载 JSON 文件
data = data_loader.load_json("test_data.json")
users = data.get("users", [])
# 从配置文件获取数据
post_data = data_loader.get_test_data("TEST_DATA_POST_DATA", [])
# 获取用户数据
users = data_loader.get_users()
3.5.4 数据文件格式
data/test_data.json:
json
{
"users": [
{
"id": 1,
"name": "张三",
"email": "zhangsan@test.com"
},
{
"id": 2,
"name": "李四",
"email": "lisi@test.com"
}
],
"post_data": [
{
"name": "test1",
"value": "value1"
}
]
}
3.5.5 DataLoader 为什么这样写、怎么用、什么意义
| 功能/API | 为什么这样写 | 怎么用 | 实际意义 |
|---|---|---|---|
| load_json(filename) | 数据驱动需要从文件读测试数据(用户、请求体等),JSON 易编辑和版本管理;统一从 data/ 目录加载,路径一致。 | 传入文件名(如 "test_data.json"),框架会到 data/ 下找;返回整个 JSON 的 dict,再用 .get("users", []) 等取列表。 |
测试数据与代码分离,便于维护多套数据。 |
| get_test_data(key, default) | 有时数据在 config.py 里以常量形式存在(如 TEST_DATA_POST_DATA),按 key 取可复用配置。 | 传入 config 里的变量名(如 "TEST_DATA_POST_DATA")和默认值;返回该变量对应的列表/字典。 |
配置与数据文件两种来源统一入口。 |
| get_users() | "用户列表"是常见需求,单独方法避免各处写 data.get("users", []) 或 get_test_data("...")。 | 在 User 的 on_start 或 task 里调用:users = data_loader.get_users();再 random.choice(users) 等取一条用。 |
数据驱动时减少重复代码。 |
注意:若 data 目录或文件不存在,load_json 应有明确报错或日志,避免静默返回空导致用例"假通过"。
3.6 性能监控器 (PerformanceMonitor)
3.6.1 模块概述
PerformanceMonitor 提供实时性能监控和统计功能,自动收集性能指标并生成报告。
3.6.2 核心功能
-
性能指标收集
- 请求总数、成功数、失败数
- 响应时间统计(平均/最小/最大/P95/P99)
- 错误信息收集
-
性能阈值监控
- 响应时间阈值告警
- 错误率阈值告警
- 成功率阈值告警
-
性能报告生成
- 测试结束时自动生成报告
- 详细的统计信息输出
3.6.3 使用示例
python
from core.performance_monitor import performance_monitor
# 设置性能阈值
performance_monitor.set_thresholds(
max_response_time=2000, # 最大响应时间 2 秒
max_error_rate=5.0, # 最大错误率 5%
min_success_rate=95.0 # 最小成功率 95%
)
# 获取统计信息
stats = performance_monitor.get_statistics("GET请求")
print(f"平均响应时间: {stats['avg_response_time']:.2f}ms")
print(f"成功率: {stats['success_rate']:.2f}%")
3.6.4 性能指标说明
| 指标 | 说明 |
|---|---|
| request_count | 请求总数 |
| success_count | 成功请求数 |
| failure_count | 失败请求数 |
| error_rate | 错误率(百分比) |
| success_rate | 成功率(百分比) |
| avg_response_time | 平均响应时间(毫秒) |
| min_response_time | 最小响应时间(毫秒) |
| max_response_time | 最大响应时间(毫秒) |
| median_response_time | 中位数响应时间(毫秒) |
| p95_response_time | P95 响应时间(毫秒) |
| p99_response_time | P99 响应时间(毫秒) |
3.6.5 性能阈值配置
在 config/config.py 中配置:
python
# 性能监控启用
PERFORMANCE_MONITOR_ENABLED = True
# 响应时间阈值(毫秒)
MAX_RESPONSE_TIME = 1000 # 最大响应时间阈值
WARNING_RESPONSE_TIME = 500 # 警告响应时间阈值
# 错误率阈值(百分比)
MAX_ERROR_RATE = 5.0 # 最大错误率阈值
WARNING_ERROR_RATE = 2.0 # 警告错误率阈值
# 成功率阈值(百分比)
MIN_SUCCESS_RATE = 95.0 # 最小成功率阈值
WARNING_SUCCESS_RATE = 98.0 # 警告成功率阈值
3.6.6 PerformanceMonitor 为什么这样写、怎么用、什么意义
| 功能/API | 为什么这样写 | 怎么用 | 实际意义 |
|---|---|---|---|
| set_thresholds(max_response_time, max_error_rate, min_success_rate) | 压测结果需要"通过/不通过"标准,阈值可配置便于不同场景(如冒烟更严、压力测试放宽)。 | 在测试开始前或从 config 读完后调用一次;单位注意(响应时间一般为毫秒,错误率/成功率为百分比)。 | 自动化判断本次压测是否达标。 |
| get_statistics(name) | 按请求名称(Locust 的 name 参数)统计该请求的请求数、成功率、响应时间分位数等,便于精确定位慢或错的接口。 | 在 task 里用 name 与发请求时一致(如 "GET请求");测试中或结束后调用 get_statistics("GET请求") 得到该接口的统计 dict。 | 按接口维度分析,而非只看整体。 |
| request_count / success_count / failure_count | 总数与成功/失败拆开,可算成功率和错误率;与 Locust 自带的统计可互为校验。 | 从 get_statistics 返回的 dict 里取;或框架在每次请求成功/失败时更新内部计数。 | 指标清晰,便于做阈值判断和报告。 |
| avg_response_time / p95_response_time / p99_response_time | 平均与分位数结合,既能看整体又能看长尾;P95/P99 对 SLA 更有意义。 | 同上,从统计 dict 取;若框架在每次请求时记录响应时间并排序,即可算分位数。 | 发现慢请求和长尾延迟。 |
注意:PerformanceMonitor 若在 Locust 的请求回调里埋点,需保证线程安全(如用线程锁或 Locust 提供的事件钩子),避免并发写导致数据错乱。
3.7 报告生成器 (ReportGenerator)
3.7.1 模块概述
ReportGenerator 负责生成各种格式的测试报告,支持 HTML、CSV 和 JSON 格式。
3.7.2 核心功能
-
HTML 报告
- 可视化测试结果
- 图表展示
- 便于查看和分析
-
CSV 报告
- 表格格式数据
- 便于 Excel 分析
- 支持批量处理
-
JSON 报告
- 结构化数据
- 便于程序处理
- 支持数据集成
3.7.3 使用示例
python
from core.report_generator import report_generator
# 生成 CSV 报告
stats = [
{"name": "GET请求", "requests": 100, "failures": 0, "avg_time": 50},
{"name": "POST请求", "requests": 50, "failures": 2, "avg_time": 80}
]
report_generator.generate_csv(stats, "performance_test")
# 生成 JSON 报告
data = {
"test_name": "performance_test",
"timestamp": "2024-01-01 12:00:00",
"summary": {
"total_requests": 1000,
"total_failures": 10,
"avg_response_time": 50
}
}
report_generator.generate_json(data, "performance_test")
3.7.4 ReportGenerator 为什么这样写、怎么用、什么意义
| 功能/API | 为什么这样写 | 怎么用 | 实际意义 |
|---|---|---|---|
| generate_csv(stats, name) | CSV 便于用 Excel 打开、做趋势对比或入库;stats 为每类请求的统计列表,一行一个接口。 | 传入 stats(如 [{"name":"GET请求","requests":100,"failures":0,"avg_time":50}])和报告名;会生成 name 相关的 csv 文件(如 reports/name_timestamp.csv)。 |
结果可被其他工具消费,便于历史对比。 |
| generate_json(data, name) | JSON 便于程序解析、集成到 CI 或监控系统;结构可包含时间戳、汇总、明细。 | 传入完整结构(如 test_name、timestamp、summary、details)和报告名;生成 name 相关的 json 文件。 | 机器可读,便于自动分析和告警。 |
| generate_html(...) | 人眼更习惯看网页报告,可含图表、表格、高亮失败项。 | 若有该方法,传入统计数据或结果路径;生成 HTML 到 reports/ 下;用浏览器打开即可查看。 | 人工排查和汇报用。 |
注意:报告输出目录(如 reports/)应在框架启动或报告生成前确保存在;若支持 REPORT_RETENTION_COUNT,应定期清理旧报告避免占满磁盘。
4. 测试用例编写
4.1 测试用例结构
每个测试用例文件都应该:
- 继承
BaseUser - 使用
@task装饰器定义测试任务 - 使用
catch_response=True捕获响应 - 使用断言模块验证响应
- 调用
response.success()或response.failure()标记结果
4.2 基本测试用例
python
from core.base_user import BaseUser
from core.assertions import assertion
from locust import task
class MyTestUser(BaseUser):
@task
def test_get(self):
"""测试 GET 请求"""
name = "GET请求"
with self.client.get(
"/api/endpoint",
name=name,
catch_response=True
) as response:
if assertion.assert_status_code(response, 200, name):
if assertion.assert_json_key_exists(response, "data", name):
response.success()
else:
response.failure("JSON 结构错误")
else:
response.failure(f"状态码错误: {response.status_code}")
4.3 带参数的测试用例
python
from core.data_loader import data_loader
class MyTestUser(BaseUser):
def on_start(self):
"""加载测试数据"""
super().on_start()
self.users = data_loader.get_users()
@task
def test_post(self):
"""使用测试数据进行 POST 请求"""
user = random.choice(self.users)
json_data = {
"name": user["name"],
"email": user["email"]
}
with self.client.post(
"/api/users",
json=json_data,
name="POST请求",
catch_response=True
) as response:
if assertion.assert_status_code(response, 201, "POST请求"):
response.success()
else:
response.failure("创建用户失败")
4.4 认证测试用例
python
class AuthTestUser(BaseUser):
def on_start(self):
"""初始化认证信息"""
super().on_start()
self.token = None
@task(2)
def test_login(self):
"""登录获取 Token"""
login_data = {
"username": "testuser",
"password": "testpass"
}
with self.client.post(
"/api/login",
json=login_data,
name="登录",
catch_response=True
) as response:
if assertion.assert_status_code(response, 200, "登录"):
response_data = response.json()
self.token = response_data.get("token")
response.success()
else:
response.failure("登录失败")
@task(5)
def test_authenticated_request(self):
"""使用 Token 的认证请求"""
if not self.token:
return
headers = {"Authorization": f"Bearer {self.token}"}
with self.client.get(
"/api/protected",
headers=headers,
name="认证请求",
catch_response=True
) as response:
if assertion.assert_status_code(response, 200, "认证请求"):
response.success()
else:
response.failure("认证请求失败")
4.5 业务流程测试用例
python
class BusinessFlowUser(BaseUser):
@task
def scenario_complete_workflow(self):
"""完整业务流程测试"""
# 步骤1: 创建资源
create_data = {"name": "test_resource"}
with self.client.post("/api/resources", json=create_data, name="创建资源", catch_response=True) as response:
if not assertion.assert_status_code(response, 201, "创建资源"):
response.failure("创建失败")
return
resource_id = response.json().get("id")
response.success()
# 步骤2: 查询资源
with self.client.get(f"/api/resources/{resource_id}", name="查询资源", catch_response=True) as response:
if not assertion.assert_status_code(response, 200, "查询资源"):
response.failure("查询失败")
return
response.success()
# 步骤3: 更新资源
update_data = {"name": "updated_resource"}
with self.client.put(f"/api/resources/{resource_id}", json=update_data, name="更新资源", catch_response=True) as response:
if not assertion.assert_status_code(response, 200, "更新资源"):
response.failure("更新失败")
return
response.success()
# 步骤4: 删除资源
with self.client.delete(f"/api/resources/{resource_id}", name="删除资源", catch_response=True) as response:
if assertion.assert_status_code(response, 204, "删除资源"):
response.success()
else:
response.failure("删除失败")
4.6 任务权重控制
使用 @task(weight) 装饰器控制任务执行频率:
python
class MyTestUser(BaseUser):
@task(10) # 权重 10,执行频率最高
def test_get(self):
pass
@task(5) # 权重 5,执行频率中等
def test_post(self):
pass
@task(1) # 权重 1,执行频率最低
def test_delete(self):
pass
4.7 错误处理
python
class MyTestUser(BaseUser):
@task
def test_with_error_handling(self):
"""带错误处理的测试"""
try:
with self.client.get("/api/endpoint", name="GET请求", catch_response=True) as response:
if response.status_code == 200:
# 验证响应内容
data = response.json()
if data.get("status") == "success":
response.success()
else:
response.failure("业务逻辑错误")
else:
response.failure(f"HTTP错误: {response.status_code}")
except Exception as e:
logger.error(f"请求异常: {str(e)}")
response.failure(f"异常: {str(e)}")
5. 配置系统
5.1 配置文件结构
config/config.py 文件包含所有配置项,按功能模块组织:
python
# ============================================================================
# 基础配置
# ============================================================================
BASE_URL = "https://httpbin.org"
WAIT_TIME_MIN = 1
WAIT_TIME_MAX = 3
# ============================================================================
# 日志配置
# ============================================================================
LOG_LEVEL = "INFO"
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
LOG_FILE_ENABLED = True
LOG_FILE_PATH = "logs/performance_test.log"
# ============================================================================
# 性能监控配置
# ============================================================================
PERFORMANCE_MONITOR_ENABLED = True
MAX_RESPONSE_TIME = 1000
MAX_ERROR_RATE = 5.0
MIN_SUCCESS_RATE = 95.0
# ... 更多配置
5.1.1 config.py 各配置块为什么这样写、怎么用、什么意义
基础 URL 与 Locust 行为
| 变量 | 为什么这样写 | 怎么用 | 实际意义 |
|---|---|---|---|
| BASE_URL | 压测目标可能随环境变化(开发/测试/生产),写死在代码里不利于切换。 | 在 config.py 里改成当前要压的域名(如 https://httpbin.org);BaseUser 的 host 会从这里读,所有请求都发往该地址。 |
换环境只改一处,避免误压生产。 |
| WAIT_TIME_MIN / WAIT_TIME_MAX | 模拟用户操作间隔,用区间更真实;从配置读便于调参(压测可改小、仿真可改大)。 | 在 config.py 里设成秒数(如 1 和 3);BaseUser 的 wait_time = between(...) 会读这两个值。 |
控制"思考时间",影响 RPS 和真实性。 |
| WEB_HOST / WEB_PORT | Locust 自带 Web UI,需要绑定地址和端口;默认 0.0.0.0 便于远程打开。 | 非 headless 启动时会用这两个值;若端口冲突可改 WEB_PORT。 | 方便在浏览器里看实时曲线和失败请求。 |
| DEFAULT_USERS / DEFAULT_SPAWN_RATE / DEFAULT_RUN_TIME | 命令行不传参时要有默认值,避免误跑"无限用户、无限时间"。 | run_tests.py 或 locust 命令里用 config_manager.get("DEFAULT_USERS", 10) 等;也可在脚本里覆盖。 |
默认行为安全、可预期。 |
日志配置(见上文 3.2.6)
性能监控配置
| 变量 | 为什么这样写 | 怎么用 | 实际意义 |
|---|---|---|---|
| PERFORMANCE_MONITOR_ENABLED | 有时只关心 Locust 自带统计,不需要框架再算一遍阈值。 | 设为 True 时 PerformanceMonitor 会按下面阈值做告警;False 则只记录不告警。 | 按需开启额外监控。 |
| MAX_RESPONSE_TIME / WARNING_RESPONSE_TIME | 响应时间超过阈值说明有性能问题,需要告警;分"警告"和"最大"两档便于区分严重程度。 | 单位毫秒;PerformanceMonitor 在统计时比较 P95/P99 与这些值,超则打日志或标记。 | 自动发现慢接口。 |
| MAX_ERROR_RATE / MIN_SUCCESS_RATE | 错误率或成功率是压测是否达标的关键指标,需要可配置阈值。 | 百分比;测试结束或周期统计时比较,超阈值则告警。 | 明确"通过/不通过"的标准。 |
测试场景配置(QUICK_TEST_ / STRESS_TEST_ 等)**
- 为什么这样写:不同目的(快速验证、标准压测、压力、负载、峰值)需要不同用户数、孵化速率、时长,用命名常量便于脚本和文档引用。
- 怎么用 :在 run_tests.py 或自定义脚本里用
config_manager.get("STRESS_TEST_USERS")等作为-u、-r、-t的默认值;或做多场景切换(如--scene stress时用 STRESS_TEST_*)。 - 意义:场景参数集中管理,避免命令行写死一长串。
HTTP 配置(HTTP_TIMEOUT、HTTP_VERIFY_SSL、HTTP_MAX_RETRIES 等)
- 为什么这样写:请求超时、SSL 校验、重试次数都影响压测行为和安全性,集中配置便于调优和区分环境(如内网关 SSL)。
- 怎么用 :在发请求的代码里用
config_manager.get("HTTP_TIMEOUT", 30)等;若 Locust 的 client 不支持,可在 on_start 或自定义 client 里设置。 - 意义:超时/重试一致,避免部分请求卡死或误跳过 SSL 校验。
报告配置(REPORT_FORMATS、REPORT_RETENTION_COUNT 等)
- 为什么这样写:报告格式、保留份数由运维或团队习惯决定,放配置里便于统一。
- 怎么用:ReportGenerator 生成报告时读这些值,决定是否生成 HTML/CSV/JSON、保留多少份历史。
- 意义:报告行为可配置、可收敛,不散落在代码里。
认证配置(BASIC_AUTH_*、BEARER_TOKEN、API_KEY)
- 为什么这样写:压测接口常需认证,账号/Token 不应写死在用例里;放 config 便于换环境换账号。
- 怎么用 :在 BaseUser.on_start 或具体 User 里用
config_manager.get("BEARER_TOKEN")设置self.client.headers["Authorization"]等;注意生产 Token 建议用环境变量覆盖。 - 意义:认证信息集中、可替换,避免泄露和误用。
数据驱动(TEST_DATA_FILE、DATA_ROTATION_STRATEGY、DATA_CACHE_ENABLED)
- 为什么这样写:数据驱动测试需要指定数据文件、轮询策略(轮询/随机/顺序)、是否缓存,集中配置便于切换策略。
- 怎么用:DataLoader 加载数据时读 TEST_DATA_FILE;按 DATA_ROTATION_STRATEGY 取每条数据;DATA_CACHE_ENABLED 控制是否在内存缓存文件内容。
- 意义:数据来源和策略统一,用例只关心"取一条数据"而不关心文件路径和策略。
5.2 环境配置
支持多环境配置:
python
# 当前环境
ENVIRONMENT = "development" # development, testing, staging, production
# 环境映射
ENVIRONMENT_MAP = {
"development": {
"BASE_URL": "https://dev-api.example.com",
"LOG_LEVEL": "DEBUG",
"HTTP_VERIFY_SSL": False
},
"production": {
"BASE_URL": "https://api.example.com",
"LOG_LEVEL": "WARNING",
"HTTP_VERIFY_SSL": True
}
}
5.3 测试场景配置
预设了 5 种测试场景:
python
# 快速测试配置
QUICK_TEST_USERS = 5
QUICK_TEST_SPAWN_RATE = 1
QUICK_TEST_RUN_TIME = "30s"
# 标准测试配置
STANDARD_TEST_USERS = 50
STANDARD_TEST_SPAWN_RATE = 5
STANDARD_TEST_RUN_TIME = "5m"
# 压力测试配置
STRESS_TEST_USERS = 200
STRESS_TEST_SPAWN_RATE = 10
STRESS_TEST_RUN_TIME = "10m"
# 负载测试配置
LOAD_TEST_USERS = 100
LOAD_TEST_SPAWN_RATE = 5
LOAD_TEST_RUN_TIME = "30m"
# 峰值测试配置
PEAK_TEST_USERS = 500
PEAK_TEST_SPAWN_RATE = 20
PEAK_TEST_RUN_TIME = "5m"
5.4 配置覆盖
支持在运行时覆盖配置:
python
# 命令行参数覆盖
python run_tests.py --users 100 --spawn-rate 10 --time 15m
# 代码中覆盖
config_manager._config.BASE_URL = "https://custom-api.com"
6. 性能监控
6.1 监控指标
性能监控器自动收集以下指标:
- 请求统计:总数、成功数、失败数
- 响应时间:平均、最小、最大、中位数、P95、P99
- 成功率:成功率和错误率
- 错误信息:错误类型、错误消息
6.2 阈值告警
当性能指标超过阈值时,会自动记录告警日志:
python
# 响应时间超过阈值
logger.warning(
f"性能告警 [GET请求]: 响应时间 1500.00ms 超过阈值 1000ms"
)
# 错误率超过阈值
logger.warning(
f"性能告警 [POST请求]: 错误率 6.50% 超过阈值 5.00%"
)
6.3 性能报告
测试结束时自动生成性能报告:
================================================================================
性能测试报告
================================================================================
请求: GET请求
总请求数: 1000
成功数: 995
失败数: 5
成功率: 99.50%
错误率: 0.50%
平均响应时间: 125.50ms
最小响应时间: 45.20ms
最大响应时间: 850.30ms
中位数响应时间: 120.00ms
P95响应时间: 250.00ms
P99响应时间: 450.00ms
请求: POST请求
...
================================================================================
7. 报告系统
7.1 HTML 报告
HTML 报告提供可视化的测试结果,包括:
- 统计表格:请求统计、响应时间统计
- 图表展示:响应时间分布、错误率趋势
- 错误详情:失败请求的详细错误信息
7.2 CSV 报告
CSV 报告便于在 Excel 中分析:
csv
Name,Requests,Failures,Median Response Time,Average Response Time,Min Response Time,Max Response Time,Requests/s,Failures/s
GET请求,1000,5,120.00,125.50,45.20,850.30,33.33,0.17
POST请求,500,2,180.00,195.30,80.50,1200.00,16.67,0.07
7.3 JSON 报告
JSON 报告便于程序处理:
json
{
"test_name": "performance_test",
"timestamp": "2024-01-01T12:00:00",
"summary": {
"total_requests": 1500,
"total_failures": 7,
"avg_response_time": 150.40
},
"requests": [
{
"name": "GET请求",
"requests": 1000,
"failures": 5,
"avg_response_time": 125.50
}
]
}
8. 最佳实践
8.1 测试用例设计
-
单一职责原则
- 每个测试方法只测试一个功能点
- 避免在一个方法中测试多个功能
-
清晰的命名
- 使用描述性的方法名
- 说明测试的目的和预期结果
-
完整的断言
- 不仅验证状态码,还要验证响应内容
- 使用合适的断言方法
-
错误处理
- 捕获和处理异常
- 记录详细的错误信息
8.2 性能测试策略
-
渐进式负载
- 从小负载开始
- 逐步增加负载
- 观察系统响应
-
测试场景选择
- 根据测试目的选择合适的场景
- 快速验证使用 quick 场景
- 压力测试使用 stress 场景
-
监控关键指标
- 响应时间
- 错误率
- 吞吐量
-
测试环境隔离
- 使用独立的测试环境
- 避免影响生产环境
8.3 数据管理
-
测试数据分离
- 使用独立的测试数据
- 避免使用生产数据
-
数据驱动测试
- 使用 JSON 文件管理测试数据
- 支持数据轮询和随机选择
-
数据清理
- 测试后清理测试数据
- 保持测试环境干净
8.4 日志管理
-
合适的日志级别
- 开发环境使用 DEBUG
- 生产环境使用 INFO 或 WARNING
-
结构化日志
- 使用统一的日志格式
- 包含足够的上下文信息
-
日志轮转
- 配置合适的文件大小限制
- 保留足够的备份数量
9. 高级用法
9.1 分布式测试
Locust 支持分布式测试,可以运行多个 Worker 节点:
bash
# Master 节点
locust -f tests/test_http_methods.py --master --host https://httpbin.org
# Worker 节点(可以运行多个)
locust -f tests/test_http_methods.py --worker --master-host=192.168.1.100
9.2 自定义事件监听
python
from locust import events
@events.test_start.add_listener
def on_test_start(environment, **kwargs):
logger.info("测试开始")
@events.test_stop.add_listener
def on_test_stop(environment, **kwargs):
logger.info("测试结束")
@events.request.add_listener
def on_request(request_type, name, response_time, response_length, **kwargs):
logger.debug(f"请求: {name}, 响应时间: {response_time}ms")
9.3 自定义测试数据生成
python
import random
import string
class DataGenerator:
@staticmethod
def generate_email():
"""生成随机邮箱"""
username = ''.join(random.choices(string.ascii_lowercase, k=8))
domain = random.choice(["example.com", "test.com", "demo.com"])
return f"{username}@{domain}"
@staticmethod
def generate_name():
"""生成随机姓名"""
first_names = ["张", "李", "王", "赵", "刘"]
last_names = ["三", "四", "五", "六", "七"]
return random.choice(first_names) + random.choice(last_names)
9.4 性能对比测试
python
class PerformanceComparisonUser(BaseUser):
def on_start(self):
super().on_start()
self.response_times = []
@task
def test_api_v1(self):
"""测试 API v1 性能"""
start_time = time.time()
with self.client.get("/api/v1/endpoint", name="API v1", catch_response=True) as response:
elapsed = (time.time() - start_time) * 1000
self.response_times.append(("v1", elapsed))
if response.status_code == 200:
response.success()
@task
def test_api_v2(self):
"""测试 API v2 性能"""
start_time = time.time()
with self.client.get("/api/v2/endpoint", name="API v2", catch_response=True) as response:
elapsed = (time.time() - start_time) * 1000
self.response_times.append(("v2", elapsed))
if response.status_code == 200:
response.success()
10. 扩展开发
10.1 添加自定义断言方法
在 core/assertions.py 中添加:
python
@staticmethod
def assert_response_size(response, min_size, max_size, name=""):
"""
断言响应大小在指定范围内
参数:
response: Locust 响应对象
min_size: 最小大小(字节)
max_size: 最大大小(字节)
name: 请求名称
返回:
True: 大小在范围内
False: 大小超出范围
"""
size = len(response.content)
if min_size <= size <= max_size:
return True
else:
logger.warning(
f"{name}: 响应大小断言失败 - 期望: {min_size}-{max_size}, 实际: {size}"
)
return False
10.2 添加自定义监控指标
在 core/performance_monitor.py 中扩展:
python
def on_request_success(self, name, response_time, response_length):
"""扩展:记录响应大小"""
stats = self.stats[name]
stats["total_response_length"] = stats.get("total_response_length", 0) + response_length
stats["max_response_length"] = max(stats.get("max_response_length", 0), response_length)
stats["min_response_length"] = min(stats.get("min_response_length", float('inf')), response_length)
# 调用原有逻辑
super().on_request_success(name, response_time, response_length)
10.3 添加自定义报告格式
在 core/report_generator.py 中添加:
python
def generate_excel(self, stats, test_name="performance_test"):
"""生成 Excel 报告"""
import pandas as pd
df = pd.DataFrame(stats)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
excel_file = os.path.join(self.report_dir, f"{test_name}_{timestamp}.xlsx")
df.to_excel(excel_file, index=False, engine='openpyxl')
logger.info(f"✅ Excel报告已生成: {excel_file}")
11. 故障排查
11.1 常见问题
问题1:导入错误
症状 :ModuleNotFoundError: No module named 'core.xxx'
解决方案:
- 检查项目根目录是否在 Python 路径中
- 确认
__init__.py文件存在 - 检查模块名称是否正确
问题2:配置文件不存在
症状 :FileNotFoundError: 配置文件不存在
解决方案:
- 检查
config/config.py文件是否存在 - 确认文件路径正确
- 检查文件权限
问题3:测试执行失败率高
可能原因:
- 目标服务不可用
- 网络连接问题
- 请求超时设置过短
- 并发数过高
解决方案:
- 检查目标服务状态
- 测试网络连接
- 增加超时时间
- 降低并发数
问题4:内存占用过高
可能原因:
- 响应数据过大
- 日志记录过多
- 统计数据积累
解决方案:
- 限制响应数据大小
- 调整日志级别
- 定期清理统计数据
11.2 调试技巧
启用调试日志
python
# 在 config/config.py 中
LOG_LEVEL = "DEBUG"
添加调试断点
python
import pdb
@task
def test_debug(self):
pdb.set_trace() # 添加断点
# 测试代码
查看详细日志
bash
# 查看日志文件
tail -f logs/performance_test.log
# 查看特定级别的日志
grep "ERROR" logs/performance_test.log
12. 性能优化
12.1 测试脚本优化
-
减少不必要的操作
- 避免在每个请求中重复初始化
- 使用
on_start()进行一次性初始化
-
优化断言逻辑
- 只验证必要的字段
- 避免深度嵌套的 JSON 解析
-
合理使用任务权重
- 根据实际业务场景设置权重
- 避免不必要的高频请求
12.2 配置优化
-
调整等待时间
pythonWAIT_TIME_MIN = 0.5 # 减少最小等待时间 WAIT_TIME_MAX = 1.0 # 减少最大等待时间 -
优化日志配置
pythonLOG_LEVEL = "WARNING" # 生产环境使用更高级别 LOG_FILE_MAX_BYTES = 5242880 # 减少文件大小 -
调整超时设置
pythonHTTP_TIMEOUT = 10 # 减少超时时间 HTTP_CONNECT_TIMEOUT = 5
12.3 系统优化
-
使用分布式测试
- 运行多个 Worker 节点
- 分散负载
-
优化网络环境
- 使用高速网络
- 减少网络延迟
-
监控系统资源
- 监控 CPU 使用率
- 监控内存使用率
- 监控网络带宽
附录
A. 配置文件完整示例
参考 config/config.py 文件。
B. 测试用例完整示例
参考 tests/ 目录下的测试文件。
C. API 参考
ConfigManager
python
config_manager.get(key, default=None) # 获取配置值
config_manager.get_base_url() # 获取基础 URL
Logger
python
logger.debug(message) # 调试日志
logger.info(message) # 信息日志
logger.warning(message) # 警告日志
logger.error(message) # 错误日志
logger.critical(message) # 严重错误日志
Assertions
python
assertion.assert_status_code(response, expected, name="")
assertion.assert_json_key_exists(response, key, name="")
assertion.assert_json_value(response, key, expected_value, name="")
assertion.assert_text_contains(response, text, name="")
DataLoader
python
data_loader.load_json(filename) # 加载 JSON 文件
data_loader.get_test_data(key, default=None) # 获取测试数据
data_loader.get_users() # 获取用户数据
PerformanceMonitor
python
performance_monitor.get_statistics(name=None) # 获取统计信息
performance_monitor.set_thresholds(...) # 设置性能阈值
performance_monitor.generate_performance_report() # 生成性能报告
ReportGenerator
python
report_generator.generate_csv(stats, test_name) # 生成 CSV 报告
report_generator.generate_json(data, test_name) # 生成 JSON 报告
总结
本框架提供了一个完整的企业级性能测试解决方案,包括:
- ✅ 模块化架构:清晰的模块划分,易于维护和扩展
- ✅ 完善的配置系统:灵活的配置管理,支持多环境
- ✅ 强大的断言机制:丰富的断言方法,详细的错误信息
- ✅ 实时性能监控:自动收集指标,阈值告警
- ✅ 多格式报告:HTML/CSV/JSON 格式,便于分析
- ✅ 便捷的运行工具:命令行脚本,批量运行支持
希望本文档能够帮助你更好地理解和使用这个性能测试框架!
围绕本框架的面试题
以下面试题围绕本 性能测试框架 的设计与实现,便于在面试中说明"你们性能测试怎么做的""框架怎么设计的"等问题。
一、框架架构与设计模式
1. 请说明本性能测试框架的整体架构分层,以及各层职责。
答:
- 配置数据层 :
config/config.py提供 BASE_URL、WAIT_TIME、日志、性能阈值、测试场景参数(用户数、孵化速率、时长)等;ConfigManager 单例统一加载。 - 基础设施层:ConfigManager(配置)、Logger(日志)、PerformanceMonitor(指标与阈值)、ReportGenerator(报告);为上层提供配置、日志、监控和报告能力。
- 核心服务层:BaseUser(继承 Locust HttpUser,提供 host、wait_time、on_start/on_stop、默认请求头)、Assertions(状态码/JSON/文本断言)、DataLoader(JSON 与 config 数据加载);定义"虚拟用户"的通用行为和断言方式。
- 测试执行层:具体 User 类(继承 BaseUser,用 @task 定义任务)、Locust 任务调度、catch_response 与断言、可选 PerformanceMonitor 埋点。
数据流:config.py → ConfigManager → BaseUser/DataLoader → 测试用例执行 → Assertions 校验 → PerformanceMonitor 统计 → ReportGenerator 出报告。
2. ConfigManager 为什么用单例模式?不用单例会有什么问题?
答:配置只需加载一次(读 config.py、执行模块),若每次 ConfigManager() 都 new 一个实例,会重复加载配置文件、多份配置对象,浪费内存且可能不一致(例如运行中有人改了 config 再 new 一个,就会出现两套配置)。单例保证全局只有一个实例,所有地方用 config_manager.get()、get_base_url() 拿到的都是同一份配置,行为可预期。
3. BaseUser 里 wait_time 和 host 为什么从 config 读,而不是在子类里写死?
答:
- wait_time:模拟用户"思考时间",不同场景需求不同(冒烟可短、仿真要长);从配置读便于调参,无需改代码。
- host :压测目标可能随环境变化(开发/测试/预发),统一从 config 的 BASE_URL 读,换环境只改配置,避免在多个 User 里改 URL。
这样 BaseUser 子类只关心"做什么任务",不关心"等多长时间、打哪个地址"。
4. 本框架里"模板方法模式"体现在哪里?子类一般要重写什么?
答:BaseUser 的 on_start()、on_stop() 是模板方法,定义了"用户启动时先打日志、再 _setup_headers""用户停止时打日志"的流程;子类若需要登录、加载数据等,可重写 on_start(),但应先调 super().on_start() 再写自己的逻辑,这样默认的请求头等仍会生效。子类主要实现的是带 @task 的方法(具体发什么请求、如何断言),一般不重写 host、wait_time。
5. 断言模块为什么设计成多个静态方法(assert_status_code、assert_json_key_exists 等),而不是一个方法传类型参数?
答:每种断言逻辑清晰、参数不同(状态码只要 expected,JSON 要 key 或 key+expected_value),拆成多个方法更易读、易扩展;新增断言类型时加一个新方法即可,不影响原有调用。在 Locust 的 catch_response=True 块里,按需调用不同断言方法,根据返回值决定 response.success() 或 response.failure("原因"),Locust 统计里会按"原因"汇总失败。
二、Locust 与用例编写
6. 本框架里 Locust 的 host、wait_time 是在哪里设置的?为什么不用在每个 @task 里写 base_url?
答:在 BaseUser 类上设置 host = config_manager.get_base_url() 和 wait_time = between(WAIT_TIME_MIN, WAIT_TIME_MAX);Locust 会用 host 作为所有请求的根 URL,用 wait_time 在每次 task 执行完后等待。因此 @task 里只需写相对路径(如 /get、/api/orders),不需要在 task 里再拼 base_url。
7. catch_response=True 和 response.success() / response.failure() 的作用是什么?
答:Locust 默认按 HTTP 状态码 2xx 判成功;但业务可能 200 却 errno≠0,需要按响应内容判成功/失败。使用 with self.client.get(..., catch_response=True) as response: 后,请求不会自动判成功,必须在 with 块里根据断言结果调用 response.success() 或 response.failure("原因"),Locust 才会正确统计成功/失败,并在报告里展示失败原因。
8. @task 和 @task(weight=5) 有什么区别?如何设计权重?
答:@task 等价于 weight=1;多个 @task 时,Locust 按权重比例随机选下一个任务,weight 越大被选中的概率越高。设计权重时,高频操作(如列表查询)权重大,低频操作(如删除、复杂提交)权重小,使虚拟用户行为更接近真实比例;例如列表:详情:提交 ≈ 6:3:1。
9. 若需要"先登录拿 token,再带 token 调其他接口",在本框架里如何实现?
答:在继承 BaseUser 的类里,在 on_start() 中调 super().on_start() 后发登录请求,从响应里取出 token,再写 self.client.headers["Authorization"] = "Bearer " + token(或项目约定的 header);之后该用户实例的所有请求都会带上该 token。若登录失败可打日志或抛异常,避免后续请求全部失败。
三、数据、监控与报告
10. DataLoader 的 load_json 和 get_test_data 分别用在什么场景?
答:
- load_json(filename) :从
data/目录加载 JSON 文件(如 test_data.json),得到用户列表、请求体列表等;用于数据驱动,同一套脚本多组数据。 - get_test_data(key, default) :从 config.py 里按变量名取数据(如 TEST_DATA_POST_DATA),适合少量、和配置放在一起的测试数据。
get_users() 是便捷方法,直接返回用户列表,避免各处写 data.get("users", [])。
11. PerformanceMonitor 的 set_thresholds 和 get_statistics 分别解决什么问题?
答:
- set_thresholds:设定"通过/不通过"的标准(如最大响应时间、最大错误率、最低成功率),测试结束或周期检查时自动判断本次压测是否达标,便于做自动化质量门禁。
- get_statistics(name):按请求 name 统计该接口的请求数、成功数、失败数、平均/P95/P99 响应时间等,精确定位哪个接口慢或错误率高,而不只是看整体。
若在请求回调里埋点,需注意线程安全(或使用 Locust 提供的事件钩子)。
12. 报告为什么要支持 CSV 和 JSON,而不仅是 HTML?
答:HTML 便于人工查看和汇报;CSV 便于用 Excel 做趋势对比、归档;JSON 便于被 CI、监控系统或脚本解析,做自动告警、历史对比。多格式满足"人看"和"机器用"两种需求,本框架通过 ReportGenerator 统一生成,输出目录和保留份数可由配置控制。
四、配置与运行
13. config.py 里 QUICK_TEST_、STRESS_TEST_ 等场景配置有什么用?
答:不同目的需要不同参数:快速验证用少量用户、短时间(QUICK_TEST_);压力测试用大用户数、高孵化速率(STRESS_TEST_ );负载/稳定性用中等用户数、长时间。在 run_tests.py 或脚本里用 config_manager.get("STRESS_TEST_USERS") 等作为 -u、-r、-t 的默认值,或按 --scene stress 等参数切换场景,避免命令行写死一长串。
14. 如何在本框架下做分布式压测?
答:Locust 支持 master-worker 模式;config 中可配置 MASTER_HOST、MASTER_PORT、WORKER_NODES 等。启动 master 后,在各机器上启动 worker 连接 master,由 master 统一调度任务和汇总统计。本框架的 BaseUser 和断言逻辑在 worker 上同样生效,只需保证 config 和代码在各节点一致、BASE_URL 指向同一目标。
15. 若压测时登录失败率很高,可能是什么原因?如何在本框架中规避?
答:可能原因:并发登录过多被限流、账号被锁、验证码、网络抖动。规避方式:① 降低孵化速率(spawn rate),让用户逐渐启动;② 在 on_start 里加随机短延迟再登录,避免同时登录;③ 每个虚拟用户用独立账号或 token 池,避免单账号并发;④ 检查 BASE_URL、超时、重试配置是否合理。本框架的 BaseUser 可在 on_start 中按上述方式扩展。
五、Logger 与配置加载
16. 本框架的日志为什么同时输出到控制台和文件?文件为什么要轮转?
答:控制台便于实时看运行情况;文件便于事后排查、归档。轮转(如按大小 10MB、保留 5 个备份)防止单文件无限增大占满磁盘,同时保留近期多份日志便于追溯。
17. ConfigManager 的 _load_config 为什么用 importlib 动态加载 config.py,而不是 import config?
答:动态加载可以明确指定 config 文件路径(如项目根下的 config/config.py),不依赖当前工作目录或包结构;且便于测试时替换成 mock 配置。若用 import config,则依赖 Python 路径,且难以在单测里换一份配置。
18. config.py 里 LOG_LEVEL 改成 DEBUG 后,本框架哪些地方会多出日志?
答:Logger 的级别设为 DEBUG 后,所有 logger.debug(...) 会输出;框架里可在请求前后、断言细节、DataLoader 加载、ConfigManager 读取等处打 debug,便于排查"请求发了没、断言取到的值是什么"等。
19. 若想在不同环境用不同的 BASE_URL,本框架可以怎么扩展?
答:在 config.py 里按 ENVIRONMENT 或环境变量选择不同 BASE_URL;或在 config 里写 ENVIRONMENT_MAP,ConfigManager 增加 get_base_url(env) 或根据当前 ENVIRONMENT 取对应 BASE_URL。run_tests 或启动 Locust 前设置好环境变量即可。
20. 性能阈值的 WARNING 和 MAX 两档分别用来做什么?
答:WARNING 用于"提醒关注",如响应时间超过 500ms 打 warning 日志,不视为不通过;MAX 用于"硬性不通过",如超过 1000ms 或错误率超过 5% 则判定本次压测不达标。这样既能提前发现劣化,又有明确的通过线。
六、BaseUser 与任务设计
21. _setup_headers 为什么在 on_start 里调,而不是在类里直接写 self.client.headers = {...}?
答:Locust 的 HttpUser 在用户启动后才会创建 self.client,类定义时 client 还不存在;on_start 是每个用户启动时调用的,此时 client 已就绪,在 on_start 里 update headers 才能生效。
22. 子类重写 on_start 时为什么要先 super().on_start()?
答:BaseUser 的 on_start 里会执行 _setup_headers,把默认的 User-Agent、Accept、Content-Type 等设好;若子类不调 super(),这些默认头就不会被设置,可能影响服务端识别或返回格式。先 super() 再在子类里追加(如 Authorization),既保留默认又加上业务头。
23. 一个 Locust 文件里可以定义多个 User 类吗?Locust 会怎么调度?
答:可以。Locust 会按比例或权重同时模拟多种用户(如 70% 普通用户、30% 管理员);在 Web UI 或命令行可配置各 User 类的数量比例。本框架可定义多个继承 BaseUser 的类,分别实现不同场景(浏览、下单、管理后台等),更贴近真实用户组成。
24. 若某个 @task 里要发多个请求(如先 get 再 post),算一次 task 还是多次?对统计有什么影响?
答:算一次 task,但会发多次 HTTP 请求;Locust 的统计按"请求"维度(每个 client.get/post 一条),所以会看到多条请求记录。若希望"这一组请求"在报告里算一个业务操作,可以用 name 参数把多个请求写成同一 name(如 name="下单流程"),便于按业务聚合。
25. wait_time 用 between(1,3) 和 constant(2) 对压测结果有什么不同影响?
答:between 更接近真实用户(每次间隔随机),constant 是固定间隔。固定间隔时 RPS 更稳定、便于算理论值;随机间隔时曲线更自然,但单次运行的 RPS 会有波动。本框架从配置读 WAIT_TIME_MIN/MAX 用 between,若需要可改为 constant 做对比测试。
七、断言与失败统计
26. response.failure("原因") 里写的"原因"在 Locust 报告里哪里能看到?
答:在 Locust Web UI 的 "Failures" 或 "Exceptions" 里会列出失败请求及对应的 failure 原因;导出报告或日志里也会包含。因此 failure 原因建议写清楚(如"状态码非 200""业务码 errno=500"),便于排查。
27. 断言模块的 name 参数是干什么的?
答:name 会出现在日志和统计里,用于区分"是哪个请求/哪类校验"失败;例如 assert_status_code(response, 200, "登录"),失败时日志里会带"登录",便于定位是登录接口有问题。
28. 若多个 task 里都用 catch_response 且可能调用 response.failure,如何保证线程安全?
答:Locust 每个虚拟用户通常在一个协程/线程里顺序执行 task,同一用户不会并发调 failure;不同用户之间各用各的 response 对象,不共享。若在 PerformanceMonitor 等模块里做跨请求的统计,需要用线程安全结构(如 threading.Lock 或 queue)或 Locust 提供的事件钩子汇总。
29. 本框架里如何新增一种断言类型(如"响应时间小于 100ms")?
答:在 Assertions 模块里加一个新方法(如 assert_response_time(response, max_ms)),在需要的地方调用;若希望和 assert_status_code 一样通用,可在文档和示例里说明,让大家在 catch_response 块里按需调用并据此 success/failure。
30. 业务上 200 但 body 里 code≠0 算成功还是失败?本框架怎么处理?
答:算业务失败;应在 catch_response 块里解析 body,若 code≠0 则 response.failure("业务错误: code=xxx"),否则 response.success()。这样 Locust 统计的失败数才反映真实业务失败,报告里也能按失败原因分类。
八、数据驱动与场景
31. 数据驱动时,若 data/test_data.json 不存在或为空,本框架应怎么处理?
答:DataLoader.load_json 应在文件不存在或解析失败时抛异常或打 ERROR 日志并返回明确结果(如 None 或空 dict),调用方检查后再用;避免静默返回空导致"看起来在跑、实则没数据"的假通过。get_users() 等可返回空列表并打 warning,由用例决定是否 skip。
32. config 里 DATA_ROTATION_STRATEGY(round_robin/random/sequential)各适合什么场景?
答:round_robin 按顺序轮流取,数据使用均匀、可复现;random 随机取,更接近真实随机行为;sequential 按顺序且可能支持"用满再从头",适合需要遍历完一轮数据的场景。本框架若支持可在 DataLoader 里按该配置选择策略。
*33. run_tests.py 里 --users、--spawn-rate、--time 和 config 里的 DEFAULT_ 是什么关系?**
答:通常命令行参数覆盖 config 默认值;若用户不传则用 config_manager.get("DEFAULT_USERS", 10) 等。这样既有合理默认,又支持单次运行覆盖,便于脚本化和 CI。
34. 如何设计"混合场景"(如 60% 浏览、30% 下单、10% 取消)?
答:定义多个 User 类,每个类里 @task 的权重或任务比例不同;或在 Locust 启动时指定各 User 类的数量比例(如 --user 比例)。本框架可定义 BrowseUser、OrderUser、CancelUser 都继承 BaseUser,再在 locustfile 或命令行里配置 6:3:1 的用户数比例。
35. 稳定性测试(长时间压测)时,本框架需要注意什么?
答:① 日志轮转和保留份数,避免日志占满磁盘。② 报告是否按时间分片或定期输出,便于中途查看。③ 内存:DataLoader 是否缓存大量数据、PerformanceMonitor 是否无限累积数据,必要时做上限或定期清理。④ 连接池/资源:长时间运行是否有连接泄漏,可观察压测机内存和连接数。
九、监控、报告与排错
36. P95、P99 响应时间比平均响应时间更能说明什么问题?
答:平均会被少数极慢请求拉高或拉低,不能反映"大多数用户"的体验;P95 表示 95% 的请求在该值以内,P99 表示 99% 的请求在该值以内,更能反映长尾和 SLA(如"99% 请求 < 500ms")。本框架 PerformanceMonitor 若支持分位数统计,可据此设阈值和做报告。
37. 压测过程中响应时间突然升高,可能从哪几方面排查?
答:① 服务端:CPU、内存、线程池、数据库连接池是否打满。② 网络:带宽、丢包、DNS。③ 本机:压测机 CPU、网络、打开文件数是否到上限。④ 业务:是否有慢 SQL、锁等待、下游超时。本框架可结合日志里失败原因、Locust 报告里慢请求的 name、以及服务端监控一起看。
38. 错误率突然升高,本框架下如何快速定位是哪些请求、什么原因?
答:看 Locust 的 Failures 列表,每条会显示 URL、name、失败原因(即 response.failure 里写的)、出现次数;按 name 或 URL 归类,再看对应断言或业务逻辑。本框架断言时 failure 原因写得越具体,定位越快。
39. 报告保留份数(REPORT_RETENTION_COUNT)设多大合适?
答:视磁盘和需求定:一般保留最近 10~30 份,便于趋势对比又不占太多空间;若 CI 每天跑多次,可适当增大或按天归档。本框架 ReportGenerator 在生成新报告时可按配置删除最旧的,保证只保留 N 份。
40. 新人接手本性能框架,你会让他先看哪几个文件、按什么顺序?
答:建议顺序:① 本文档的"框架概述"和"架构设计",建立整体印象。② config/config.py,看有哪些可配置项。③ core/config_manager.py 和 core/base_user.py,看配置怎么读、用户怎么启动和发请求。④ 一个具体的 User 类(如 tests 下某文件),看 @task 和 catch_response 怎么写。⑤ core/assertions.py 和 DataLoader,看断言和数据怎么用。⑥ run_tests.py,看如何一键跑起来。这样由总到分、由配置到执行,容易上手。
十、设计取舍与对比
41. 本框架用 config.py(Python)而不是 YAML/JSON 做配置,有什么优缺点?
答:优点:可写注释、可做简单运算、可 import 其他模块,类型清晰。缺点:改配置要动代码仓库、非开发不习惯。YAML/JSON 易被其他系统解析、便于界面编辑;若团队希望运维改配置不动代码,可增加一层"从 YAML 加载并覆盖 config 默认值"。
42. BaseUser 不继承时,直接写 HttpUser + 自己设 host,和用本框架相比差在哪?
答:直接写要自己读配置、自己设 wait_time、自己写请求头、自己写断言和 success/failure;本框架把这些收敛到 BaseUser、ConfigManager、Assertions,子类只写业务 task,维护和统一行为更好。多人协作时都继承 BaseUser,风格一致、改一处全局生效。
43. Locust 和 JMeter 比,本框架选 Locust 的原因一般有哪些?
答:Locust 纯 Python,和本框架一致、易扩展和二次开发;脚本即代码,版本管理和评审方便;分布式、Web UI、命令行都支持。JMeter 界面化、无代码,但复杂逻辑和定制不如代码灵活。本框架在 Locust 之上再封一层配置和断言,兼顾"写代码"的灵活和"配参数"的集中管理。
44. 性能测试里"用户数"和"RPS"的关系,本框架里如何理解?
答:RPS = 用户数 × 每用户每秒请求数;每用户每秒请求数受 wait_time 和单次请求耗时影响。本框架 wait_time 从 config 读,用户数由 -u 和孵化速率控制;调高用户数或减小 wait_time 会提高 RPS,但服务端可能成为瓶颈导致响应变慢,需结合监控看。
45. 本框架的 PerformanceMonitor 和 Locust 自带的统计有什么区别?
答:Locust 自带统计请求数、响应时间、失败数等;本框架 PerformanceMonitor 可在此基础上做阈值判断(如超过 MAX_RESPONSE_TIME 判不通过)、按 name 聚合、或输出自定义格式报告。两者可并存:Locust 看实时曲线,PerformanceMonitor 做质量门禁和归档。
十一、安全、数据与稳定性
46. 压测用的账号、Token 放在 config.py 里提交到 Git 有什么风险?如何规避?
答:风险:仓库泄露则账号泄露,可能被滥用或误操作生产。规避:敏感信息用环境变量(如 TEST_USER、TEST_TOKEN),config 里用 os.getenv() 读取;CI 里把变量配在流水线 secret 中;config.py 只保留占位符或示例值。
47. 数据驱动时,若多用户共用一个账号或同一批数据,会有什么问题?
答:可能造成数据冲突(如同时改同一条订单)、或服务端限流/踢人(单账号多会话)。本框架更稳妥的方式是:每个虚拟用户独立账号,或从 DataLoader 轮询/随机取不同数据,避免多用户抢同一份数据。
48. 长时间压测时,如何避免本框架或 Locust 自身内存持续增长?
答:① DataLoader 若全量加载大 JSON 进内存,可改为按需读取或流式解析。② PerformanceMonitor 若逐条记录请求,可设上限或只保留聚合结果、定期清明细。③ Locust 的请求历史可配置保留条数。④ 观察压测机内存曲线,定位是框架还是被测服务增长。
49. 本框架如何支持"压测开始前先执行一批准备请求(如预热、造数)"?
答:可在 run_tests 或 Locust 的 on_start 事件里,在启动虚拟用户前用 requests 或同一 BaseUser 的 client 发一批预热请求;或写独立脚本先造数再启动 Locust。若希望每个用户启动前都做一次准备,在 BaseUser.on_start 里调 super() 后发准备请求即可。
50. 压测过程中发现大量 502/503,可能是什么原因?本框架如何配合排查?
答:可能原因:服务过载、上游超时、连接池耗尽、依赖下游挂掉。本框架可配合:① 看 Locust Failures 里失败请求的 name 和 URL,定位是哪些接口。② 看 failure 原因是否统一(如"连接被重置""超时")。③ 结合服务端监控(CPU、内存、连接数、下游调用)和日志;本框架 Logger 和报告可记录时间点,便于和服务端日志对齐。
十二、进阶与扩展
51. 若要在本框架里支持"按比例分配不同 User 类"(如 70% 浏览 30% 下单),如何配置?
答:Locust 启动时可指定多个 User 类及权重或数量比例;或在 locustfile 里导出多个类,Web UI 里可填各类的数量。本框架定义多个继承 BaseUser 的类,在 run_tests 或命令行里传相应参数即可实现比例控制。
52. 本框架的 ReportGenerator 若想增加"和上次运行结果对比"的功能,可以怎么设计?
答:报告里带时间戳或 build id;生成新报告时读取上一份报告(如按命名规范取最新),解析出各 name 的 RPS、响应时间、错误率,和新结果做差或比,在 HTML/JSON 里增加"较上次 ±xx%"的展示。需约定报告命名和存放路径。
53. 如何在本框架下做"阶梯增压"(用户数随时间阶梯增加)?
答:Locust 默认是线性孵化;阶梯增压可用 LoadShape:自定义 shape 类,在 tick() 里根据当前运行时间返回 (user_count, spawn_rate),实现"前 5 分钟 50 用户、接下来 5 分钟 100 用户"等。本框架可在 locustfile 里引入该 shape,其余 BaseUser/断言不变。
54. 本框架里如何区分"预期失败"(如故意传错参数)和"非预期失败"?
答:在 catch_response 里根据业务逻辑区分:若请求是"故意测错误参数",返回 4xx 或 errno≠0 时调用 response.success(),并可在 name 里带后缀(如 "登录-错误参数"),便于在报告里单独筛;非预期失败照常 response.failure("原因")。这样统计里成功/失败含义清晰。
55. 你如何向产品或开发解释"我们性能测试框架是怎么做的"?
答:可以这样说:① 我们用 Locust 模拟多用户并发访问接口,用户行为(访问哪些接口、间隔多久)用 Python 脚本定义,继承统一的 BaseUser,读统一配置。② 每个请求会根据响应内容判断成功还是失败(不只看 200),失败原因会记到报告里。③ 运行前可配置用户数、时长、思考时间等,支持多场景(快速验证、压力、稳定性)。④ 结果有实时曲线和多格式报告,还能按阈值自动判断本次压测是否达标,便于做容量评估和发布门禁。
性能测试通用面试题(基础与进阶)
以下为性能测试领域的通用题,不限于本框架,适合考察基础概念和一定难度的问题。
一、性能测试基础题
1. 什么是性能测试?和功能测试有什么区别?
答:性能测试是评估系统在特定负载下的响应时间、吞吐量、资源占用、稳定性等非功能指标。功能测试关注"对不对",性能测试关注"快不快、稳不稳、能撑多少"。性能测试通常在功能稳定后进行,需要模拟多用户、持续施压并收集指标。
2. 常见的性能测试类型有哪些?分别解决什么问题?
答:① 负载测试:在预期负载下验证响应时间和吞吐量是否达标。② 压力测试:逐步加压直到系统出现性能拐点或错误,找瓶颈和极限。③ 稳定性/耐力测试:在一定负载下长时间运行,看是否有内存泄漏、性能劣化。④ 并发测试:验证多用户同时操作时的正确性和性能。⑤ 峰值测试:模拟短时高峰(如秒杀),看系统能否扛住。
3. 性能测试常用指标有哪些?分别怎么理解?
答:① 响应时间:从发请求到收到完整响应的时间,可看平均、P95、P99。② 吞吐量(TPS/RPS):每秒事务数/请求数,表示系统处理能力。③ 并发数:同时处理的请求或用户数。④ 成功率/错误率:成功请求占比、失败占比。⑤ 资源使用率:CPU、内存、磁盘 I/O、网络带宽。⑥ 数据库:连接数、慢查询、锁等待。
4. 什么是并发用户数、在线用户数?它们和 TPS 有什么关系?
答:在线用户数指同时"在线"的用户(可能在看页面不操作);并发用户数指同时"发起请求"的用户。TPS = 并发用户数 × 每用户每秒请求数(受业务操作和响应时间影响)。例如:1000 在线、10% 在操作、平均每人每秒 0.5 次请求,则并发约 100,TPS 约 50。
5. 什么是思考时间(Think Time)?为什么要设置?
答:思考时间是用户两次操作之间的间隔,模拟"阅读、点击、输入"等行为。设置后,虚拟用户不会连续发请求,RPS 会下降但更接近真实;不设则会把系统压到极限,适合找瓶颈,但不代表真实体验。性能测试时可根据业务设定 between(min, max) 或固定值。
6. 性能测试的基本流程是什么?
答:① 需求与目标:明确性能指标(如 P99<500ms、TPS≥1000)、业务场景。② 环境与数据:准备与生产接近的测试环境、基础数据。③ 场景设计:模拟用户行为、比例、时长、数据。④ 脚本开发:用工具(如 Locust/JMeter)实现场景、断言、数据驱动。⑤ 执行与监控:施压同时监控服务端和压测机资源、日志。⑥ 结果分析:看响应时间、TPS、错误率、资源,定位瓶颈。⑦ 优化与回归:优化后再次压测验证。
7. 如何确定"要压多少用户、压多久"?
答:用户数:可根据历史峰值或业务预估(如日活×同时在线比例×操作比例),或从少到多逐步加压直到响应时间/错误率恶化。时长:功能验证可 1~5 分钟;负载/压力测试一般 10~30 分钟;稳定性测试建议 1~4 小时或更长。可参考业务高峰时长或 SLA 要求。
8. 性能测试时为什么要监控服务端资源(CPU、内存等)?
答:性能问题往往伴随 CPU 打满、内存增长、磁盘 I/O 高、网络带宽满等;通过监控可判断瓶颈在应用、数据库还是网络。不监控则只能看到"变慢",难以定位是代码、配置还是资源不足。
9. 什么是性能基线(Baseline)?有什么用?
答:基线是某次(或某版本)压测的典型结果(如 TPS、P95、错误率),作为后续版本的对比参考。新版本或优化后再次压测,与基线对比,可判断是否退化、优化是否有效;也可作为 SLA 或发布门禁的参考。
10. 性能测试和压力测试是一回事吗?
答:不完全是。性能测试是广义的,包含负载、压力、稳定性等;压力测试特指"不断加压直到系统出现明显劣化或崩溃",目的是找极限和瓶颈。日常说的"做性能"常指负载测试(验证是否达标),"做压测"常指压力测试(找拐点)。
二、性能测试进阶 / 有难度题
11. 如何分析性能瓶颈?从哪些层面入手?
答:① 应用层:线程池、连接池是否打满;是否有慢方法、死锁、大量 GC。② 数据库:慢 SQL、锁等待、连接池耗尽、索引缺失。③ 网络:带宽、延迟、丢包、DNS。④ 中间件:MQ 堆积、缓存命中率、限流。⑤ 系统资源:CPU、内存、磁盘 I/O、文件句柄。手段:APM、日志、数据库监控、系统监控(top/iostat)、压测报告中的慢请求与错误分布。
12. 响应时间曲线出现"拐点"说明什么?如何利用拐点?
答:拐点表示随并发或负载增加,响应时间明显变陡或错误率明显上升,说明系统开始扛不住。利用方式:记录拐点对应的并发数、TPS,作为容量参考;拐点前可作为"安全容量",拐点附近可作为"极限容量";优化后再次压测,看拐点是否右移(容量是否提升)。
13. 如何设计"接近真实"的压测场景?
答:① 用户行为:按业务统计各操作占比(如浏览:下单:支付 ≈ 7:2:1),用 task 权重或用户类比例模拟。② 思考时间:用 between 或分布模拟操作间隔。③ 数据:用真实或脱敏数据、数据量级接近生产。④ 时段:可模拟高峰时段流量曲线(阶梯增压)。⑤ 混合场景:多类用户(普通用户、管理员)按比例混合。
14. 压测时如何避免"压测机"成为瓶颈?
答:① 用多台压测机做分布式(如 Locust worker),把负载分散。② 监控压测机 CPU、内存、网络、连接数;若打满则加机器或减单机并发。③ 请求体、断言尽量简单,减少本机 CPU 消耗。④ 使用连接复用(如 Session)、合理超时,避免本机句柄或端口耗尽。
15. 什么是容量规划?性能测试如何支撑容量规划?
答:容量规划是根据业务增长预估所需资源(机器、数据库、带宽等)。性能测试支撑:通过压测得到单机或当前集群的 TPS/容量拐点、以及资源使用率;结合业务峰值预估(如大促 3 倍流量),计算需要多少实例、多少数据库连接等,并留一定余量。
16. 稳定性测试中如何判断"通过"或"不通过"?
答:① 明确指标:如 2 小时内 P99 不持续劣化、错误率<0.1%、无内存持续增长。② 持续监控:记录响应时间、错误率、内存等随时间变化;若内存单调上升、错误率后期明显升高则不合格。③ 结合日志和 dump:若有 OOM、连接泄漏、慢查询增多,则判不通过并分析根因。
17. 性能测试结果受哪些因素影响?如何保证结果可复现、可对比?
答:影响因素:环境(机器、网络、数据)、代码版本、配置、并发策略、思考时间、数据量。可复现与对比:① 环境与数据尽量固定或脚本化。② 记录代码版本、配置、压测参数。③ 多次运行取平均或中位数,减少波动。④ 同一基线环境上对比不同版本或不同优化。
18. 什么是秒杀/高并发场景的性能测试要点?
答:① 瞬时高并发:短时间(如 1 秒内)大量请求,模拟秒杀开始。② 库存与超卖:校验库存扣减正确、无超卖。③ 限流与降级:观察限流、排队、降级策略是否生效。④ 响应时间与错误率:即使部分请求被限流,也要关注成功请求的响应时间和整体错误率。⑤ 数据一致性:压测后校验订单、库存等数据是否正确。
19. 性能测试和全链路压测有什么区别?
答:性能测试一般在独立测试环境,用模拟或脱敏数据。全链路压测是在生产或类生产环境,沿真实调用链施压(含下游、中间件、数据库),数据常做染色或隔离。全链路更真实但成本高、风险大,需要流量隔离、数据隔离、可灰度;性能测试更安全,适合日常容量评估和优化验证。
20. 如何向非技术人员解释"这次压测的结论"?
答:用业务语言:① 在多少用户/多少请求量下,系统表现如何(如"1000 人同时用,页面平均 1 秒内打开")。② 有没有报错、卡死(如"错误率 0.1%,可接受")。③ 瓶颈在哪(如"数据库慢了,加索引/扩容后可改善")。④ 建议(如"当前配置可支持 500 人在线,大促建议扩容到 2 倍")。避免堆术语,用"快/慢/能撑多少人"等说法。
三、性能测试补充题(简单 + 有点难度)
简单题
21. 响应时间"平均"和"P95"哪个更能反映用户体感?为什么?
答:P95 更能反映。平均会被少数极慢或极快请求拉偏;P95 表示 95% 的请求在该值以内,更能代表"大多数用户"的体验,也常用来定 SLA(如 P95 < 500ms)。
22. 压测时为什么要单独监控数据库?
答:很多性能瓶颈在数据库:慢 SQL、锁等待、连接池满会导致接口变慢甚至超时。不单独监控数据库时,只能看到"接口慢",难以判断是应用逻辑慢还是数据库慢;监控后可针对性优化 SQL、索引或库容量。
23. 什么是 TPS?和 QPS 有区别吗?
答:TPS 是每秒事务数,QPS 是每秒请求数。一个"事务"可能包含多个请求(如一次下单可能调多个接口),所以 TPS 一般 ≤ QPS。很多场景混用两者,接口压测时更常说 RPS/QPS(每秒请求数)。
24. 压测环境和生产环境不一致时,结果还能参考吗?
答:可参考趋势和相对关系(如哪个接口慢、优化后是否变好),但绝对值(如"生产能撑 1000 TPS")不能直接照搬。要评估生产容量时,需尽量缩小环境差异(机器规格、数据量、网络)或按比例折算,并留余量。
25. 性能测试通过的标准一般怎么定?
答:结合业务和 SLA:如响应时间 P95 < 500ms、错误率 < 0.1%、成功率 > 99.9%、在预期并发下无超时或崩溃。标准要在压测前和产品/开发达成一致,便于结论有据可依。
有点难度
26. 为什么有时"加机器"后 TPS 没成倍涨?
答:可能瓶颈不在 CPU/内存:① 瓶颈在数据库,加应用机无法提升。② 瓶颈在单点(如单库、单 MQ),需要分库分表或扩容中间件。③ 存在锁或串行化逻辑,并发上去后锁竞争加剧。④ 网络或带宽成为瓶颈。要结合监控找真正的瓶颈点。
27. 如何设计"梯度增压"场景?目的是什么?
答:设计:用户数或 RPS 按时间阶梯增加(如每 5 分钟加 100 用户),而不是一上来就满负载。目的:观察系统随负载升高时的表现变化,找到拐点;避免一开始就压垮导致无法观察"从正常到异常"的过程。
28. 压测结果波动很大(同一脚本多次跑差异大),可能是什么原因?怎么减小波动?
答:可能原因:环境中有其他任务、网络抖动、GC、数据库缓存冷热、数据量变化。减小波动:多跑几次取平均或中位数;压测前预热(先跑一段再统计);固定数据量或脚本化造数;环境尽量独占、减少干扰。
29. 性能测试和混沌工程有什么关系?
答:性能测试主要看"在预期负载下是否达标、极限在哪";混沌工程是在运行中人为制造故障(如断网、杀进程、延迟),看系统是否可用、是否自恢复。两者结合:先做性能测试了解基线,再在类似负载下做混沌实验,验证高可用和容错。
30. 线上能不能做性能测试?全链路压测和普通压测有什么不同?
答:线上可以做,但需严格控制:用染色/隔离的流量,不影响正常用户;数据隔离(如压测订单单独标记);限流、降级、监控告警就绪。全链路压测是在真实调用链上施压(含下游、中间件、数据库),更真实但成本高、风险大;普通压测多在测试环境,成本低、易反复做。
JMeter 使用面试题
以下为 Apache JMeter 相关的基础与进阶题,可与本框架(Locust)对比理解。
一、JMeter 基础题
1. JMeter 是什么?一般用来做什么?
答:Apache JMeter 是 Java 开发的性能与负载测试工具,支持 HTTP、FTP、JDBC、JMS 等协议。常用于接口压测、Web 压力测试、数据库压力测试,通过线程组模拟多用户并发。
2. 线程组是什么?和"用户数""Ramp-Up"有什么关系?
答:线程组定义虚拟用户:线程数相当于并发用户数;Ramp-Up 是这些用户在多少秒内全部启动完成。例如 100 线程、Ramp-Up 50s,表示 50 秒内均匀启动 100 个用户。Ramp-Up 过短会瞬间打满,过长则达到目标并发的时间拉长。
3. 取样器(Sampler)、监听器(Listener)、断言(Assertion)在 JMeter 里分别做什么?
答:取样器:发请求(如 HTTP Request)。监听器:收集与展示结果(如查看结果树、聚合报告、图形结果)。断言:对响应做校验(如响应码、响应体包含某文本)。执行顺序一般是:取样器执行 → 断言校验 → 监听器记录。
4. 参数化是什么?JMeter 里常用哪几种方式?
答:参数化是把请求中的固定值改为变量,使每次请求可带不同数据。常用方式:① CSV 数据文件:从 CSV 读数据,多线程可配置是否共享。② 用户自定义变量:全局或线程组级变量。③ 函数:如 __Random、__time、__UUID 等。用于登录态、订单号、用户 ID 等不重复数据。
5. 什么是关联?为什么需要?JMeter 里怎么实现?
答:关联指从上一个请求的响应里取出数据(如 token、订单号),供后续请求使用。因为很多接口依赖前面接口的返回。实现:用后置处理器,如正则提取器(Regular Expression Extractor)或 JSON 提取器,把匹配到的值写入变量,后续请求用 ${变量名} 引用。
6. 聚合报告里常见指标有哪些?分别表示什么?
答:样本数(请求数)、平均值/中位数(响应时间)、90%/95%/99% 百分位、最小值/最大值、异常%(失败率)、吞吐量(TPS)、接收/发送 KB/s。性能分析时重点看吞吐量、异常率、以及 P90/P95 响应时间。
7. 定时器(Timer)有什么作用?思考时间怎么加?
答:定时器用于在请求之间加入等待时间,模拟用户操作间隔。常用"固定定时器"或"均匀随机定时器"设置思考时间(如 1~3 秒)。放在线程组下对所有请求生效;放在某取样器下只对该请求后生效。不加定时器会全力压测,适合找瓶颈;加定时器更接近真实用户行为。
8. 分布式压测在 JMeter 里怎么实现?
答:一台控制机(Controller)配多台压力机(Agent)。压力机运行 jmeter-server,控制机用远程启动把脚本分发到各 Agent 执行,结果回传控制机汇总。用于单机线程数或网络受限时,把负载分散到多台机器。
9. 如何用 JMeter 做接口的简单功能校验(断言)?
答:在 HTTP 请求下加断言:响应断言可检查响应码、响应体包含/匹配某字符串、响应时间;JSON 断言可检查 JSON 路径对应的值。断言失败则该取样器记为失败,在监听器里能看到失败数和原因。
10. 测试计划里"用户定义的变量"和"用户参数"有什么区别?
答:用户定义的变量:全局,所有线程共享同一组值,在启动时初始化一次。用户参数:可为每个线程(或迭代)提供不同值,适合每用户一个账号等场景。按需选择:全局配置用前者,每用户不同数据用后者或 CSV。
二、JMeter 进阶 / 有点难度题
11. JMeter 里如何实现"只登录一次,后续请求复用登录态"?
答:① 把登录请求放在"仅一次控制器"(Once Only Controller)里,或放在线程组最前且用 If 控制器控制只跑第一次迭代。② 用后置处理器从登录响应提取 token/session,存到变量。③ 后续请求在 HTTP 信息头管理器或 Cookie 管理器中引用该变量。注意 Cookie 管理器可自动管理 Set-Cookie,若接口用 Cookie 可依赖自动携带。
12. 阶梯增压(阶梯增加并发)在 JMeter 里怎么做?
答:可用"Stepping Thread Group"插件(需安装插件),或通过多个线程组 + 调度器:例如第 1 个线程组 0~60s 跑 50 线程,第 2 个 60~120s 再增加 50 线程。也可用 JSR223 或 BeanShell 配合定时器动态改线程数(较复杂)。目的与 Locust 的 LoadShape 类似:观察系统随负载升高的表现。
13. 为什么 JMeter 单机线程数开很大时,结果反而变差或报错?
答:① 单机 CPU、内存、网络、文件句柄有限,线程过多会导致上下文切换、内存占用高、端口耗尽。② 每个线程默认一个连接,高并发时需要调大系统参数(如 Linux 下 net.ipv4.ip_local_port_range、ulimit)。③ 建议用分布式或多台 JMeter 分散负载,或降低单机线程数、增加 Ramp-Up。
14. JMeter 脚本如何在 CI 里无界面运行?常用参数有哪些?
答:用命令行:jmeter -n -t script.jmx -l result.jtl -e -o report/。-n 无 GUI,-t 指定脚本,-l 结果文件,-e -o 生成 HTML 报告。可加 -J 传变量(如 -Jthreads=100)、-j 指定日志。CI 里执行该命令,根据退出码或解析 result.jtl/HTML 判断通过与否。
15. 如何用 JMeter 模拟"同一用户顺序执行多个接口"(业务流)?
答:在同一线程组内按顺序添加多个 HTTP 请求;用关联把前面响应的数据传给后面;需要思考时间时在请求间加定时器。一个线程代表一个用户,迭代次数大于 1 时该用户会重复执行整条业务流。若需不同用户执行不同流程,可用多个线程组或 If 控制器 + 随机/比例控制。
16. JMeter 和 Locust 的优缺点对比?选型时怎么考虑?
答:JMeter:图形化配置、协议支持多、插件丰富、分布式成熟,但资源占用较大、复杂逻辑需 BeanShell/Groovy。Locust:用 Python 写场景、代码灵活、易扩展、资源占用相对小,但协议和生态不如 JMeter 全。选型看:团队语言(Java vs Python)、是否需要 GUI、协议需求(如 JMeter 对 JDBC/FTP 支持好)、是否要写复杂逻辑(Locust 代码更直观)。
17. 参数化时"多线程共享 CSV"和"每线程独立取 CSV 行"如何配置?
答:CSV 数据配置中有"是否共享":共享则所有线程共用同一份迭代(适合少量数据重复用);不共享则每个线程按顺序取不同行(适合每用户一个账号)。配合"遇到文件尾"时的行为:继续循环、从头开始、停止线程等,可控制数据用尽时的策略。
18. 如何减少 JMeter 结果文件(jtl/csv)体积,又保留分析所需信息?
答:① 在监听器或 jmeter.properties 里关闭不需要的字段(如不保存响应体、只保存时间戳/耗时/状态码/标签)。② 用聚合报告等只写汇总数据,不写每条请求。③ 采样:只记录 1/N 的请求。④ 压测结束后用后处理脚本从完整 jtl 做二次聚合。平衡"排查问题需要明细"和"长时间压测的磁盘与 I/O"。
19. JMeter 里如何做"条件判断"(如根据上一步是否成功决定是否执行下一步)?
答:用 If 控制器:条件写成 ${__jexl3(条件)} 或 KaTeX parse error: Expected group after '_' at position 2: {_̲_groovy(条件)},如 ...{JMeterThread.last_sample_ok}` 表示上一步是否成功。在 If 控制器下放需要条件执行的取样器。也可用断言失败后配合流程控制(如 Stop 线程),实现"失败则停止该用户后续步骤"。
20. 压测时 JMeter 报 "Address already in use" 或连接被重置,可能原因与解决?
答:原因:① 短时间内大量建连又关闭,TIME_WAIT 占满本地端口,导致"地址已使用"。② 服务端或中间件连接数/限流触发断开。解决:① 勾选 HTTP 请求的"使用 KeepAlive",复用连接。② 调大系统可用端口范围、缩短 TIME_WAIT(需谨慎)。③ 降低单机并发或做分布式,分散源端口与连接数。
Python 常用库用法与面试题
以下为性能/接口自动化中常用的 Python 标准库与第三方库:用法要点 + 基础面试题,便于在脚本与框架中正确使用并应对面试。
一、requests
用法要点
- 发请求:
requests.get(url, params=..., headers=...)、requests.post(url, json=..., data=...)。 - 会话复用:
s = requests.Session(),再s.get()/s.post(),自动带 Cookie、复用连接。 - 超时:
requests.get(url, timeout=5)(连接+读超时),或timeout=(3, 10)分别指定连接/读超时。 - 响应:
r.status_code、r.json()、r.text、r.headers;r.raise_for_status()在 4xx/5xx 时抛异常。
python
# 示例:带鉴权与超时的 GET
r = requests.get("https://api.example.com/data", headers={"Authorization": "Bearer " + token}, timeout=10)
data = r.json() if r.ok else None
面试题
1. requests 里 Session 和直接 get/post 有什么区别?性能测试里为什么要用 Session?
答:Session 复用 TCP 连接、自动管理 Cookie,多次请求同一 host 时更省连接、更接近真实浏览器。性能测试里用 Session 可减少建连开销、提高单机并发能力,且避免端口耗尽(TIME_WAIT 过多)。
2. 如何用 requests 设置超时?连接超时和读超时分别指什么?
答:timeout=5 表示连接+读总共 5 秒;timeout=(3, 10) 表示连接 3 秒、读 10 秒。连接超时是建立 TCP 的时间,读超时是等待首字节后读取 body 的时间。压测时合理设置可避免请求长时间挂起。
二、json
用法要点
- 序列化:
json.dumps(obj)转成字符串,ensure_ascii=False不转义中文;indent=2美化。 - 反序列化:
json.loads(s)字符串转 Python 对象。 - 文件:
json.dump(obj, f)、obj = json.load(f)。 - 取深层值:可先
loads再逐层[],或配合 jmespath/jsonpath 等库。
python
import json
d = {"a": 1, "b": [2, 3]}
s = json.dumps(d, ensure_ascii=False, indent=2)
obj = json.loads(s)
# 从响应取字段
data = json.loads(response.text)
id = data["result"]["id"]
面试题
3. json.dumps 时遇到 datetime 或自定义对象报错怎么办?
答:默认只支持基本类型。可写 default:json.dumps(obj, default=lambda x: x.isoformat() if hasattr(x, 'isoformat') else str(x)),或先把对象转成 dict。性能测试里若要把含日期的结果写报告,可先转成字符串或时间戳再序列化。
4. JSON 和 Python dict 在类型上有什么对应关系?
答:JSON 的 object→dict,array→list,string→str,number→int/float,true/false→True/False,null→None。Python 的 set、tuple、自定义类不能直接 dumps,需先转成上述类型或提供 default。
三、os 与 pathlib
用法要点
- 环境变量:
os.environ.get("KEY", "default")、os.environ["KEY"]。 - 路径:
os.path.join(a, b)、os.path.abspath(path)、os.path.exists(path)、os.path.dirname(__file__)。 - pathlib(推荐):
Path(__file__).resolve().parent、Path("a") / "b" / "c"、p.exists()、p.read_text()。
python
import os
from pathlib import Path
config_dir = Path(__file__).resolve().parent / "config"
env = os.environ.get("ENV", "test")
面试题
5. 为什么推荐用 pathlib 而不是 os.path 拼接路径?
答:pathlib 面向对象、支持 / 拼接、跨平台一致,且可直接 .read_text()、.write_text(),代码更清晰。性能脚本里读配置文件、写报告路径时用 Path 可读性更好,也不易搞错正反斜杠。
6. 如何用 Python 获取当前脚本所在目录?
答:os.path.dirname(os.path.abspath(__file__)) 或 Path(__file__).resolve().parent。框架里常据此找 config、data 等相对路径,保证无论从哪执行都能定位到项目根或配置目录。
四、re(正则)
用法要点
- 匹配:
re.search(r"\d+", s)返回 Match 或 None;re.match(r"^...", s)从开头匹配。 - 查找全部:
re.findall(r"\d+", s)返回列表。 - 替换:
re.sub(r"\d+", "X", s)。 - 编译复用:
pat = re.compile(r"..."),再pat.search(s),多次用时更高效。
python
import re
# 从响应里提数字
ids = re.findall(r'"id":\s*(\d+)', response.text)
# 替换
s = re.sub(r"\s+", " ", s)
面试题
7. re.search 和 re.match 有什么区别?
答:search 在字符串任意位置找第一个匹配;match 只从字符串开头匹配。若要从整串中找某模式用 search;若要求"整串符合某格式"可用 match 或 search 后检查 match 是否从 0 开始。
8. 正则里贪婪和非贪婪怎么表示?
答:*、+ 默认贪婪(尽量多匹配),加 ? 为非贪婪:如 .*? 尽量少匹配。从 HTML/JSON 里抠一段内容时常用非贪婪避免跨标签/跨括号。
五、datetime
用法要点
- 当前时间:
datetime.datetime.now()、datetime.date.today()。 - 构造:
datetime.datetime(2024, 1, 1, 12, 0, 0)。 - 格式化:
dt.strftime("%Y-%m-%d %H:%M:%S");解析:datetime.datetime.strptime(s, "%Y-%m-%d")。 - 时间戳:
dt.timestamp()、datetime.datetime.fromtimestamp(ts)。
python
from datetime import datetime, timedelta
now = datetime.now()
s = now.strftime("%Y-%m-%d %H:%M:%S")
dt = datetime.strptime("2024-01-01", "%Y-%m-%d")
tomorrow = now + timedelta(days=1)
面试题
9. 如何用 Python 生成"当前时间戳"(秒级/毫秒级)?
答:秒级 int(datetime.now().timestamp()) 或 int(time.time());毫秒级 int(time.time() * 1000)。接口签名、压测报告命名、唯一 ID 等常用时间戳。
10. 为什么说"时间敏感"的用例要 mock 或固定时间?
答:断言里若用"当前时间"或"今天",每次运行结果不同,用例不稳定。做法:用固定时间、或 pytest 的 freezegun 等冻结时间,保证可重复执行;性能脚本里打日志用真实时间即可。
六、logging
用法要点
- 级别:DEBUG、INFO、WARNING、ERROR;
logging.basicConfig(level=logging.INFO)。 - 输出格式:
format="%(asctime)s [%(levelname)s] %(message)s"。 - 同时写文件:
logging.FileHandler("app.log")、addHandler。 - 按模块区分:
logger = logging.getLogger(__name__),不同模块可设不同 level。
python
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
logger.info("request %s", url)
logger.exception("failed") # 带异常栈
面试题
11. logging 和 print 在测试/压测脚本里该怎么选?
答:logging 可分级、可输出到文件、可带时间与模块名,便于排查和归档;print 只到控制台、无法按级别过滤。正式脚本和框架应用 logging,临时调试可用 print。压测时大量请求用 INFO 会打很多日志,可适当提高级别或对请求日志做采样。
12. 如何让 logging 同时输出到控制台和文件?
答:先 logger = logging.getLogger(...),再 logger.addHandler(logging.StreamHandler()) 和 logger.addHandler(logging.FileHandler("app.log")),并设置 logger.setLevel 和各 handler 的 formatter。这样控制台和文件都能看到同一套日志。
七、subprocess
用法要点
- 执行命令:
subprocess.run(["ls", "-l"], capture_output=True, text=True),返回 CompletedProcess,有 returncode、stdout、stderr。 - 超时:
subprocess.run(..., timeout=10),超时抛 TimeoutExpired。 - 不等待:
subprocess.Popen(...),再poll()或wait()。
python
import subprocess
r = subprocess.run(["python", "-c", "print(1)"], capture_output=True, text=True, timeout=5)
print(r.returncode, r.stdout)
面试题
13. subprocess.run 里 shell=True 有什么风险?什么时候必须用?
答:shell=True 会经过 shell 解析,命令若含用户输入易有注入风险;且依赖当前 shell 环境。能不用就不用,用列表传参 ["cmd", "arg1"]。必须用的情况:需要管道、通配符等 shell 特性时,且不要拼接未校验的外部输入。
14. 如何在 Python 里执行一条命令并拿到标准输出?
答:subprocess.run(["cmd", "arg"], capture_output=True, text=True).stdout,或 subprocess.check_output(["cmd", "arg"], text=True)。性能脚本里调系统命令、启动服务、跑 JMeter 等常用 subprocess。
八、yaml
用法要点
- 安装:
pip install pyyaml。 - 读:
yaml.safe_load(open("config.yaml"))或yaml.safe_load(s),得到 dict/list。 - 写:
yaml.dump(data, f, allow_unicode=True)。 - 安全:用
safe_load不要用load,避免反序列化执行任意代码。
python
import yaml
with open("config.yaml") as f:
cfg = yaml.safe_load(f)
base_url = cfg["base_url"]
面试题
15. YAML 和 JSON 在配置文件里各有什么优劣?
答:YAML 可写注释、支持多行、层次更易读,适合人工维护的配置;JSON 无注释、严格格式、很多接口和前端直接用。性能/接口框架里环境配置常用 YAML,接口请求体或 API 约定常用 JSON。
16. 为什么 yaml 要用 safe_load 而不是 load?
答:yaml.load 可反序列化任意 Python 类,若配置来自不可信来源可能执行恶意代码。safe_load 只允许基本类型与安全结构,适合配置与数据文件。从文件或环境读配置时应用 safe_load。
九、csv
用法要点
- 读:
csv.reader(f)按行迭代;csv.DictReader(f)每行是 dict,键为首行。 - 写:
csv.writer(f)的writerow、writerows;csv.DictWriter(f, fieldnames=[...])。 - 编码:打开文件时指定
encoding="utf-8",避免中文乱码。
python
import csv
with open("data.csv", encoding="utf-8") as f:
for row in csv.DictReader(f):
print(row["name"], row["id"])
面试题
17. 数据驱动时用 CSV 和用 JSON 各适合什么场景?
答:CSV 适合表格式、多行同结构、给非开发看或 Excel 编辑;读用 DictReader 即可。JSON 适合嵌套、多类型、和接口请求体一致;框架里接口定义、复杂用例数据常用 JSON。性能测试里账号列表、参数组合常用 CSV。
18. 读 CSV 时如何避免首行被当成数据?
答:用 csv.DictReader 时首行自动当表头;用 csv.reader 时第一行可 next(reader) 跳过,或判断是否表头再处理。写 CSV 时先写一行表头再写数据。
十、pytest(接口/性能脚本中常用)
用法要点
- 用例:以
test_开头的函数;断言用assert。 - fixture:
@pytest.fixture提供依赖,scope="session"可只执行一次。 - 参数化:
@pytest.mark.parametrize("a,b,expected", [(1,2,3), (2,3,5)])。 - 运行:
pytest -v、pytest -k "name"、pytest --tb=short。
python
import pytest
@pytest.fixture(scope="session")
def client():
return requests.Session()
def test_api(client):
r = client.get("/api/status")
assert r.status_code == 200
面试题
19. pytest 的 fixture 的 scope 有哪几种?性能测试里 session 和 function 怎么选?
答:function(默认,每个用例一次)、class、module、package、session。性能测试里若"只起一次环境、多用例复用"(如同一 Session、同一 token),用 session 或 module 减少重复;若每用例要独立环境或数据,用 function。
20. pytest 和 unittest 的主要区别?
答:pytest 无需继承、assert 直接写、fixture 灵活、参数化简单、插件多;unittest 需继承 TestCase、用 assertXxx。写接口或性能相关用例时 pytest 更简洁,也便于和 Allure、CI 集成。
十一、其他常用库速览与面试题
typing
- 用法:类型注解
def f(x: int) -> str:、List[str]、Dict[str, Any]、Optional[str],便于阅读和 IDE 提示。 - 面试题 21:类型注解会提升运行性能吗?答:不会,运行时默认不检查,主要方便静态检查(如 mypy)和可读性;对性能测试脚本来说可选,复杂框架建议写上便于维护。
collections
- 用法:
defaultdict(list)免写 key 是否存在判断;Counter(list)统计频次;deque双端队列。 - 面试题 22 :性能脚本里用 defaultdict 有什么好处?答:避免多次
if key not in d: d[key]=[],代码更短;统计接口名、状态码分布时用 Counter 很合适。
random
- 用法:
random.randint(a, b)、random.choice(list)、random.shuffle(list);random.seed(42)可复现。 - 面试题 23:为什么测试脚本里有时要 random.seed?答:用例或数据若依赖随机数,不设 seed 每次结果不同,用例不稳定;设固定 seed 后随机序列可复现,便于回归和排查。
hashlib
- 用法:
hashlib.md5(s.encode()).hexdigest()、hashlib.sha256(...).hexdigest(),用于签名、校验文件。 - 面试题 24:接口签名用 MD5 和 SHA256 在测试里要注意什么?答:按接口文档实现相同算法与拼接顺序;编码统一(如 UTF-8);测试环境密钥用配置或环境变量,不写死。MD5 已不推荐用于安全场景,但很多老接口仍用,测试只需按约定实现。
threading / concurrent.futures
- 用法:
threading.Thread(target=f, args=(...))、ThreadPoolExecutor(max_workers=5).submit(f, arg)、as_completed(futures)。 - 面试题 25:做简单并发请求时,多线程和多进程怎么选?答:I/O 多(如发 HTTP)用多线程或 asyncio 即可;CPU 多(如大量计算)用多进程避免 GIL。性能压测一般用 Locust/JMeter 等专门工具,脚本里小规模并发可用 ThreadPoolExecutor 或 asyncio。
configparser
- 用法:读 ini:
config = configparser.ConfigParser()、config.read("a.ini")、config.get("section", "key")。 - 面试题 26:ini 和 yaml/json 做配置各适合什么场景?答:ini 适合简单 key=value、分 section;yaml/json 适合层级多、结构复杂。性能框架里若已有 ini 习惯可继续用;新项目更推荐 yaml 或 json 便于嵌套。
文档版本 :1.0.0
最后更新 :2024-01-01
维护者:性能测试团队