
在 Python 生态里做自动化测试绕不过 unittest。不管你是刚接触测试开发的新人还是已经在接口、UI 自动化里摸爬滚打了一阵子你会发现社区里铺天盖地都是 pytest 的教程但真正到了企业级项目里unittest 依然是那个“最稳的底牌”——不用装任何依赖Python 装好就能跑规范清晰、集成省心而且 pytest 底层的很多设计理念其实也源于它。这篇就把 unittest 框架本身和它背后的自动化测试实现流程完整拆一遍从一个可落地的项目角度讲清楚怎么设计用例、怎么组织目录、怎么跑出报告、怎么处理数据驱动和 token 依赖再顺带聊聊它和 pytest 到底该怎么选。废话不多说直接进入正题。1. 为什么还在用 unittest这套框架的真正价值1.1 标准库的底气零依赖也能撑起自动化unittest 是 Python 标准库自带的测试框架从 Python 2.1 时代就存在早期叫 PyUnit脱胎于 Java 的 JUnit。这个出身决定了它的一个最大特质无需单独安装、无第三方依赖、环境一致性极好。在 CI/CD 流水线里这意味着你只需要一个装了 Python 的镜像或者 runner就能直接跑测试不用为环境问题扯皮。那有人会问pytest 功能更强大插件生态更丰富为什么还要回过头来用 unittest我的看法是框架选型从来不是单选题而是围绕项目特点做取舍。如果你负责的是一个快速迭代的中小型项目或者团队里大部分人是写 Python 脚本出身、对复杂 fixture 机制接受度有限那 unittest 这种“一切显式、一切可控”的风格反而是优势。另外还有一个很现实的理由unittest 是学习自动化测试的最佳起点。它的设计非常标准TestCase、TestSuite、TestRunner、TestLoader 这些概念是 xUnit 体系的标准模型。把这个模型吃透了你去看 Java 的 JUnit、C# 的 NUnit、甚至 JavaScript 的 Mocha会发现架构思路几乎一模一样。这套思维是跨语言的通用资产值得花时间打牢。1.2 unittest 和 pytest 怎么选现实场景的取舍为了不让你在选择时纠结我直接把两者的差异讲透。unittest 的优势零依赖项目在任何环境下都能直接跑。断言方法丰富且语义明确assertEqual、assertTrue、assertIn、assertRaises等几十个方法覆盖了绝大多数场景定位失败原因更精准。原生支撑测试套件TestSuite和用例加载器TestLoader的编程式组合后端执行逻辑完全可控。通过discover可以自动递归地发现所有测试用例不用手动维护用例清单。unittest.mock 是内置的 mock 库做单元测试打桩非常方便不需要额外安装。它的短板fixture 体系写起来相对啰嗦setUp、tearDown的细粒度控制不如 pytest 的 fixture 灵活。断言要调用self.assertXxx()不像 pytest 里直接assert xxx yyy那么顺手。插件生态、报告美观度需要额外整合不像 pytest 附带 allure、pytest-html 等一整套现成方案。我的选型建议做单元测试、写算法测试或工具函数测试首选 unittest。轻便、隔离性好不会有依赖冲突。做接口自动化、UI 自动化这类业务型测试如果团队已经是 pytest 生态可以用 pytest如果是从零搭一套干净的项目或者团队对 unittest 更熟坚持用 unittest 完全没有问题后面我会展示如何用 unittest 搭出实用的接口自动化框架。说白了很多招聘 JD 里写的“熟悉 unittest 或 pytest”本质上就是要求你理解“测试框架”这个抽象层怎么实现断言、怎么管理用例生命周期、怎么生成报告。框架只是工具思路才是核心。2. Unittest 核心机制拆解测试用例是怎么跑起来的2.1 骨架TestCase、TestSuite、TestRunner 各司其职unittest 的核心模型就是一句话把测试逻辑封装成 TestCase把多个 TestCase 汇总成 TestSuite再由 TestRunner 去执行并将结果告诉开发者。TestCase一个继承unittest.TestCase的类每个以test_开头的方法就是一个独立的测试用例。TestSuite用例容器可以把分散在不同类、不同模块中的用例按需组合起来。TestRunner执行器负责真正运行 TestSuite、收集执行结果、输出到屏幕或文件。举个简单例子import unittest class TestMath(unittest.TestCase): def test_add(self): self.assertEqual(1 1, 2) if __name__ __main__: unittest.main()这里unittest.main()会自动查找当前模块中 TestCase 子类中所有test_开头的方法并执行。main()是 TestRunner 的“快捷方式”帮你把查找用例、组装套件、运行测试三步合并执行。如果不想用main()的自动发现可以编程式手动组织import unittest from test_math import TestMath suite unittest.TestSuite() suite.addTest(TestMath(test_add)) runner unittest.TextTestRunner(verbosity2) runner.run(suite)这种编程方式最大的价值在于你可以在运行时动态决定跑哪些用例。比如只跑冒烟用例、只跑某个模块的用例、甚至根据环境变量来选择执行范围这在自动化测试里非常常见。2.2 fixture 体系setUp 和 tearDown 的生命周期管理自动化测试里最怕什么数据污染和状态残留。一个用例跑完数据库多了一条记录另一个用例跑完登录态过期了。这些问题靠 fixture 机制来解决。unittest 提供了四个层级的 fixture按粒度从小到大setUp/tearDown每个用例执行前后运行。setUpClass/tearDownClass整个测试类执行前后运行一次必须用classmethod修饰。setUpModule/tearDownModule模块级别的 fixture定义在模块内执行模块中所有用例之前和之后运行。addCleanup/addClassCleanup动态注册的清理函数即使用例断言失败也会执行。实际项目里最常用的是前三种。以接口测试为例import unittest import requests class TestOrderAPI(unittest.TestCase): classmethod def setUpClass(cls): # 整个类只登录一次拿到的 token 放到类变量里共享 resp requests.post(http://example.com/api/login, json{ username: admin, password: 123456 }) cls.token resp.json()[data][token] def setUp(self): # 每个用例之前重置请求头 self.headers {Authorization: fBearer {self.token}} def test_get_order(self): resp requests.get(http://example.com/api/order/1001, headersself.headers) self.assertEqual(resp.status_code, 200) def tearDown(self): # 每个用例之后清理测试产生的临时数据 requests.post(http://example.com/api/test-cleanup, headersself.headers)这套机制在 UI 自动化里对应的是浏览器的启动和关闭。通常把浏览器初始化放在setUp里确保每个用例都是独立的浏览器状态但如果浏览器启动特别耗时也可以放在setUpClass里整个类共享一个浏览器实例——这就需要你对用例之间的耦合风险有清晰认知。2.3 断言体系用对断言才能少踩坑断言是测试的灵魂。测试用例跑完了到底是通过还是失败取决于断言。unittest 的断言方法看起来简单但用错了会有两个典型问题断言太宽比如只用assertTrue(result expected)失败时只告诉你 False排查成本高。断言太窄比如强行断言一个不稳定的时间戳项目一上线就疯狂报错。我常用的断言选择清单需求推荐断言原因判断两个值相等assertEqual(a, b)失败时同时打印 a 和 b判断值在集合中assertIn(item, container)比assertTrue(item in container)更直观判断类型assertIsInstance(obj, Class)明确类型检查判断异常assertRaises(SomeError, func, args...)不依赖 try/except 手写判断响应结构assertIn(code, resp.json())接口返回字段校验判断数值范围assertGreater(a, b)或assertAlmostEqual避免浮点误差实际做接口自动化时我最常用的组合是assertEqual校验 HTTP 状态码assertIn校验关键的响应字段assertEqual校验业务 code再用assertGreater校验耗时是否在预期内。多层断言虽然代码多几行但每个断言失败都能给到足够明确的定位信息比一个大而全的assert实用得多。2.4 用例收集TestLoader.discover 的做法单文件跑测试unittest.main()就够了但真实项目通常有几十个测试文件不可能一个个手动添加。这时用TestLoader.discover。import unittest if __name__ __main__: loader unittest.TestLoader() suite loader.discover(start_dir./testcases, patterntest_*.py) runner unittest.TextTestRunner(verbosity2) runner.run(suite)discover会递归扫描start_dir目录下所有文件名匹配pattern的模块自动加载其中的 TestCase。这套机制把项目规模扩展的难题解决了——你每新增一个测试文件只要命名规范test_开头无需改任何代码即可纳入执行。我习惯在执行入口加一个-v参数或者verbosity2可以看到每个用例的独立执行结果定位失败用例时不用再猜。3. 自动化测试完整实现流程从零到可交付3.1 需求分析和用例设计自动化测什么最关键很多新手一上来就开始写代码结果写了三天才全跑通发现测试的是个马上就要改版的接口——这就是选择被测对象出了问题。自动化测试的第一步永远是搞清楚到底该测什么。做接口自动化优先选择以下类型的接口核心主流程比如登录、下单、支付这些链路一旦出问题影响面最大。稳定且不易变更的接口文档成熟、字段稳定的接口自动化用例不容易维护崩盘。高频回归需求接口每次发版都要手工回归的接口自动化以后节省人工成本。多数据组合的接口同样的接口需要验证不同参数组合靠自动化批量跑人力做容易遗漏。同时要明确“不测什么”。只测一次、马上要下线的功能没必要投入自动化成本。自动化要解决的是重复性高、可预知结果、需要反复验证的那部分工作。用例设计层面我习惯按“正常流、异常流、边界值”三个维度来覆盖正常流一个合法入参组合验证接口返回预期业务结果。异常流缺失参数、非法 token、错误的请求方法、超长字段。边界值分页的 page1/page0/超大 page金额为 0、负数、极大值字符长度等于最大值和最大值加 1。用例不要贪多每个用例只验证一个核心点这样失败时你一眼能看出是哪个条件引起的。把多个独立条件塞进一个用例里失败了还要二次排查是你写用例阶段给自己挖的最大的坑。3.2 项目结构搭建怎么组织代码才不失控自动化测试写到最后失败通常不是因为技术难点而是因为代码失控目录乱、复用差、改一个接口要连着改五个文件。我的标准目录结构是这样autotest/ ├── config/ # 配置文件环境地址、账号密码、数据库连接等 │ └── config.yaml ├── common/ # 公共封装请求类、日志类、数据库操作类、加解密工具 │ ├── request_client.py │ ├── logger.py │ └── db_client.py ├── test_data/ # 测试数据接口入参、期望结果yaml/excel/json │ ├── login_data.yaml │ └── order_data.yaml ├── testcases/ # 测试用例目录按业务模块拆分 │ ├── test_login.py │ ├── test_order.py │ └── test_pay.py ├── reports/ # 测试报告和日志输出目录 ├── runners/ # 执行入口、用例组装逻辑 │ └── run_all.py └── requirements.txt # 依赖清单这个结构的核心思路是按职责分层数据、配置和代码分离。一个测试用例由三部分组成用例逻辑testcases、测试数据test_data、公共能力common/config。任何一部分变化尽量只影响对应层不牵连其他层。举个例子接口请求的域名从测试环境切到预发环境应该只改config/config.yaml否则你就要打开每一个测试文件去换 URL那是灾难级的项目维护体验。3.3 脚本编写与数据驱动把重复劳动交给数据单元测试里每个用例写一套独立逻辑没问题但接口自动化场景里同一个接口往往需要几十组数据校验。如果每个数据组合都写一个test_方法代码会膨胀到没法看。这时候就要上数据驱动。unittest 官方不直接提供数据驱动能力但配合第三方库DDTData-Driven Tests可以实现import unittest from ddt import ddt, data, unpack ddt class TestLoginAPI(unittest.TestCase): data( (admin, 123456, 200, success), (admin, wrong, 200, password_error), (, 123456, 200, username_empty), (admin, , 200, password_empty), ) unpack def test_login(self, username, password, status_code, expect_code): resp self.client.post(/api/login, json{username: username, password: password}) self.assertEqual(resp.status_code, status_code) self.assertEqual(resp.json()[code], expect_code)data传入一组组的元组unpack负责解包映射到测试方法的参数上。这样跑出来的结果里每一条数据都是一个独立的用例失败时能精确定位是哪组数据触发的。这段代码里测试数据还硬编码在用例模块里。更彻底的做法是把数据放到 yaml 或 excel 文件中用ddt.file_data直接加载from ddt import file_data ddt class TestLoginAPI(unittest.TestCase): file_data(login_data.yaml) def test_login(self, username, password, status_code, expect_code): ...login_data.yaml这样写case_01: username: admin password: 123456 status_code: 200 expect_code: success case_02: username: admin password: wrong status_code: 200 expect_code: password_error到这里你的测试逻辑已经和数据彻底分开了。产品经理来跟你说“加 30 组数据”你只需要往 yaml 里加字段代码一行不动。这是自动化测试工程化的关键一步。3.4 执行策略与测试报告看得到的结果才有价值自动化测试跑完要是不生成一份能看懂的报告执行闭环就断了。unittest 自带TextTestRunner屏幕输出太简陋实际项目中一般配 HTML 报告。主流的方案有两个方案一HTMLTestRunner老牌方案基于 unittest 的 TestResult 二次封装生成一份单文件的 HTML 报告。Python 3 环境下有社区维护的兼容版本直接在项目里from HTMLTestRunner import HTMLTestRunner就能用。import unittest import time from HTMLTestRunner import HTMLTestRunner if __name__ __main__: suite unittest.TestLoader().discover(testcases, patterntest_*.py) report open(freports/report_{time.strftime(%Y%m%d_%H%M%S)}.html, wb) runner HTMLTestRunner(streamreport, title接口自动化测试报告, description项目回归报告) runner.run(suite)方案二BeautifulReport第三方库界面更好看适合团队内部展示。接口差不多from BeautifulReport import BeautifulReport result BeautifulReport(suite) result.report(filename接口自动化测试报告, description登录、订单模块回归, report_dirreports/)报告命名加上时间戳方便后续追溯历史执行记录。把报告文件放入 CI 产物每次构建后都能下载查看。4. 实战案例登录接口与订单查询接口的自动化测试4.1 被测接口的拆分设计所有模式讲一遍都不如完整跑一个案例。下面以一个典型的电商后端接口为例把整个流程串起来。被测接口有两个POST /api/login入参 username、password正确时返回{code: success, data: {token: xxx}}。GET /api/order/query入参 order_id请求头需要带Authorization: Bearer token返回订单基本信息。这个场景最有代表性的一点是接口之间存在依赖关系。登录拿到 token后续接口要用 token这是接口自动化测试里最经典的问题。4.2 基础请求封装requests 的二次封装思路直接在所有用例里requests.post(...)当然可以但项目中一旦要统一加日志、统一加超时、统一处理重试就会处处改代码。我习惯在common层封装一个请求类import requests import time import logging logger logging.getLogger(autotest) class RequestClient: def __init__(self, base_url, timeout10): self.base_url base_url self.timeout timeout def post(self, path, jsonNone, headersNone): url f{self.base_url}{path} logger.info(fPOST {url} data{json}) resp requests.post(url, jsonjson, headersheaders, timeoutself.timeout) logger.info(fResponse status{resp.status_code} body{resp.text}) return resp def get(self, path, paramsNone, headersNone): url f{self.base_url}{path} logger.info(fGET {url} params{params}) resp requests.get(url, paramsparams, headersheaders, timeoutself.timeout) logger.info(fResponse status{resp.status_code} body{resp.text}) return resp统一封装的收益是埋点式的日志、耗时统计、超时控制、重试策略都集中在一个类里。后续如果要把请求记录打进 ELK只需要改这个文件。4.3 用例类编写setUpClass 的妙用核心代码import unittest from common.request_client import RequestClient class TestOrderQuery(unittest.TestCase): base_url http://127.0.0.1:8080 classmethod def setUpClass(cls): # 一个测试类只登录一次token 整个类共享 client RequestClient(cls.base_url) resp client.post(/api/login, json{username: admin, password: 123456}) cls.token resp.json()[data][token] cls.client RequestClient(cls.base_url) def get_headers(self): return {Authorization: fBearer {self.token}} def test_query_exist_order(self): resp self.client.get(/api/order/query, params{order_id: 1001}, headersself.get_headers()) self.assertEqual(resp.status_code, 200) self.assertEqual(resp.json()[code], success) self.assertIn(order_id, resp.json()[data]) self.assertEqual(resp.json()[data][order_id], 1001) def test_query_without_token(self): resp self.client.get(/api/order/query, params{order_id: 1001}, headers{}) self.assertEqual(resp.status_code, 200) self.assertEqual(resp.json()[code], unauthorized) def test_query_nonexist_order(self): resp self.client.get(/api/order/query, params{order_id: 999999}, headersself.get_headers()) self.assertEqual(resp.status_code, 200) self.assertEqual(resp.json()[data], None)这里有个容易踩的坑setUpClass里如果断言失败后续所有用例都会报错。所以登录失败时我建议抛出明确的异常而不是静默通过data resp.json() if data[code] ! success: raise RuntimeError(flogin failed: {data})这样一旦登录环节出问题你能第一时间发现而不是被一堆“token 非法”的用例失败刷屏。4.4 数据驱动改造让用例数量翻倍而不增加代码订单查询接口同样可以用数据驱动把不同的订单状态、预期返回组合塞进 yaml 数据case_query_normal: order_id: 1001 expect_code: success expect_data_not_null: true case_query_nonexist: order_id: 999999 expect_code: success expect_data_not_null: false case_query_negative: order_id: -1 expect_code: param_error用例类改写成from ddt import ddt, file_data ddt class TestOrderQueryDDT(unittest.TestCase): ... file_data(order_query_data.yaml) def test_query(self, order_id, expect_code, expect_data_not_null): resp self.client.get(/api/order/query, params{order_id: order_id}, headersself.get_headers()) self.assertEqual(resp.json()[code], expect_code) data resp.json().get(data) if expect_data_not_null: self.assertIsNotNone(data) else: self.assertIsNone(data)这样做的好处体现在回归阶段产品在订单模块新增了一个“已取消订单不可见”的规则你只需要往数据集里加一行取消状态的订单数据用例自动扩充不需要改任何逻辑代码。4.5 报告生成与执行入口整合最后把执行入口写到runners/run_all.pyimport time import unittest from HTMLTestRunner import HTMLTestRunner if __name__ __main__: suite unittest.TestLoader().discover(start_dir../testcases, patterntest_*.py) now time.strftime(%Y%m%d_%H%M%S) with open(f../reports/test_report_{now}.html, wb) as report_file: runner HTMLTestRunner(streamreport_file, title订单接口自动化报告, description回归测试, verbosity2) runner.run(suite)从命令行直接python runners/run_all.py就能一键执行全部用例并输出 HTML 报告。如果接入 Jenkins可以在构建任务里加一步 “Execute Python script”把这个命令挂进去产物归档reports/*.html每次构建结束就能在 Jenkins 上直接查看测试报告。5. 进阶扩展unittest 怎么撑起 UI 自动化5.1 selenium 集成与 POM 分层很多人以为 unittest 只适合接口和单元测试其实用它做 UI 自动化selenium、playwright、appium也是常见方案。unittest 的setUp/tearDown和浏览器生命周期天然契合。以 selenium 为例最基本的模板import unittest from selenium import webdriver class TestLoginUIT(unittest.TestCase): def setUp(self): self.driver webdriver.Chrome() self.driver.get(http://example.com/login) self.driver.implicitly_wait(10) def tearDown(self): self.driver.quit() def test_invalid_password(self): self.driver.find_element(name, username).send_keys(admin) self.driver.find_element(name, password).send_keys(wrong) self.driver.find_element(tag name, button).click() self.assertIn(密码错误, self.driver.page_source) def test_valid_login(self): self.driver.find_element(name, username).send_keys(admin) self.driver.find_element(name, password).send_keys(123456) self.driver.find_element(tag name, button).click() self.assertIn(欢迎回来, self.driver.page_source)不过 UI 自动化最怕元素定位逻辑散落在各处后期维护很痛苦。项目里我都会引入POMPage Object Model页面对象模型每个页面一个类封装页面元素定位和操作用例层只关心业务操作和断言不直接写 find_element。# pages/login_page.py from selenium.webdriver.common.by import By class LoginPage: def __init__(self, driver): self.driver driver def input_username(self, text): self.driver.find_element(By.NAME, username).send_keys(text) def input_password(self, text): self.driver.find_element(By.NAME, password).send_keys(text) def click_login(self): self.driver.find_element(By.TAG_NAME, button).click()用例层调用from pages.login_page import LoginPage class TestLoginUIT(unittest.TestCase): def setUp(self): ... self.login LoginPage(self.driver) def test_valid_login(self): self.login.input_username(admin) self.login.input_password(123456) self.login.click_login() self.assertIn(欢迎回来, self.driver.page_source)页面类一旦写好后续页面结构调整时只需要改pages层的一个文件所有用例自动同步修复这是 UI 自动化可持续维护的底线。5.2 并行执行与 CI 集成unittest 默认串行执行用例多了以后执行时间不可接受。我的做法是用unittest-xdist或者直接用 pytest 跑 unittest 用例pytest 能兼容 unittest 用例再开并行。更精简的做法是手动分片把用例按业务模块切分在 CI 上配置多个 job每个 job 跑一部分用例。比如订单回归、支付回归、用户中心回归分别跑不同的 discover 路径互不干扰、各出各的报告。这种方式在 Jenkins 的 Multijob 插件里非常好用。CI 集成的核心动作就三个拉代码后安装依赖pip install -r requirements.txt执行测试python runners/run_all.py归档报告把reports/目录设为构建产物。测试结果不通过时让 CI 任务失败阻断发布流程。这一步才是自动化测试能守住质量关的关键。6. 常见问题排查我在实战中踩过的坑6.1 用例不执行、执行了但数量对不上新手的第一个坎往往是跑了但没有用例输出。排查顺序类名必须继承unittest.TestCase不能是裸类。测试方法必须test_开头否则discover不会收集。文件名必须匹配pattern默认test*.py或test_*.py我建议统一规范为test_开头。如果你在自己的测试类里写了__init__记得调用super().__init__(methodName)否则用例加载报错。最容易迷惑人的一个场景是discover的start_dir设置错误。它要求传入的是包的路径或者模块搜索路径不是单纯的相对路径瞎试。我建议用os.path.dirname(os.path.abspath(__file__))拼绝对路径一劳永逸import os import unittest base_dir os.path.dirname(os.path.dirname(os.path.abspath(__file__))) suite unittest.TestLoader().discover(start_diros.path.join(base_dir, testcases), patterntest_*.py)6.2 fixture 相关的坑共享状态和数据污染用setUpClass共享 token 是常见做法但要注意用例之间的状态污染。典型错误一个用例修改了类变量下一个用例读到被修改的值产生偶发失败。比如classmethod def setUpClass(cls): cls.order_id 1001 def test_order_a(self): self.__class__.order_id 999999 # 修改了类变量 ... def test_order_b(self): # 这里拿到的可能是被打乱后的值 ...规避方式跨用例共享的数据尽量设计成只读需要变化的放在独立文件里每个用例自己的临时数据用setUp初始化。这个经验是“用例隔离性”的第一原则比什么都重要。另一个常见坑是tearDown里清理失败导致后续用例受影响。我的建议是清理动作失败不要让它阻断用例结果用 try/except 包住记录日志。测试的主逻辑是验证功能不是验证清理动作。6.3 断言和响应处理接口返回结构变化怎么应对接口自动化很容易因为后端字段调整导致大量用例失败。遇到这类问题不要每个用例都写一整套resp.json()[data][xxx]的深层取值。统一写一个响应解析工具提供get(resp, data.order_id)这种安全取值方式字段不存在时返回 None 而不是抛 KeyError。断言时先判状态码再判业务 code最后才验证具体字段值。这样失败信息有层次先知道接口通没通再知道业务对不对。对时间戳、随机数这类不稳定字段用assertIn或正则范围匹配不要精确断言。6.4 报告生成失败的排查HTMLTestRunner 在某些 Python 3 环境下报AttributeError: TestResult object has no attribute output之类的错误通常是因为安装的版本太老和当前 Python 版本不兼容。排查思路确报使用的 HTMLTestRunner 是 Python 3 兼容版很多老教程的版本只支持 Python 2。报告文件打开乱码时检查打开模式是不是二进制写wb以及报告模板是否硬编码了无 BOM 的 utf-8。报告路径不存在时先os.makedirs(reports, exist_okTrue)再写入。实在不想折腾 HTMLTestRunner用 BeautifulReport 能省不少事交互界面也更顺眼。7. 经验沉淀把这些规则写进你的代码习惯里最后说几点我在多个自动化测试项目里沉淀下来的规则每一行都是踩坑换来的测试用例设计层面一条用例只验证一个核心业务点断言不超过 5 个为宜多则乱。用例命名要能表达业务意图test_query_normal_success、test_query_invalid_token不看代码也知道在测什么场景。用例之间不要有执行顺序依赖。unittest 默认按方法名排序执行但你不能依赖这个顺序因为套件重组会改变它。每个用例要能独立运行。框架使用层面setUpClass里做重量级的准备登录、初始化浏览器、连接数据库setUp里做轻量级重置。共享数据放类变量要克制放配置文件更安全。公共封装要尽早做请求封装、日志封装、报告封装。项目前期多做一步后期省十步。项目维护层面测试代码也是代码要走代码评审、版本管理。你们团队有没有把 testcases 目录纳入 Code Review 范围我见过太多测试代码成了“一滩死水”功能改了十次测试脚本还停留在五个月前的参数。每周或每两周跑一次完整的回归及时清理失效用例。用例不是越跑越多就一定好维护不了还不如删掉。我个人在实际项目里的体会是自动化测试的难点从来不是某个框架的 API 记不熟而是用例设计能力和工程化能力。unittest 只是把 xUnit 的那套标准模型忠实实现了一遍核心价值在于它的规范性和稳定性。你把 unittest 跑透了等于把自动化测试的基本功练扎实了以后切 pytest、接 allure、做平台都是水到渠成的事。如果看完这篇你打算从零搭一套接口自动化项目就从第二节的目录结构开始先把公共请求封装写好再把登录用例跑通第一个人力释放的节点就不远了。