很多测试同学在入门自动化时,都会遇到一个很奇怪的瓶颈:单个功能写脚本完全没有问题,但只要功能一多、用例一多,脚本就开始失控。今天这篇教程要讲的内容,就是打破这个瓶颈的两个基础概念——类与模块。它们不是程序员专属的“高深理论”,而是测试代码能否工程化的分水岭。
我的判断是:类和模块解决的不是“能不能写出来”的问题,而是“能不能长期维护”的问题。一个人手写的测试脚本,如果没有合理的类设计和模块拆分,十个用例以内还能忍,一百个用例以上基本就是灾难。
这篇文章会从软件测试的实际场景出发,先讲清楚类与模块到底解决什么痛点,再用三个递进的示例,从“函数脚本”进化到“类封装”,再进化到“模块化项目”,最后用一个可以本地跑通的小型接口测试项目收尾。读完你会明白:同样一段测试逻辑,用什么样的代码组织方式,决定了日后维护的成本。
1. 这篇文章真正要解决的问题
先看三个测试同学最常遇到的真实场景。
第一个场景是脚本越来越长。刚开始学自动化时,很多人喜欢把所有步骤写在一个文件里,登录、点击、断言、清理数据全部顺序往下排。功能少的时候没有问题,但一旦需要验证多条用例,脚本就会膨胀到几百行甚至上千行,每次定位失败原因都要上下翻找。
第二个场景是复用基本靠复制粘贴。今天测试登录,明天测试下单,后天测试支付,每个脚本里都有一段“初始化浏览器/初始化请求会话”“输入用户名密码”“点击登录按钮”的代码。多数人第一反应是复制上一份脚本,然后改几个参数。这样看起来很快,但隐患在于:一旦登录逻辑发生变化,比如新增了一个登录前的验证码输入步骤,你就要把所有复制过这段代码的文件全部找出来,逐个修改,漏掉一个就是线上事故。
第三个场景是一改全崩。因为代码之间没有清晰的边界,所有函数都可以互相调用,所有变量都暴露在同一个全局作用域里。一个简单的功能变更,可能牵连到好几个原本不相关的测试脚本。“修复一个bug,引入两个新bug”在测试代码里同样常见。
这篇文章要解决的问题,就是上面三种现象背后的根因:测试代码缺乏结构。类能帮你把数据和操作数据的动作封装在一起,模块能帮你把不同职责的代码拆分到不同文件中,两者结合起来,测试脚本才能从“一次性脚本”变成“可持续维护的自动化资产”。
什么样的读者最应该读这篇文章?如果你是零基础准备入行软件测试,或者已经会用 Python 写简单脚本、但写出来的代码自己都嫌乱,又或者正在准备测试开发相关岗位的笔面试,那这篇文章就是给你准备的。类与模块是 Python 面试中出现频率极高的话题,也是后续学习 pytest、Selenium、Requests 等测试框架绕不开的基础。
2. 类与模块:测试代码工程化的起点
2.1 类是什么:把数据和行为绑在一起的模板
用一句话概括:类就是把数据和对数据的操作打包在一起的一种代码组织方式。
在这个定义里,“数据”在 Python 中叫属性,“对数据的操作”叫方法。比如你测试一个登录接口,登录功能涉及的数据是用户名、密码、服务器地址,涉及的操作是发送登录请求、判断登录是否成功。把这一组数据和操作放在一起,就得到一个“登录客户端”类。
不过要特别说明,类中的属性和方法都是“模板级别”的,它不会在定义类的时候真正执行,而是等你创建对象(实例化)之后才生效。这个设计背后的原因是:你希望同一套登录逻辑,可以被多个测试用例复用,每一个测试用例只需要传入不同的用户名和密码,就能得到一个独立的登录实例,互不干扰。
新手最容易误解的一点是:类是一种语法,而不是一种必须的规则。小脚本里不用类也没问题,就像你家里东西少,不需要定制收纳柜一样。但当测试用例变多、被测系统变复杂,没有类的封装,你的脚本会退化成一段段彼此纠缠的顺序代码。类是测试脚本从“能用”走向“好维护”的第一步。
2.2 模块是什么:把相关代码放进一个文件里
模块的定义比类更简单:一个.py文件就是一个模块,它可以包含变量、函数、类,也可以包含一段可以执行的逻辑。当你需要复用另一个文件里的代码时,用import导入即可。
模块解决的痛点和类不同。类解决的是“同一组逻辑如何复用”,模块解决的是“不同职责的代码如何拆分”。打个比方,类像是车间里的一台加工设备,模块像是整个车间里划分出的不同功能区:原料区、加工区、质检区。你可以把原料区和加工区放在一个房间里,但那样一乱起来就很难管理。
在软件测试项目中,合理的模块拆分通常遵循一个直观原则:和界面打交道的代码放一个模块,和断言计算打交道的代码放一个模块,纯测试用例放一个模块。这样任何一个模块发生变化,其他模块都不会受牵连。
2.3 类和模块的分工对比
| 维度 | 类 | 模块 |
|---|---|---|
| 粒度 | 较小,描述一组相关数据与操作 | 较大,包含多个类、函数和变量 |
| 解决的问题 | 代码复用、封装、状态管理 | 代码组织、职责分离、命名空间 |
| 语法关键词 | class | import、from ... import |
| 在测试项目中的角色 | 封装页面对象、接口客户端、断言工具 | 组织配置文件、工具库、用例集 |
| 类比 | 一台加工设备 | 一个车间功能区 |
类与模块从来不是二选一的关系。一个良好的测试项目中,模块用来划分大边界,类用来在每一个边界内部做更细粒度的封装。两者一起用,代码才会既有清晰的分层,又有足够的复用性。
3. 环境准备与基础约定
在开始写代码之前,先把环境准备好。本文所有示例都以 Python 3.x 为演示语言,具体版本请以你实际安装的版本为准。之所以选 Python,是因为它已经是当前软件测试自动化领域最主流的语言之一,无论是接口测试、Web UI 测试还是测试脚本工具,Python 都有非常成熟的生态。
建议你在本机安装一个虚拟环境,避免项目之间的依赖相互冲突。创建虚拟环境的命令如下:
# 创建名为 venv 的虚拟环境 python3 -m venv venv # 激活虚拟环境(Windows) venv\Scripts\activate # 激活虚拟环境(macOS / Linux) source venv/bin/activate在后面的综合项目实战中,我们需要安装 Flask、Requests 和 pytest 这三个库。Flask 用来启动一个本地 Mock 被测服务,Requests 用来发送 HTTP 请求,pytest 用来收集和执行测试用例。安装命令如下,版本请以实际安装时的最新稳定版为准:
pip install flask requests pytest除 IDE 外,你不需要再安装其他工具。IDE 方面,PyCharm Community Edition 和 VS Code 都足够,关键是要能识别 Python 解释器。如果你用 VS Code,建议在项目根目录下创建.vscode/settings.json,指定当前项目的解释器路径,避免打开项目后使用错误的全局环境。
4. 第一步:从混乱的函数脚本到类
4.1 反例:一个典型的“面条式”测试脚本
假设我们要测试一个用户登录接口。没有使用类之前,很多人写出来的脚本长这样:
# 文件路径:demo_no_class.py # 注意:这个示例刻意写得重复,用来展示不使用类的痛点 import requests def test_user_login_success(): url = "http://127.0.0.1:5000/api/login" payload = {"username": "admin", "password": "123456"} response = requests.post(url, json=payload) data = response.json() if data.get("code") == 200: print("登录成功") else: print("登录失败") def test_user_login_wrong_password(): url = "http://127.0.0.1:5000/api/login" payload = {"username": "admin", "password": "wrong"} response = requests.post(url, json=payload) data = response.json() if data.get("code") == 401: print("密码错误,返回符合预期") else: print("密码错误场景校验失败") test_user_login_success() test_user_login_wrong_password()这段代码能运行,但问题很明显:url、payload、response.json()这些操作在每一个用例里都重复出现。如果接口地址变了,你要改两处;如果登录接口的返回结构从code变成了status_code,你还要改两处。这还只是两个用例,如果是二十个用例呢?
这段脚本真正的缺陷不是代码长度,而是“变量和操作被绑死在每一个函数里”,你没有任何一个地方可以统一修改公共逻辑。这个问题的本质,就是缺少类的封装。
4.2 正例:用类封装登录操作
我们引入一个LoginClient类,把登录接口的地址、请求构造、结果解析都封装起来。测试用例只负责传入参数和判断结果,不再关心网络请求的细节:
# 文件路径:demo_with_class.py import requests class LoginClient: """封装登录接口的公共逻辑""" def __init__(self, base_url): self.base_url = base_url self.session = requests.Session() def login(self, username, password): url = f"{self.base_url}/api/login" payload = {"username": username, "password": password} response = self.session.post(url, json=payload) return response.json() def is_login_success(self, data): return data.get("code") == 200 if __name__ == "__main__": client = LoginClient("http://127.0.0.1:5000") result = client.login("admin", "123456") print("登录成功" if client.is_login_success(result) else "登录失败")这段代码的价值体现在三个地方。第一,base_url只初始化一次,后续所有用例共用同一个客户端实例;第二,登录请求被封装成login方法,调用方不需要关心 URL 拼接和请求头构造;第三,如果登录逻辑变化,只需要修改LoginClient内部代码,所有调用它的测试用例不用动。
很多新手看这段代码会觉得“也没少几行啊”,那是因为示例规模太小。请想象一下:当你的测试代码里有页面元素定位、数据库连接、文件读写、日志记录、断言工具时,类封装带来的维护收益是指数级增长的。这里真正容易踩坑的地方是__init__方法里的self,如果不写self.base_url = base_url,后面的方法就拿不到这个变量。
4.3 面试高频:self 到底是什么
面试时,面试官经常问“Python 类中self的作用是什么”。从测试代码的实际运行角度看,self指向当前实例对象本身。你创建client = LoginClient(...)时,Python 会自动把client作为第一个参数传给__init__和后续所有实例方法,所以你在方法里写的self.base_url本质上是“当前这个实例自己的 base_url”。
这个概念在测试代码中有非常大的实际意义:多个测试用例可以各自创建不同的LoginClient实例,分别连接不同的环境地址,互不干扰。这就是为什么类封装比全局变量更安全——一个实例改了自己的状态,不会影响另一个实例。
5. 第二步:模块拆分与导入
5.1 一个文件装不下所有代码
类把同一组逻辑封装好了,但如果你把断言工具、页面封装、测试用例全部塞进同一个文件,当项目变大后,这个文件依然会变得难以维护。模块化就是把不同职责的代码放到不同的.py文件里,通过import把它们连接起来。
以一个简单的登录测试为例,我们可以拆成三个模块:
assert_utils.py:放通用断言方法,和具体业务无关。login_page.py:放登录接口的封装,属于业务操作层。test_login.py:放具体测试用例,只关心测试数据和预期结果。
5.2 断言工具模块
# 文件路径:assert_utils.py """通用的断言工具模块""" class AssertUtils: @staticmethod def assert_equals(actual, expected, message=""): assert actual == expected, f"{message},实际值: {actual},期望值: {expected}"这里的AssertUtils类里只有一个静态方法assert_equals。静态方法意味着不需要创建实例就能直接调用,它的作用是把“断言相等”这个动作统一收口。将来如果公司规范要求断言失败必须附带日志,只需要改这里一处。
5.3 接口封装模块
# 文件路径:login_page.py """登录接口封装模块,隔离底层请求细节""" import requests class LoginPage: def __init__(self, base_url): self.base_url = base_url self.session = requests.Session() def login(self, username, password): url = f"{self.base_url}/api/login" response = self.session.post(url, json={ "username": username, "password": password, }) return response.json() def get_error_message(self, data): return data.get("message", "")这个模块的名字叫LoginPage,灵感来自测试领域非常经典的“页面对象模型”(Page Object Model)。即使你测的是接口不是网页,这种“把一个业务入口封装成一个对象”的思路也完全适用。它的好处是测试用例层根本不需要知道请求是用requests发的,还是用其他库发的。
5.4 测试用例模块
# 文件路径:test_login.py """测试用例模块,只关注测试数据和预期结果""" from login_page import LoginPage from assert_utils import AssertUtils def test_login_success(): page = LoginPage("http://127.0.0.1:5000") data = page.login("admin", "123456") AssertUtils.assert_equals(data.get("code"), 200, "登录成功场景") def test_login_wrong_password(): page = LoginPage("http://127.0.0.1:5000") data = page.login("admin", "wrong") AssertUtils.assert_equals(data.get("code"), 401, "密码错误场景")在测试用例模块中,只出现了“创建页面对象”“调用业务方法”“断言结果”三类动作。这就是模块化想达到的效果:测试用例阅读起来像一份产品验收文档,而不是一段网络请求代码。
5.5 导入时的常见坑
模块化之后,最常见的异常是ModuleNotFoundError。这个错误通常发生在三种情况:第一,你运行脚本的目录和模块文件不在同一个目录下;第二,模块名和 Python 内置模块重名;第三,两个模块互相导入。
最稳妥的解决方法是:把项目根目录设置为根路径,所有模块之间的导入都从根目录开始写。比如上面的from login_page import LoginPage,要求你运行测试时的工作目录在项目根目录下。如果使用 pytest,通常会从根目录收集用例,因此这类问题会少很多。
6. 第三步:综合项目实战——小型接口测试项目
前面两步是基础拆解,这一节要把类和模块组合起来,完成一个可以完整运行的小型接口测试项目。为了让读者不需要依赖外部公司环境就能跑通,我们会先写一个基于 Flask 的本地 Mock 服务,把它当成“被测系统”,然后编写接口测试脚本对它进行测试。
6.1 项目结构
api_test_project/ ├── app.py # Flask Mock 被测服务 ├── config.py # 公共配置 ├── api_client.py # 接口客户端封装(类) ├── test_user_api.py # pytest 测试用例 ├── pytest.ini # pytest 配置 └── requirements.txt # 依赖清单6.2 被测 Mock 服务
# 文件路径:api_test_project/app.py """基于 Flask 的本地 Mock 服务,模拟被测系统的用户管理接口""" from flask import Flask, request, jsonify app = Flask(__name__) USERS = { "admin": {"password": "123456", "nickname": "管理员"}, } @app.route("/api/login", methods=["POST"]) def login(): data = request.get_json(force=True) username = data.get("username") password = data.get("password") user = USERS.get(username) if user and user["password"] == password: return jsonify({"code": 200, "message": "登录成功", "token": "mock-token-xxxx"}) return jsonify({"code": 401, "message": "用户名或密码错误"}), 401 @app.route("/api/users/<username>", methods=["GET"]) def get_user(username): user = USERS.get(username) if not user: return jsonify({"code": 404, "message": "用户不存在"}), 404 return jsonify({"code": 200, "data": {"username": username, "nickname": user["nickname"]}}) if __name__ == "__main__": app.run(host="127.0.0.1", port=5000, debug=False)这个 Mock 服务非常小,但已经覆盖了接口测试中常见的两类请求:POST 登录接口和 GET 用户查询接口。其中登录失败时会返回 401 状态码,这是刻意设计的,用来演示测试中“既要验证业务成功、也要验证业务失败”的常见场景。
6.3 公共配置模块
# 文件路径:api_test_project/config.py """公共配置模块,环境相关配置统一放到这里维护""" BASE_URL = "http://127.0.0.1:5000" TIMEOUT = 5把BASE_URL和TIMEOUT单独放在一个配置模块里,是一个非常重要的工程习惯。将来测试环境从本地切换到测试服务器,只需要改config.py这一个文件,所有调用方都会自动生效。如果测试脚本里到处硬编码地址,换环境时就会像前面说的那样,满项目找字符串替换。
6.4 接口客户端封装
# 文件路径:api_test_project/api_client.py """接口客户端封装:用类组织登录和用户查询的请求逻辑""" import requests import config class UserApiClient: def __init__(self, base_url=config.BASE_URL, timeout=config.TIMEOUT): self.base_url = base_url self.timeout = timeout self.session = requests.Session() def login(self, username, password): url = f"{self.base_url}/api/login" response = self.session.post( url, json={"username": username, "password": password}, timeout=self.timeout, ) return response.status_code, response.json() def get_user(self, username, token=None): url = f"{self.base_url}/api/users/{username}" headers = {} if token: headers["Authorization"] = f"Bearer {token}" response = self.session.get(url, headers=headers, timeout=self.timeout) return response.status_code, response.json()UserApiClient是整个测试项目中最核心的类。它把“登录”“获取用户信息”两个业务操作封装成方法,并且返回一个包含status_code和response.json()的元组,方便测试用例拿到状态码和响应体分别断言。session对象是复用连接的关键,如果接口有跨请求关联,比如登录后保存 Cookie 或 Token,使用同一个 session 可以模拟出真实的用户会话状态。
6.5 pytest 测试用例
# 文件路径:api_test_project/test_user_api.py """pytest 测试用例:关注结果验证,不关注请求细节""" from api_client import UserApiClient from assert_utils import AssertUtils def setup_function(): # 每个测试用例执行前,创建一个新的客户端实例 global client client = UserApiClient() def test_login_success(): status_code, data = client.login("admin", "123456") AssertUtils.assert_equals(status_code, 200, "登录接口状态码") AssertUtils.assert_equals(data.get("code"), 200, "登录业务码") assert "token" in data, "登录成功响应中应包含 token" def test_login_wrong_password(): status_code, data = client.login("admin", "wrong") AssertUtils.assert_equals(status_code, 401, "错误密码状态码") AssertUtils.assert_equals(data.get("code"), 401, "错误密码业务码") def test_get_user_success(): _, login_data = client.login("admin", "123456") token = login_data.get("token") status_code, data = client.get_user("admin", token=token) AssertUtils.assert_equals(status_code, 200, "查询用户状态码") assert data.get("data", {}).get("nickname") == "管理员", "昵称应为管理员" def test_get_user_not_found(): status_code, data = client.get_user("nobody") AssertUtils.assert_equals(status_code, 404, "不存在的用户状态码") AssertUtils.assert_equals(data.get("code"), 404, "不存在的用户业务码")注意看test_get_user_success这个用例:它先调用login拿到 token,再带着 token 去查询用户信息。这种“用例间通过返回结果传递数据”的方式,比把 token 存成全局变量更安全,因为它完全依赖对象的方法返回值,不会出现测试用例顺序干扰的问题。这也是类封装带来的直接收益:每个实例的状态是独立的,测试用例之间的关系是显式的。
6.6 pytest 配置和依赖清单
# 文件路径:api_test_project/pytest.ini [pytest] testpaths = . python_files = test_*.py python_functions = test_*# 文件路径:api_test_project/requirements.txt flask requests pytestpytest.ini的作用是告诉 pytest 在哪些目录下找测试文件、哪些文件算测试文件、哪些函数算测试函数。如果不配置,pytest 也有默认规则,比如文件名以test_开头。这里显式配置的目的是为了避免测试目录中出现其他.py文件被误收集。
7. 运行结果与效果验证
接口测试项目的运行需要两个步骤:先启动 Mock 服务,再执行 pytest 测试。开发过程中我建议开两个终端窗口。
第一个终端窗口启动被测服务:
cd api_test_project python app.py如果 Flask 安装成功,你会看到类似下面的输出:
* Serving Flask app 'app' * Debug mode: off * Running on http://127.0.0.1:5000看到Running on http://127.0.0.1:5000说明 Mock 服务已经就绪。这时不要关闭窗口,保持它在后台运行。
第二个终端窗口执行测试:
cd api_test_project pytest -v正常情况下,预期输出类似:
============================= test session starts ============================= collected 4 items test_user_api.py::test_login_success PASSED test_user_api.py::test_login_wrong_password PASSED test_user_api.py::test_get_user_success PASSED test_user_api.py::test_get_user_not_found PASSED ============================== 4 passed in 0.43s ==============================看到4 passed说明全部用例通过。如果某一个用例失败,pytest 会直接打印失败位置和断言对比信息。第一步应该检查你自己的断言是否符合 Mock 服务的实际返回,而不是急着改代码。因为很多时候“测试失败”恰恰说明被测代码或测试数据有问题,这正是测试脚本存在的意义。
如果所有用例都失败,优先检查 Flask 是否还在运行,以及config.py里的BASE_URL是否正确。常见的错误是把127.0.0.1写成了localhost,在某些系统上这不会造成问题,但在个别代理环境下会导致请求失败。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
ModuleNotFoundError: No module named 'xxx' | 模块文件不在当前路径,或模块名拼写错误 | 检查文件目录结构,确认import路径 | 调整项目根目录后重新运行;避免模块名与 Python 内置模块重名 |
AttributeError: 'UserApiClient' object has no attribute 'session' | 忘记在__init__中初始化self.session | 查看构造方法是否完整 | 在__init__中给属性赋值后,其他方法才能通过self.xxx访问 |
TypeError: login() missing 1 required positional argument: 'self' | 直接使用类名调用了实例方法 | 检查调用代码,确认是否创建了实例 | 先实例化client = UserApiClient(),再调用client.login(...) |
| pytest 收集到 0 个测试用例 | 文件命名或函数命名不符合 pytest 规则 | 运行pytest --collect-only查看收集结果 | 将测试文件和函数以test_开头,或在pytest.ini中配置匹配规则 |
| Flask 端口被占用 | 上一次运行的进程未关闭 | 查看端口占用,或换一个端口 | 关闭旧进程,或修改app.run(port=5001)并同步修改配置 |
| 接口返回 500 错误 | Mock 服务内部异常或get_json解析失败 | 查看 Flask 终端日志 | 检查请求是否使用了正确的Content-Type,请求体是否为合法 JSON |
排查时建议遵循一个顺序:先看错误信息的第一行,再定位到对应的文件与行号,最后看是导入问题、语法问题还是逻辑问题。不要一上来就搜索“为什么报错”,很多错误信息已经直接告诉了你修改方向。
9. 最佳实践与工程建议
9.1 命名规范要统一
类名使用大驼峰命名法,例如UserApiClient;函数名和变量名使用小写下划线命名法,例如get_user、base_url;常量使用全大写,例如BASE_URL。测试模块尽量用test_开头,测试函数也用test_开头,这样 pytest 可以自动收集,不需要手工维护测试清单。
命名规范在这个阶段看起来是小事,但当你进入团队协作时,它会直接影响代码 Review 效率和信息检索效率。一个测试项目中如果一半人用驼峰、一半人用下划线,搜索代码时会非常痛苦。
9.2 每个类只做一件事
UserApiClient只负责接口请求,AssertUtils只负责断言,app.py只负责模拟被测服务。如果一个类既处理接口请求又处理数据库校验又负责生成测试报告,那么任何一处需求变化都会导致你改动这个类,回归风险随之上升。在测试领域,这一点与被测系统设计中的“单一职责原则”完全一致。
9.3 测试代码与生产代码分离
Mock 服务、测试用例、公共配置都要放在独立的项目目录中,不要和被测试的业务系统混在一起。测试代码中的依赖和生产代码无关,混在一起会导致双方的依赖管理互相污染。在真实的测试开发团队中,自动化测试通常有独立的代码仓库和独立的 CI 流水线。
9.4 配置与测试数据分离
config.py里只放环境地址、超时时间这类配置,不要把具体的用户名、密码、业务数据写死在这里。测试数据建议放到独立的 JSON 或 YAML 数据文件中,或者直接写在测试用例函数中。这样做的目的是隔离变化:配置变化和环境相关,测试数据变化和具体场景相关,两者的修改频率和触发条件完全不同。
9.5 安全边界意识
在接口测试中,你可能会接触到用户名密码、Token、数据库连接串等敏感信息。测试代码也是代码,同样要遵循最小权限原则。不要使用生产环境的超管账号跑测试,不要在代码仓库中提交真实密码和密钥,Mock 环境尽量使用本机回环地址,避免暴露到公网。如果涉及数据库操作,一定要在测试库或临时库中执行,并且保证测试数据可回收。
9.6 从项目一开始就使用虚拟环境
Python 项目最常见的依赖冲突,往往不是代码逻辑问题,而是不同项目依赖了同一个第三方库的不同版本。从项目第一天就创建虚拟环境,把依赖清单固化到requirements.txt,可以避免很多莫名其妙的“我本地明明能跑,怎么别人那里就报错”问题。
10. 总结与后续学习方向
这一篇教程把类和模块放到了软件测试的真实场景里讲,核心收获可以归纳成三点:类把同一组数据和操作封装在一起,模块把不同职责的代码拆分到不同文件里,两者结合才能支撑起可持续维护的自动化测试项目。你在阅读时不需要一开始就追求“写出完美架构”,先从小例子改起,把一个函数脚本逐步改造成类封装和模块拆分,过程中体会维护成本的变化,比背任何理论都有效。
下一步,建议你按这个顺序继续深入:先把本文的项目实战代码在本地跑通,然后把UserApiClient扩展成更多业务接口的封装,再接触 pytest 的固件机制和参数化特性,最后学习 Requests 的高级用法和实际接口联调中的鉴权、重试、日志处理。类与模块之后,还有一个非常值得深挖的方向是“测试分层”,也就是把 UI 测试、接口测试、单元测试的组织方式分开设计。
如果你正在准备软件测试的笔试面试,可以把本文中的LoginClient、UserApiClient这类代码当成手写题素材,重点理解self的作用、静态方法与实例方法的区别、模块导入的路径规则。这些知识点在面试中出现的频率极高,而且一旦理解了背后的逻辑,你不需要死记硬背,就能用自己的话表达清楚。希望这篇教程能成为你测试之路上的一个实用路标。