性能测试以及面试题

企业级性能测试框架完整指南

本文档全面讲解性能测试框架的架构设计、核心模块、使用方法和最佳实践。
代码讲解约定 :涉及配置或核心代码时,会从三个角度说明------为什么这样写在框架里该怎么用实际意义是什么,尽量覆盖所有相关代码,便于理解和扩展。

📑 目录

  • [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 核心特性

✨ 企业级特性
  1. 模块化架构

    • 清晰的模块划分
    • 松耦合设计
    • 易于扩展和维护
  2. 配置管理

    • 统一的配置管理
    • 支持多环境配置
    • 灵活的配置覆盖
  3. 日志系统

    • 多级别日志记录
    • 日志轮转机制
    • 文件和控制台双重输出
  4. 断言机制

    • 丰富的断言方法
    • JSON 路径支持
    • 详细的错误信息
  5. 性能监控

    • 实时性能指标收集
    • 性能阈值告警
    • 详细的统计报告
  6. 报告生成

    • 多格式报告(HTML/CSV/JSON)
    • 自动报告归档
    • 可视化数据展示
🚀 技术特性
  1. 基于 Locust

    • 分布式测试支持
    • 高并发性能
    • 实时监控界面
  2. Python 生态

    • 纯 Python 实现
    • 易于集成和扩展
    • 丰富的第三方库支持
  3. 数据驱动

    • 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 核心功能
  1. 配置文件加载

    • 动态加载 config/config.py
    • 支持配置文件路径自定义
    • 配置文件不存在时抛出异常
  2. 配置访问

    • get(key, default):获取配置值
    • get_base_url():获取基础 URL(便捷方法)
  3. 单例保证

    • 确保全局唯一实例
    • 避免重复加载配置
    • 节省内存资源
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_URLWAIT_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 核心功能
  1. 多级别日志

    • DEBUG:详细信息
    • INFO:一般信息
    • WARNING:警告信息
    • ERROR:错误信息
    • CRITICAL:严重错误
  2. 日志轮转

    • 文件大小限制(默认 10MB)
    • 自动备份(默认保留 5 个备份)
    • 防止日志文件过大
  3. 双重输出

    • 控制台输出(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 大小时:

  1. 当前日志文件重命名为 performance_test.log.1
  2. 创建新的 performance_test.log 文件
  3. 旧的备份文件依次重命名(.1.2.3 → ...)
  4. 超过 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 核心功能
  1. 自动配置

    • 自动从配置文件读取 BASE_URL
    • 自动设置 wait_time(等待时间)
  2. 生命周期管理

    • on_start():用户启动时执行
    • on_stop():用户停止时执行
  3. 请求头设置

    • 默认请求头配置
    • 支持自定义请求头
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 = 1WAIT_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 核心功能
  1. 状态码断言

    • 验证 HTTP 状态码是否匹配
  2. JSON 断言

    • 验证 JSON 键是否存在(支持点号分隔路径)
    • 验证 JSON 值是否匹配
  3. 文本断言

    • 验证响应文本是否包含指定内容
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 核心功能
  1. JSON 文件加载

    • data/ 目录加载 JSON 文件
    • 自动创建目录(如果不存在)
    • 错误处理和日志记录
  2. 配置文件数据

    • config/config.py 读取测试数据
    • 支持列表和字典类型数据
  3. 便捷方法

    • 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 核心功能
  1. 性能指标收集

    • 请求总数、成功数、失败数
    • 响应时间统计(平均/最小/最大/P95/P99)
    • 错误信息收集
  2. 性能阈值监控

    • 响应时间阈值告警
    • 错误率阈值告警
    • 成功率阈值告警
  3. 性能报告生成

    • 测试结束时自动生成报告
    • 详细的统计信息输出
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 核心功能
  1. HTML 报告

    • 可视化测试结果
    • 图表展示
    • 便于查看和分析
  2. CSV 报告

    • 表格格式数据
    • 便于 Excel 分析
    • 支持批量处理
  3. 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 测试用例结构

每个测试用例文件都应该:

  1. 继承 BaseUser
  2. 使用 @task 装饰器定义测试任务
  3. 使用 catch_response=True 捕获响应
  4. 使用断言模块验证响应
  5. 调用 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 测试用例设计

  1. 单一职责原则

    • 每个测试方法只测试一个功能点
    • 避免在一个方法中测试多个功能
  2. 清晰的命名

    • 使用描述性的方法名
    • 说明测试的目的和预期结果
  3. 完整的断言

    • 不仅验证状态码,还要验证响应内容
    • 使用合适的断言方法
  4. 错误处理

    • 捕获和处理异常
    • 记录详细的错误信息

8.2 性能测试策略

  1. 渐进式负载

    • 从小负载开始
    • 逐步增加负载
    • 观察系统响应
  2. 测试场景选择

    • 根据测试目的选择合适的场景
    • 快速验证使用 quick 场景
    • 压力测试使用 stress 场景
  3. 监控关键指标

    • 响应时间
    • 错误率
    • 吞吐量
  4. 测试环境隔离

    • 使用独立的测试环境
    • 避免影响生产环境

8.3 数据管理

  1. 测试数据分离

    • 使用独立的测试数据
    • 避免使用生产数据
  2. 数据驱动测试

    • 使用 JSON 文件管理测试数据
    • 支持数据轮询和随机选择
  3. 数据清理

    • 测试后清理测试数据
    • 保持测试环境干净

8.4 日志管理

  1. 合适的日志级别

    • 开发环境使用 DEBUG
    • 生产环境使用 INFO 或 WARNING
  2. 结构化日志

    • 使用统一的日志格式
    • 包含足够的上下文信息
  3. 日志轮转

    • 配置合适的文件大小限制
    • 保留足够的备份数量

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'

解决方案

  1. 检查项目根目录是否在 Python 路径中
  2. 确认 __init__.py 文件存在
  3. 检查模块名称是否正确
问题2:配置文件不存在

症状FileNotFoundError: 配置文件不存在

解决方案

  1. 检查 config/config.py 文件是否存在
  2. 确认文件路径正确
  3. 检查文件权限
问题3:测试执行失败率高

可能原因

  • 目标服务不可用
  • 网络连接问题
  • 请求超时设置过短
  • 并发数过高

解决方案

  1. 检查目标服务状态
  2. 测试网络连接
  3. 增加超时时间
  4. 降低并发数
问题4:内存占用过高

可能原因

  • 响应数据过大
  • 日志记录过多
  • 统计数据积累

解决方案

  1. 限制响应数据大小
  2. 调整日志级别
  3. 定期清理统计数据

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 测试脚本优化

  1. 减少不必要的操作

    • 避免在每个请求中重复初始化
    • 使用 on_start() 进行一次性初始化
  2. 优化断言逻辑

    • 只验证必要的字段
    • 避免深度嵌套的 JSON 解析
  3. 合理使用任务权重

    • 根据实际业务场景设置权重
    • 避免不必要的高频请求

12.2 配置优化

  1. 调整等待时间

    python 复制代码
    WAIT_TIME_MIN = 0.5  # 减少最小等待时间
    WAIT_TIME_MAX = 1.0  # 减少最大等待时间
  2. 优化日志配置

    python 复制代码
    LOG_LEVEL = "WARNING"  # 生产环境使用更高级别
    LOG_FILE_MAX_BYTES = 5242880  # 减少文件大小
  3. 调整超时设置

    python 复制代码
    HTTP_TIMEOUT = 10  # 减少超时时间
    HTTP_CONNECT_TIMEOUT = 5

12.3 系统优化

  1. 使用分布式测试

    • 运行多个 Worker 节点
    • 分散负载
  2. 优化网络环境

    • 使用高速网络
    • 减少网络延迟
  3. 监控系统资源

    • 监控 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_coder.json()r.textr.headersr.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 或自定义对象报错怎么办?

答:默认只支持基本类型。可写 defaultjson.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().parentPath("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)writerowwriterowscsv.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 -vpytest -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
维护者:性能测试团队

相关推荐
52Hz1181 小时前
力扣394.字符串解码、739.每日温度、84.柱状图中最大的矩形
python·算法·leetcode
XW01059991 小时前
4-10大公约数和最小公倍数
python·gcd·lcm
Cxiaomu2 小时前
Python 文件解析: Excel / Word / PDF 的解析、处理、预览与下载
python·word·excel
啊阿狸不会拉杆2 小时前
《计算机视觉:模型、学习和推理》第 10 章-图模型
人工智能·python·学习·机器学习·计算机视觉·图模型
测试老哥2 小时前
如何使用Postman做接口测试?
自动化测试·软件测试·python·测试工具·测试用例·接口测试·postman
七夜zippoe2 小时前
安全测试实战:OWASP Top 10全面防护指南
python·sql·xss·安全测试·安全框架·核心漏洞
Loo国昌2 小时前
【AI应用开发实战】Guardrail风险控制中间件:Agent系统的安全防线
人工智能·python·安全·自然语言处理·中间件·prompt
苡~2 小时前
【openclaw+claude系列02】全景拆解——手机、电脑、AI 三者如何协同工作
java·人工智能·python·智能手机·电脑·ai编程
chao_7892 小时前
构建start_app.sh,实现快速启动项目
python·bash·终端·前后端