前言
这个标题里有一个需要先纠正的说法:requests不是测试框架。requests是一个 HTTP 客户端库,它只负责发请求、收响应,跟「组织测试用例、收集测试结果、生成报告」这些测试框架的职责完全无关。它出现在自动化测试话题里,是因为写接口测试时常常用它来发请求——它是被测试代码调用的工具,不是承载测试的框架。
同理,Selenium严格说也不是「测试框架」,而是浏览器自动化驱动库:它把「打开浏览器、点按钮、读文本」这些动作暴露成 Python 方法,至于怎么组织用例、怎么断言,还是交给unittest或pytest。
把前提理清之后,标题里这五个名字其实分属四类,各司其职:
| 类别 | 名字 | 定位 | 来源 |
|---|
| 测试框架 | unittest | 内核自带,类式用例,自带断言方法 | 标准库 |
| 测试框架 | pytest | 第三方,函数式用例,裸assert | 需安装 |
| 浏览器驱动 | Selenium | 驱动真实浏览器做端到端操作 | 需安装 |
| BDD 工具 | pytest-bdd | 把 Gherkin 的场景文本映射到 pytest 用例 | 需安装 |
| HTTP 辅助 | requests | 在测试里发 HTTP 请求 | 需安装 |
本文按这个分类逐个讲它们的定位与分工,并说明它们通常怎么组合。注意requests只作为「测试里的辅助工具」出现,本文不把它当框架讲。版本提示:按 PyPI 元数据,最新的pytest、selenium、pytest-bdd都声明requires_python为>=3.10;unittest属于标准库,随解释器走。Python 2.7 已于2020 年 1 月 1 日停止维护,下面一律用 Python 3 写法(例如断言用assertEqual而不是 2 时代的种种别名习惯,print用函数形式)。
一、unittest:标准库自带的测试框架
unittest直接import就能用,零依赖。它的组织方式是类 + 方法:测试类继承unittest.TestCase,方法名以test开头才会被收集。
# 适用于 Python 3.8+
import unittest
def add(a, b):
return a + b
class TestAdd(unittest.TestCase):
def setUp(self):
# 每个 test_ 方法执行前都会跑一次
self.samples = [1, 2, 3]
def tearDown(self):
# 每个 test_ 方法执行后都会跑一次,用来清理
self.samples = None
def test_add_positive(self):
self.assertEqual(add(1, 2), 3)
def test_add_negative(self):
self.assertEqual(add(-1, -2), -3)
def test_sum_of_samples(self):
self.assertEqual(sum(self.samples), 6)
if __name__ == "__main__":
unittest.main()要点:
- 断言是方法:
assertEqual、assertTrue、assertIn、assertRaises等,名字写在TestCase上。写错名字不会报「断言失败」,而是直接AttributeError。 setUp/tearDown是每条用例前后各跑一次;如果希望整个类只准备一次,用setUpClass/tearDownClass(它们是类方法)。- 保存为
test_*.py后,可以用python -m unittest discover自动发现并运行。
二、pytest:更轻的第三方测试框架
pytest需要pip install pytest。它最大的特点是不需要继承任何基类,普通函数就是用例,断言直接用 Python 的assert——失败时 pytest 会做「断言内省」,把表达式两边的实际值都打出来,所以不必去记几十个assertXxx方法名。
# 适用于 Python 3.10+(需要 pip install pytest)
import pytest
def add(a, b):
return a + b
def test_add_positive():
assert add(1, 2) == 3
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(-1, -2, -3),
(0, 0, 0),
])
def test_add_many(a, b, expected):
assert add(a, b) == expected
@pytest.fixture
def samples():
"""fixture:把测试需要的数据/资源准备好,由参数名注入。"""
return [1, 2, 3]
def test_sum_of_samples(samples):
assert sum(samples) == 6要点:
@pytest.mark.parametrize把一组数据展开成多条独立用例,失败时能精确指出是哪组数据挂了。- fixture用
@pytest.fixture声明,用例只要在参数列表里写上同名参数就能拿到,不需要手工调用、不需要继承。fixture 还能声明作用域(函数级、类级、模块级等)来做复用与清理。 - 运行方式是命令行
pytest或python -m pytest;后者能保证用的是当前解释器对应的 pytest。
两者的分工可以这样理解:新项目优先pytest,不需要额外依赖或要交付到受限环境时用unittest。它们不是互斥的——pytest能直接收集并运行unittest.TestCase写成的用例。
三、Selenium:驱动浏览器的工具库
Selenium解决的问题是「用户界面行为」:点按钮、填表单、等待元素出现、读取渲染后的文本。这些是纯 HTTP 请求库做不到的,因为浏览器要执行 JavaScript 才能把页面渲染出来。
# 适用于 Python 3.10+(需要 pip install selenium,且本机装有 Chrome)
from selenium import webdriver
from selenium.webdriver.common.by import By
def test_page_title():
driver = webdriver.Chrome() # 启动一个浏览器会话
try:
driver.get("https://example.com/")
heading = driver.find_element(By.TAG_NAME, "h1")
assert heading.text
finally:
driver.quit() # 无论如何都要退出,否则留下僵尸进程关键在于谁在启动时帮你把驱动准备好。Selenium 需要 chromedriver、geckodriver 这类浏览器驱动;从 Selenium 4.6 起,官方提供了Selenium Manager——官方文档称它是「Selenium 官方的驱动管理器」,随每个 Selenium 版本一起发布,Selenium的各个语言绑定默认会调用它,因此不需要你手工下载驱动或改代码。它在驱动不可用时才介入,会自动发现本机浏览器版本、下载匹配的驱动并缓存到本地缓存目录。
再强调一次:Selenium是驱动库,不是框架。真正的用例组织仍然由pytest或unittest完成——上面那个函数能被 pytest 直接收集运行,就是因为它写成了 pytest 认得的普通函数。
四、pytest-bdd:把场景文本映射到 pytest
BDD(行为驱动开发,Behavior-Driven Development)想让「业务人员也能读的用例描述」直接变成可执行的测试。pytest-bdd把 Gherkin 语法(Given/When/Then)的场景文件和方法绑定起来,而运行器仍然是 pytest。
先写一个.feature文件:
Feature: 购物车
Scenario: 加入一件商品
Given 购物车是空的
When 我加入一件价格为 10 的商品
Then 购物车总额应该是 10再用装饰器把它接上:
# 适用于 Python 3.10+(需要 pip install pytest-bdd)
from pytest_bdd import scenario, given, when, then
@scenario("cart.feature", "加入一件商品")
def test_add_one_item():
"""这个空函数是一个占位用例,steps 会在它之前执行。"""
@given("购物车是空的", target_fixture="cart")
def empty_cart():
return []
@when("我加入一件价格为 10 的商品")
def add_item(cart):
cart.append(10)
@then("购物车总额应该是 10")
def check_total(cart):
assert sum(cart) == 10要点:
@scenario("文件.feature", "场景名")把某个场景绑定到一个测试函数;这个函数体本身通常是空的,Given/When/Then步骤会在它之前按顺序执行。- 步骤函数可以用
target_fixture把返回值变成下游步骤共享的 fixture(例子里的cart就是这样在三个步骤之间传递的)。 - 步骤函数本身也是普通函数,可以用
assert、可以抛异常,失败了 pytest 照常报告。
pytest-bdd的价值在于用例描述与实现分离:改业务描述不用动断言代码,改实现也不影响场景文本的可读性。代价是多了一层Given/When/Then的映射,小项目里可能显得啰嗦。
五、requests:它在测试里到底是什么角色
回到开头那个纠正:requests是 HTTP 客户端库,它的工作是构造请求、发送、解析响应。在自动化测试体系里,它扮演的是辅助工具——测试「接口」时,用它发请求,再用assert断言响应。
# 适用于 Python 3.10+(需要 pip install requests pytest)
import pytest
import requests
def test_public_api_shape():
"""对一个公开的只读接口做最基本的契约检查。"""
r = requests.get("https://example.com/api/status", timeout=10)
assert r.status_code == 200
assert r.headers["Content-Type"].startswith("application/json")
payload = r.json()
assert isinstance(payload, dict)
assert "version" in payload把它放进这个分类表里,位置就清楚了:
| 名字 | 是不是「测试框架」 | 真正角色 |
|---|
unittest | 是 | 标准库测试框架 |
pytest | 是 | 第三方测试框架,也是前者的替代/超集 |
Selenium | 否 | 浏览器自动化驱动库 |
pytest-bdd | 否(是插件) | 挂在 pytest 上的 BDD 插件 |
requests | 否 | HTTP 客户端库,测试里的辅助工具 |
它们常见的组合方式是:用pytest组织用例和 fixture,用requests测接口,用Selenium测界面,需要业务可读场景时加pytest-bdd。unittest则在不想引入第三方依赖时顶替pytest的位置。
顺便说一句合规与伦理:用requests或Selenium做测试时,目标应该是你自己负责的系统,或明确提供测试环境的公开接口。拿它们去批量打扰别人的站点,无论包装成「压测」还是「爬虫」,都不合适——限速、可识别 UA、遵守 robots.txt 这些规则在测试场景里同样适用。
常见坑点
1. 把requests当测试框架。
❌ 在介绍「自动化测试框架」时把requests和pytest并列,以为它负责收集和运行用例。 ✅requests只发 HTTP 请求;用例收集、断言报告由pytest/unittest负责。
2.unittest里把断言写成assert x == y却奇怪地「全过」。
❌ 在TestCase子类里用普通函数名(如check_foo)命名用例 —— 不会被收集,自然也不失败。 ✅ 方法名必须以test开头,且断言优先用self.assertEqual等方法,报告信息更全。
3.setUp当成「整个类只跑一次」。
❌ 把昂贵的初始化写在setUp里,以为只执行一次 —— 它每条用例前都跑。 ✅ 一次性准备用setUpClass(类方法);每条用例前的准备才用setUp。
4. Selenium 忘了driver.quit()。
❌ 测试抛异常后直接退出,浏览器进程残留,跑几十条用例后机器被拖垮。 ✅ 用try / finally保证driver.quit()一定执行;用 pytest 时可以写成 fixture 的清理段。
5. 以为 Selenium 要手工下载驱动。
❌ 到处找「chromedriver 与 Chrome 版本对应表」然后手动塞进 PATH。 ✅ Selenium 4.6 起自带的 Selenium Manager 会默认处理驱动的发现、下载与缓存;只有在它解决不了时才需要手工指定。
6.pytest-bdd的步骤文本与 feature 文件对不上。
❌ feature 里写When 我加入一件价格为 10 的商品,@when装饰器里写的是另一个字面量 —— 场景找不到步骤,报StepDefinitionNotFoundError。 ✅ 装饰器里的字符串必须与 feature 文件逐字一致;重复的步骤说明可以抽成公共步骤。
7. 测试直接打真实生产环境。
❌requests.get()和Selenium都指向线上地址,测试一跑就给线上刷流量。 ✅ 指向测试环境或本地服务;确需访问公开接口时也要限速、只做只读检查。
8. 认为Selenium就是「测试框架」,于是也拿它去跑接口测试。
❌ 用Selenium去调接口 —— 启动整个浏览器只为发一个 HTTP 请求,慢且脆弱。 ✅ 接口层面用requests,只有需要真实渲染和交互时才用Selenium。
总结
| 名字 | 类别 | 一句话职责 | 安装方式 |
|---|
unittest | 测试框架 | 类式用例、自带断言 | 标准库,免装 |
pytest | 测试框架 | 函数式用例、裸assert、fixture、参数化 | pip install pytest |
Selenium | 浏览器驱动 | 操作真实浏览器,做端到端测试 | pip install selenium |
pytest-bdd | BDD 插件 | 把 Gherkin 场景映射成 pytest 用例 | pip install pytest-bdd |
requests | HTTP 客户端 | 在测试里发请求,属于工具而非框架 | pip install requests |
回到标题:把这五个名字并称「自动化测试框架」是一种笼统的说法。准确的理解是——两个框架(unittest、pytest)负责「怎么组织与运行测试」,一个插件(pytest-bdd)负责「怎么用业务语言描述测试」,两个库(Selenium、requests)负责「怎么与被测系统交互」。分清这四层,选型时就不会再拿浏览器驱动去做接口测试,也不会指望 HTTP 客户端帮你生成测试报告。